pax_global_header00006660000000000000000000000064146365420420014520gustar00rootroot0000000000000052 comment=0795eefc78f365e6c4658d74e79d02bbf1528be9 engrbm87-notifications_android_tv-0795eef/000077500000000000000000000000001463654204200206575ustar00rootroot00000000000000engrbm87-notifications_android_tv-0795eef/.github/000077500000000000000000000000001463654204200222175ustar00rootroot00000000000000engrbm87-notifications_android_tv-0795eef/.github/workflows/000077500000000000000000000000001463654204200242545ustar00rootroot00000000000000engrbm87-notifications_android_tv-0795eef/.github/workflows/release.yml000066400000000000000000000011021463654204200264110ustar00rootroot00000000000000name: Release on: release: types: - created jobs: publish: strategy: fail-fast: false runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.1.7 - uses: actions/setup-python@v5.1.0 with: python-version: "3.10" - name: Run image uses: abatilo/actions-poetry@v3.0.0 with: poetry-version: "1.7.1" - name: Publish env: PYPI_TOKEN: ${{ secrets.PYPI_PASSWORD }} run: | poetry config pypi-token.pypi $PYPI_TOKEN poetry publish --build engrbm87-notifications_android_tv-0795eef/.github/workflows/tests.yml000066400000000000000000000024301463654204200261400ustar00rootroot00000000000000name: Testing on: push: branches: [dev] pull_request: branches: [dev] env: DEFAULT_PYTHON: 3.9 jobs: build: name: Run Tests runs-on: ubuntu-latest strategy: matrix: python-version: - "3.10" - "3.11" - "3.12" steps: - name: Checkout uses: actions/checkout@v3.0.2 with: fetch-depth: 2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4.2.0 with: python-version: ${{ matrix.python-version }} - name: Install Poetry uses: snok/install-poetry@v1 with: virtualenvs-create: true virtualenvs-in-project: true installer-parallel: true - name: Load cached venv id: cached-poetry-dependencies uses: actions/cache@v2 with: path: .venv key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} - name: Install dependencies if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' run: poetry install --no-interaction --no-root - name: Install library run: poetry install --no-interaction - name: Run tests run: | source .venv/bin/activate pytest tests/ engrbm87-notifications_android_tv-0795eef/.gitignore000066400000000000000000000003561463654204200226530ustar00rootroot00000000000000# pytest .pytest_cache .cache # GITHUB Proposed Python stuff: *.py[cod] __pycache__ pytest-*.txt # Visual Studio Code .vscode/* !.vscode/cSpell.json !.vscode/extensions.json !.vscode/tasks.json .env # mypy /.mypy_cache/* /.dmypy.json engrbm87-notifications_android_tv-0795eef/.pre-commit-config.yaml000066400000000000000000000023471463654204200251460ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-docstring-first - id: check-yaml - id: debug-statements - id: no-commit-to-branch args: - --branch=dev - --branch=master - repo: https://gitlab.com/pycqa/flake8 rev: 4.0.1 hooks: - id: flake8 args: - --max-line-length=500 - --ignore=E203,E266,E501,W503 - --max-complexity=18 - --select=B,C,E,F,W,T4,B9 - repo: https://github.com/ambv/black rev: 22.8.0 hooks: - id: black language_version: python3 - repo: https://github.com/asottile/pyupgrade rev: v2.31.0 hooks: - id: pyupgrade args: ["--py39-plus"] - repo: https://github.com/pre-commit/mirrors-isort rev: v5.10.1 hooks: - id: isort args: - --multi-line=3 - --trailing-comma - --force-grid-wrap=0 - --use-parentheses - --line-width=88 - -p=homeassistant - --force-sort-within-sections - repo: https://github.com/PyCQA/pydocstyle rev: 6.1.1 hooks: - id: pydocstyle engrbm87-notifications_android_tv-0795eef/.vscode/000077500000000000000000000000001463654204200222205ustar00rootroot00000000000000engrbm87-notifications_android_tv-0795eef/.vscode/extensions.json000066400000000000000000000003411463654204200253100ustar00rootroot00000000000000{ "recommendations": [ "charliermarsh.ruff", "ms-python.pylint", "ms-python.vscode-pylance", "visualstudioexptteam.vscyarodeintellicode", "github.vscode-pull-request-github", "eamodio.gitlens" ] } engrbm87-notifications_android_tv-0795eef/.vscode/settings.json000066400000000000000000000002001463654204200247430ustar00rootroot00000000000000{ "python.testing.pytestArgs": ["tests"], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true } engrbm87-notifications_android_tv-0795eef/CHANGES.rst000066400000000000000000000013101463654204200224540ustar00rootroot00000000000000Changes ======= 20220915 - 1.0.0 ---------------- - Switch to httpx to make the library Async (@erngrbm87 - #9) - Use poetry for pypi builds (@erngrbm87 - #10) - Add tests (@erngrbm87 - #12) - Use Enum for message parameters (@erngrbm87 - #13) - Add helper method to get image from path or url (@erngrbm87 - #14) - Add example file (@erngrbm87 - #17) 20220329 - 0.1.5 ---------------- - Fix setup by @tkdrob in #8 20220324 - 0.1.4 ---------------- - Update MANIFEST.in, add missing file (@onkelbeh - #3) - Pin this project to Python 3.8 or higher (@tkdrob - #5) - Add test port function (@tkdrob - #6) - Add static type checking support (@tkdrob - #7) 20210706 - 0.1.0 ---------------- - Initial release engrbm87-notifications_android_tv-0795eef/LICENSE000066400000000000000000000021111463654204200216570ustar00rootroot00000000000000# MIT License Copyright (c) 2021-2022 Rami Mousleh 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. engrbm87-notifications_android_tv-0795eef/README.md000066400000000000000000000036161463654204200221440ustar00rootroot00000000000000# Android TV / Fire TV Notifications Python package that interfaces with [Notifications for Android TV](https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google) and [Notifications for Fire TV](https://play.google.com/store/apps/details?id=de.cyberdream.firenotifications.google) to send notifications to your TV. ## Usage - Install the application on your TV - Get the IP of the TV unit ```python from notifications_android_tv import Notifications notify = Notifications("192.168.1.10") # validate connection try: await notify.async_connect() expect ConnectError: return False await notify.async_send( "message text", title="Title text", ) ``` ## Optional parameters - `title`: Notification title - `duration`: Display the notification for the specified period. Default is 5 seconds - `fontsize`: Text font size. Use `FontSizes` class to set the fontsize. Default is `FontSizes.MEDIUM` - `position`: Notification position. Use `Positions` class to set position. Default is `Positions.BOTTOM_RIGHT`. - `bkgcolor`: Notification background color. Use `BkgColors` class to set color. Default is `BkgColors.GREY`. - `transparency`: Background transparency of the notification. Use `Transparencies` class. Default is `Transparencies._0_PERCENT`. - `interrupt`: Setting it to `True` makes the notification interactive and can be dismissed or selected to display more details. Default is `False` - `icon`: Can be `str` represnting the file path or an `ImageUrlSource` that includes the url and authentication params to fetch the image from a url. - `image_file`: Can be `str` represnting the file path or an `ImageUrlSource` that includes the url and authentication params to fetch the image from a url. Refer to the [example file](example.py) for setting these parameters directly or from a data dictionary (as documented in ) engrbm87-notifications_android_tv-0795eef/example.py000066400000000000000000000032321463654204200226640ustar00rootroot00000000000000"""Example scripts for sending notifications.""" import asyncio import logging from notifications_android_tv import ConnectError, Notifications from notifications_android_tv.exceptions import NotificationException from notifications_android_tv.notifier import NotificationParams _LOGGER = logging.getLogger(__name__) HOST = "" ICON = "" IMAGE = "" async def main() -> None: """Run the example script.""" notifier = Notifications(HOST) # validate connection try: await notifier.async_connect() except ConnectError as err: _LOGGER.error(err) return # # Send a basic notification with message only await notifier.async_send("This is a simple notification message") # For constructing paramters from string values as documented # in Home Assistant https://www.home-assistant.io/integrations/nfandroidtv try: notification_params = NotificationParams.from_dict( { "duration": "10", "color": "red", "fontsize": "small", "position": "bottom-right", "transparency": "25%", "interrupt": 0, "icon": {"path": ICON}, "image": {"url": IMAGE}, } ) except ValueError as err: _LOGGER.error(err) return try: await notifier.async_send( "This is a notification message", title="Notification Title", params=notification_params, ) except NotificationException as err: _LOGGER.error(err) if __name__ == "__main__": asyncio.run(main()) engrbm87-notifications_android_tv-0795eef/notifications_android_tv/000077500000000000000000000000001463654204200257415ustar00rootroot00000000000000engrbm87-notifications_android_tv-0795eef/notifications_android_tv/__init__.py000066400000000000000000000007421463654204200300550ustar00rootroot00000000000000"""Library for sending notifications to Android/Fire TVs.""" from .notifier import Notifications from .helpers import ImageSource, NotificationParams from .exceptions import ( ConnectError, InvalidImage, InvalidImageData, InvalidResponse, NotificationException, ) __all__ = [ "Notifications", "ImageSource", "NotificationParams", "ConnectError", "InvalidImage", "InvalidImageData", "InvalidResponse", "NotificationException", ] engrbm87-notifications_android_tv-0795eef/notifications_android_tv/const.py000066400000000000000000000034201463654204200274400ustar00rootroot00000000000000"""Constants for the library.""" from enum import Enum, IntEnum from typing import Final class BkgColor(Enum): """Background color options.""" GREY = "#607d8b" BLACK = "#000000" INDIGO = "#303F9F" GREEN = "#4CAF50" RED = "#F44336" CYAN = "#00BCD4" TEAL = "#009688" AMBER = "#FFC107" PINK = "#E91E63" class FontSize(IntEnum): """Supported font sizes for notification text.""" SMALL = 1 MEDIUM = 0 LARGE = 2 MAX = 3 class Position: """Position of the notification. Supported values: - 0: Bottom right - 1: Bottom left - 2: Top right - 3: Top left - 4: Center """ @classmethod def from_string(cls, position: str) -> int: """Convert position to int.""" _mapping = { "bottom-right": 0, "bottom-left": 1, "top-right": 2, "top-left": 3, "center": 4, } return _mapping.get(position, 0) class Transparency: """Transparency for the notification overlay. Supported values: - 1: 0% - 2; 25% - 3: 50% - 4: 75% - 5: 100% """ @classmethod def from_percentage(cls, percentage: str) -> int: """Convert percentage to int.""" _mapping = { "0%": 1, "25%": 2, "50%": 3, "75%": 4, "100%": 5, } return _mapping.get(percentage, 1) DEFAULT_TITLE: Final = "Notification" DEFAULT_DURATION: Final = 5 DEFAULT_POSITION: Final = 0 DEFAULT_BKGCOLOR: Final = BkgColor.GREY DEFAULT_FONTSIZE: Final = FontSize.MEDIUM DEFAULT_TRANSPARENCY: Final = 1 DEFAULT_ICON: Final = ( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGP6zwAAAgcBApo" "cMXEAAAAASUVORK5CYII=" ) engrbm87-notifications_android_tv-0795eef/notifications_android_tv/exceptions.py000066400000000000000000000010331463654204200304710ustar00rootroot00000000000000"""Exceptions raised by the library.""" class NotificationException(Exception): """Base class for all exceptions raised by the library.""" class ConnectError(NotificationException): """Exception raised for connection error.""" class InvalidResponse(NotificationException): """Exception raised for invalid response.""" class InvalidImage(NotificationException): """Exception raised for invalid image.""" class InvalidImageData(NotificationException): """Exception raised for invalid image data is provided.""" engrbm87-notifications_android_tv-0795eef/notifications_android_tv/helpers.py000066400000000000000000000161201463654204200277550ustar00rootroot00000000000000"""Helper methods for the library.""" from __future__ import annotations import base64 from dataclasses import dataclass from typing import Any import httpx from .const import ( DEFAULT_BKGCOLOR, DEFAULT_DURATION, DEFAULT_FONTSIZE, DEFAULT_ICON, DEFAULT_POSITION, DEFAULT_TRANSPARENCY, BkgColor, FontSize, Position, Transparency, ) from .exceptions import ConnectError, InvalidImage, InvalidImageData @dataclass class ImageSource: """Image source from url or local path.""" path: str | None = None url: str | None = None auth: httpx.Auth | None = None @classmethod def from_path(cls, path: str) -> ImageSource: """Initiate image source class.""" return cls(path=path) @classmethod def from_url( cls, url: str, username: str | None = None, password: str | None = None, auth: str | None = None, ) -> ImageSource: """Initiate image source class.""" _cls = cls(url=url) if auth: if auth not in ["basic", "digest"]: raise ValueError(f"Invalid auth '{auth}', must be 'basic' or 'digest'") if username is None or password is None: raise ValueError("username and password must be specified") if auth == "basic": _cls.auth = httpx.BasicAuth(username, password) else: _cls.auth = httpx.DigestAuth(username, password) return _cls async def async_get_image(self) -> bytes: """Load file from path or url.""" if self.path is not None: try: with open(self.path, "rb") as file: return file.read() except FileNotFoundError as err: raise InvalidImage(err) from err if self.url is not None: try: async with httpx.AsyncClient(verify=False) as client: response = await client.get(self.url, auth=self.auth, timeout=30) except (httpx.ConnectError, httpx.TimeoutException) as err: raise ConnectError( f"Error fetching image from {self.url}: {err}" ) from err if response.status_code != httpx.codes.OK: raise InvalidImage(f"Error fetching image from {self.url}: {response}") if "image" not in response.headers["content-type"]: raise InvalidImage( f"Response content type is not an image: {response.headers['content-type']}" ) return response.content raise ValueError("Either path or url must be specified") @dataclass class NotificationParams: """Notification parameters. :param duration: (Optional) Display the notification for the specified period. Default duration is 5 seconds. :param position: (Optional) Specify notification position from class Position. Default is `Positions.BOTTOM_RIGHT`. :param fontsize: (Optional) Specify text font size from class FontSize. Default is `FontSizes.MEDIUM`. :param color: (Optional) Specify background color from class BkgColor. Default is `BkgColors.GREY`. :param transparency: (Optional) Specify the background transparency of the notification from class `Transparency`. Default is 0%. :param interrupt: (Optional) Setting it to true makes the notification interactive and can be dismissed or selected to display more details. Default is False :param icon: (Optional) Attach icon to notification. Construct using ImageSource. :param image_file: (Optional) Attach image to notification. Construct using ImageSource. """ duration: int = DEFAULT_DURATION position: int = DEFAULT_POSITION fontsize: FontSize = DEFAULT_FONTSIZE transparency: int = DEFAULT_TRANSPARENCY color: BkgColor = DEFAULT_BKGCOLOR interrupt: bool = False icon: ImageSource | None = None image: ImageSource | None = None @property def data_params(self) -> dict[str, Any]: """Return notification parameters as a dict.""" return { "duration": self.duration, "position": self.position, "fontsize": self.fontsize.value, "transparency": self.transparency, "color": self.color.value, "interrupt": self.interrupt, } async def get_images(self) -> dict[str, Any]: """Return notification icon and image.""" icon_bytes = base64.b64decode(DEFAULT_ICON) if self.icon is not None: icon_bytes = await self.icon.async_get_image() files = { "filename": ( "image", icon_bytes, "application/octet-stream", {"Expires": "0"}, ) } if self.image is not None: image_bytes = await self.image.async_get_image() files["filename2"] = ( "image", image_bytes, "application/octet-stream", {"Expires": "0"}, ) return files @classmethod def from_dict(cls, kwargs: dict[str, Any]) -> NotificationParams: """Initiate notification parameters class.""" _params: dict[str, Any] = {} if duration := kwargs.get("duration"): try: _params["duration"] = int(duration) except ValueError: _params["duration"] = DEFAULT_DURATION if position := kwargs.get("position"): _params["position"] = Position.from_string(position) if (font_size := kwargs.get("fontsize")) and hasattr( FontSize, font_size.upper() ): _params["fontsize"] = getattr(FontSize, font_size.upper()) if transparency := kwargs.get("transparency"): _params["transparency"] = Transparency.from_percentage(transparency) if (color := kwargs.get("color")) and hasattr(BkgColor, color.upper()): _params["color"] = getattr(BkgColor, color.upper()) if interrupt := kwargs.get("interrupt"): _params["interrupt"] = bool(interrupt) if icon := kwargs.get("icon"): _params["icon"] = create_image_source("icon", icon) if image := kwargs.get("image"): _params["image"] = create_image_source("image", image) return NotificationParams(**_params) def create_image_source(key: str, data: dict[str, Any]) -> ImageSource: """create image source class.""" if isinstance(data, str): return ( ImageSource.from_url(data) if data.startswith("http") else ImageSource.from_path(data) ) elif isinstance(data, dict) and "path" in data: return ImageSource.from_path(data["path"]) elif ( isinstance(data, dict) and (url := data.get("url")) and (url.startswith("http")) ): try: return ImageSource.from_url(**data) except ValueError as err: raise InvalidImageData(f"Invalid '{key}' data: {str(err)}") from err else: raise InvalidImageData(f"Invalid '{key}' data") engrbm87-notifications_android_tv-0795eef/notifications_android_tv/notifier.py000066400000000000000000000062331463654204200301360ustar00rootroot00000000000000"""Notification class for Android TV.""" import base64 import logging from typing import Any import httpx from .const import ( DEFAULT_ICON, DEFAULT_TITLE, ) from .exceptions import ConnectError, InvalidResponse from .helpers import NotificationParams _LOGGER = logging.getLogger(__name__) class Notifications: """Notifications class for Android/Fire Tvs.""" def __init__( self, host: str, port: int = 7676, httpx_client: httpx.AsyncClient | None = None, ) -> None: """Initialize notifier.""" self.url = f"http://{host}:{port}" self.httpx_client = httpx_client async def async_connect(self) -> None: """Test connecting to server.""" httpx_client: httpx.AsyncClient = self.httpx_client or httpx.AsyncClient( verify=False ) try: async with httpx_client as client: await client.get(self.url, timeout=30) except (httpx.ConnectError, httpx.TimeoutException) as err: raise ConnectError(f"Connection to {self.url} failed: {err}") from err async def async_send( self, message: str, *, title: str | None = None, params: NotificationParams | None = None, ) -> None: """Send message with parameters. :param message: The notification message. :param title: (Optional) The notification title. :params params: (Optional) Notification parameters. Construct using NotificationParams. Usage: >>> from notifications_android_tv import Notifications >>> notifier = Notifications("192.168.3.88") >>> await notifier.async_connect() >>> await notifier.async_send( "message to be sent", title="Notification title", params=NotificationParams( duration=5, position=Positions.BOTTOM_RIGHT, fontsize=FontSize.MEDIUM, ) ) """ data: dict[str, Any] = { "msg": message, "title": title or DEFAULT_TITLE, } if params is not None: data.update(params.data_params) files = await params.get_images() else: icon_bytes = base64.b64decode(DEFAULT_ICON) files = { "filename": ( "image", icon_bytes, "application/octet-stream", {"Expires": "0"}, ) } _LOGGER.debug("data: %s, files: %s", data, files) httpx_client: httpx.AsyncClient = self.httpx_client or httpx.AsyncClient( verify=False ) try: async with httpx_client as client: response = await client.post( self.url, data=data, files=files, timeout=10 ) except (httpx.ConnectError, httpx.TimeoutException) as err: raise ConnectError(f"Error communicating with {self.url}: {err}") from err if response.status_code != httpx.codes.OK: raise InvalidResponse(f"Error sending message: {response}") engrbm87-notifications_android_tv-0795eef/poetry.lock000066400000000000000000000612501463654204200230570ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "anyio" version = "4.4.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" files = [ {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] [[package]] name = "certifi" version = "2024.6.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] [[package]] name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "colorlog" version = "6.8.2" description = "Add colours to the output of Python's logging module." optional = false python-versions = ">=3.6" files = [ {file = "colorlog-6.8.2-py3-none-any.whl", hash = "sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33"}, {file = "colorlog-6.8.2.tar.gz", hash = "sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] development = ["black", "flake8", "mypy", "pytest", "types-colorama"] [[package]] name = "distlib" version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" files = [ {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] name = "exceptiongroup" version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "filelock" version = "3.15.3" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ {file = "filelock-3.15.3-py3-none-any.whl", hash = "sha256:0151273e5b5d6cf753a61ec83b3a9b7d8821c39ae9af9d7ecf2f9e2f17404103"}, {file = "filelock-3.15.3.tar.gz", hash = "sha256:e1199bf5194a2277273dacd50269f0d87d0682088a3c561c15674ea9005d8635"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] [[package]] name = "httpcore" version = "1.0.5" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, ] [package.dependencies] certifi = "*" h11 = ">=0.13,<0.15" [package.extras] asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] trio = ["trio (>=0.22.0,<0.26.0)"] [[package]] name = "httpx" version = "0.27.0" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, ] [package.dependencies] anyio = "*" certifi = "*" httpcore = "==1.*" idna = "*" sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] [[package]] name = "identify" version = "2.5.36" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, ] [package.extras] license = ["ukkonen"] [[package]] name = "idna" version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "nodeenv" version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] [[package]] name = "packaging" version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] name = "platformdirs" version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] type = ["mypy (>=1.8)"] [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" version = "3.7.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, ] [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" [[package]] name = "pytest" version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" version = "0.16.0" description = "Pytest support for asyncio." optional = false python-versions = ">= 3.6" files = [ {file = "pytest-asyncio-0.16.0.tar.gz", hash = "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb"}, {file = "pytest_asyncio-0.16.0-py3-none-any.whl", hash = "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b"}, ] [package.dependencies] pytest = ">=5.4.0" [package.extras] testing = ["coverage", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-httpx" version = "0.30.0" description = "Send responses to httpx." optional = false python-versions = ">=3.9" files = [ {file = "pytest-httpx-0.30.0.tar.gz", hash = "sha256:755b8edca87c974dd4f3605c374fda11db84631de3d163b99c0df5807023a19a"}, {file = "pytest_httpx-0.30.0-py3-none-any.whl", hash = "sha256:6d47849691faf11d2532565d0c8e0e02b9f4ee730da31687feae315581d7520c"}, ] [package.dependencies] httpx = "==0.27.*" pytest = ">=7,<9" [package.extras] testing = ["pytest-asyncio (==0.23.*)", "pytest-cov (==4.*)"] [[package]] name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" files = [ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] [[package]] name = "ruff" version = "0.3.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"}, {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"}, {file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"}, {file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"}, {file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"}, {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"}, {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"}, {file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"}, {file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"}, {file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"}, {file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"}, {file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"}, {file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"}, {file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"}, {file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"}, {file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"}, {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, ] [[package]] name = "sniffio" version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] [[package]] name = "typing-extensions" version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "virtualenv" version = "20.26.2" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [metadata] lock-version = "2.0" python-versions = "^3.10" content-hash = "ea027ae067feaff809d08d4f01271acd2cd448480e1e82d4e257c0aa03ec1fed" engrbm87-notifications_android_tv-0795eef/py.typed000066400000000000000000000000001463654204200223440ustar00rootroot00000000000000engrbm87-notifications_android_tv-0795eef/pyproject.toml000066400000000000000000000016761463654204200236050ustar00rootroot00000000000000[tool.poetry] name = "notifications-android-tv" version = "1.2.2" description = "Python API for sending notifications to Android/Fire TVs" authors = ["Rami Mosleh "] homepage = "https://github.com/engrbm87/notifications_android_tv" repository = "https://github.com/engrbm87/notifications_android_tv/releases" readme = "README.md" license = "MIT" classifiers = [ "Development Status :: 3 - Alpha", "Environment :: Console", "Intended Audience :: Developers", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Topic :: Utilities", ] [tool.poetry.dependencies] python = "^3.10" httpx = "^0.27.0" [tool.poetry.group.dev.dependencies] colorlog = "^6.6.0" ruff = "^0.3.7" pre-commit = "^3.7.1" pytest = "^7.1.2" pytest-httpx = ">0.15,<1" pytest-asyncio = "^0.16.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" engrbm87-notifications_android_tv-0795eef/tests/000077500000000000000000000000001463654204200220215ustar00rootroot00000000000000engrbm87-notifications_android_tv-0795eef/tests/__init__.py000066400000000000000000000000351463654204200241300ustar00rootroot00000000000000"""Tests for the package.""" engrbm87-notifications_android_tv-0795eef/tests/test_helpers.py000066400000000000000000000052231463654204200250760ustar00rootroot00000000000000"""Tests for helper classes.""" import httpx import pytest from pytest_httpx import HTTPXMock from notifications_android_tv import ImageSource from notifications_android_tv import ConnectError, InvalidImage @pytest.mark.asyncio async def test_image_source() -> None: """Test constructing ImageSource.""" # test provding wrong authentication type with pytest.raises(ValueError) as err: ImageSource.from_url(url="http://example.com/image.png", auth="something") assert err == "authentication must be 'basic' or 'digest'" # test missing password with pytest.raises(ValueError) as err: ImageSource.from_url( url="http://example.com/image.png", auth="basic", username="user" ) assert err == "username and password must be specified" # test missing username with pytest.raises(ValueError) as err: ImageSource.from_url( "http://example.com/image.png", auth="basic", password="pass" ) assert err == "username and password must be specified" # test url with basic auth image_source_dict = { "url": "http://example.com/image.png", "auth": "basic", "username": "user", "password": "pass", } image_source = ImageSource.from_url(**image_source_dict) assert image_source.url == "http://example.com/image.png" assert isinstance(image_source.auth, httpx.BasicAuth) # test url with digest auth image_source_dict = { "url": "http://example.com/image.png", "auth": "digest", "username": "user", "password": "pass", } image_source = ImageSource.from_url(**image_source_dict) assert image_source.url == "http://example.com/image.png" assert isinstance(image_source.auth, httpx.DigestAuth) @pytest.mark.asyncio async def test_get_image_fails(httpx_mock: HTTPXMock) -> None: """Test getting an image from source fails.""" image = ImageSource.from_url(url="http://example.com") # test timeout fetching image with pytest.raises(ConnectError): httpx_mock.add_exception(httpx.TimeoutException("")) await image.async_get_image() # test image url doesn't return 200 httpx_mock.add_response(status_code=400) with pytest.raises(InvalidImage): await image.async_get_image() # test returned content is not an image type httpx_mock.add_response(headers={"content-type": "text/html"}) with pytest.raises(InvalidImage): await image.async_get_image() # test getting image non existing file fails image2 = ImageSource.from_path("image_file.jpg") with pytest.raises(InvalidImage): await image2.async_get_image() engrbm87-notifications_android_tv-0795eef/tests/test_notifier.py000066400000000000000000000023541463654204200252550ustar00rootroot00000000000000"""Tests for Notifications.""" import httpx import pytest from pytest_httpx import HTTPXMock from notifications_android_tv import ConnectError, InvalidResponse, Notifications @pytest.mark.asyncio async def test_timeout(httpx_mock: HTTPXMock) -> None: """Test if the connection is hitting the timeout.""" def raise_timeout(request): """Set the timeout for the requests.""" raise httpx.ReadTimeout( f"Unable to read within {request.extensions['timeout']}", request=request ) httpx_mock.add_callback(raise_timeout) with pytest.raises(ConnectError): notifier = Notifications("0.0.0.0") await notifier.async_connect() @pytest.mark.asyncio async def test_sending_failed(httpx_mock: HTTPXMock) -> None: """Test sending a message fails.""" httpx_mock.add_response(status_code=400) notifier = Notifications("0.0.0.0") with pytest.raises(InvalidResponse): await notifier.async_send("Message text") @pytest.mark.asyncio async def test_sending_successfull(httpx_mock: HTTPXMock) -> None: """Test sending a message is successful.""" httpx_mock.add_response(status_code=200) notifier = Notifications("0.0.0.0") await notifier.async_send("Message text")