├── tests ├── conftest.py ├── example-final-screen.yml ├── test_plugin_run.py ├── test_screen_package_utils.py ├── example.yml ├── example-save-state.yml ├── example-system.yml ├── example-multi-package.yml └── test_screen_package_state.py ├── .github ├── semantic.yml ├── dependabot.yml └── workflows │ ├── unit.yml │ ├── release-please.yml │ └── lint.yml ├── yafti ├── share.py ├── screen │ ├── package │ │ ├── __init__.py │ │ ├── screen │ │ │ ├── __init__.py │ │ │ ├── package.py │ │ │ ├── install.py │ │ │ └── picker.py │ │ ├── models.py │ │ ├── utils.py │ │ └── state.py │ ├── utils.py │ ├── dialog.py │ ├── console.py │ ├── consent.py │ ├── title.py │ └── window.py ├── registry.py ├── __init__.py ├── setup.py ├── log.py ├── __main__.py ├── parser.py ├── events.py ├── abc.py ├── app.py └── plugin │ ├── run.py │ └── flatpak.py ├── renovate.json ├── pkg ├── PKGBUILD └── rpm.spec ├── pyproject.toml ├── .gitignore ├── README.md ├── LICENSE ├── CHANGELOG.md └── poetry.lock /tests/conftest.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | enabled: true 2 | titleOnly: true 3 | -------------------------------------------------------------------------------- /yafti/share.py: -------------------------------------------------------------------------------- 1 | BTN_NEXT = None 2 | BTN_BACK = None 3 | -------------------------------------------------------------------------------- /yafti/screen/package/__init__.py: -------------------------------------------------------------------------------- 1 | from .screen.package import PackageScreen # noqa 2 | -------------------------------------------------------------------------------- /yafti/screen/package/screen/__init__.py: -------------------------------------------------------------------------------- 1 | from .install import PackageInstallScreen # noqa 2 | from .picker import PackagePickerScreen # noqa 3 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /yafti/registry.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Marco Ceppi 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from importlib.metadata import entry_points 5 | 6 | _plugins = entry_points(group="yafti.plugin") 7 | PLUGINS = {s.name: s.load()() for s in _plugins} 8 | 9 | _screens = entry_points(group="yafti.screen") 10 | SCREENS = {s.name: s.load() for s in _screens} 11 | -------------------------------------------------------------------------------- /yafti/screen/package/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import RootModel, BaseModel 2 | 3 | 4 | PackageConfig = RootModel[dict[str, str | dict]] 5 | 6 | 7 | class PackageGroupConfigDetails(BaseModel): 8 | description: str 9 | default: bool = True 10 | packages: list[PackageConfig] 11 | 12 | 13 | PackageGroupConfig = RootModel[dict[str, PackageGroupConfigDetails]] 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /yafti/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2023 Marco Ceppi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | -------------------------------------------------------------------------------- /yafti/setup.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | import gi 5 | from gi.events import GLibEventLoopPolicy 6 | from rich.logging import RichHandler 7 | 8 | logging.basicConfig( 9 | level="NOTSET", 10 | format="%(message)s", 11 | datefmt="[%X]", 12 | handlers=[ 13 | RichHandler( 14 | show_path=False, 15 | show_time=False, 16 | rich_tracebacks=True, 17 | tracebacks_suppress=[gi, logging], 18 | ) 19 | ], 20 | ) 21 | 22 | gi.require_version("Gtk", "4.0") 23 | gi.require_version("Adw", "1") 24 | 25 | asyncio.set_event_loop_policy(GLibEventLoopPolicy()) 26 | -------------------------------------------------------------------------------- /yafti/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | __all__ = ["info", "warn", "error", "debug", "set_level"] 4 | 5 | _l = logging.getLogger("yafti") 6 | 7 | 8 | def _fmt(msg: dict) -> str: 9 | return " ".join([f"{k}={v}" for k, v in msg.items()]) 10 | 11 | 12 | def set_level(level): 13 | _l.setLevel(level) 14 | 15 | 16 | def debug(message, **kwargs): 17 | _l.debug(f"{message} {_fmt(kwargs)}") 18 | 19 | 20 | def info(message, **kwargs): 21 | _l.info(f"{message} {_fmt(kwargs)}") 22 | 23 | 24 | def warn(message, *kwargs): 25 | _l.warn(f"{message} {_fmt(kwargs)}") 26 | 27 | 28 | def error(message, *kwargs): 29 | _l.error(f"{message} {_fmt(kwargs)}") 30 | -------------------------------------------------------------------------------- /pkg/PKGBUILD: -------------------------------------------------------------------------------- 1 | pkgname=yafti 2 | pkgver=0.10.2 3 | pkgrel=1 4 | pkgdesc="Yet Another First Time Installer" 5 | arch=(any) 6 | depends=(libadwaita gtk4 gobject-introspection python-{pydantic,rich,typer,yaml}) 7 | source=(https://github.com/ublue-os/yafti/archive/refs/tags/v$pkgver.zip) 8 | sha512sums=('e37e72f0cf53c0f1593b1feaeb2499bb038f70b2788a01140a935599a1c846db868082f0eaff17e5312d32c9000bfd40cd078d022fc371d629c5040211c4c920') 9 | makedepends=(python-{build,installer,wheel,poetry,gobject}) 10 | 11 | build() { 12 | cd "$pkgname-$pkgver" 13 | python -m build --wheel --no-isolation 14 | } 15 | 16 | package() { 17 | cd "$pkgname-$pkgver" 18 | python -m installer --destdir="$pkgdir" dist/*.whl 19 | } 20 | -------------------------------------------------------------------------------- /yafti/screen/package/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any 3 | from hashlib import sha256 4 | 5 | 6 | def generate_fingerprint(obj: Any): 7 | return sha256(json.dumps(obj).encode()).hexdigest() 8 | 9 | 10 | def parse_packages(packages: dict | list) -> dict: 11 | output = {} 12 | 13 | if isinstance(packages, dict): 14 | for group, value in packages.items(): 15 | output[f"group:{group}"] = True 16 | output.update(parse_packages(value["packages"])) 17 | return output 18 | 19 | for pkgcfg in packages: 20 | for package in pkgcfg.values(): 21 | if isinstance(package, dict): 22 | package = json.dumps(package) 23 | output[f"pkg:{package}"] = True 24 | return output 25 | -------------------------------------------------------------------------------- /tests/example-final-screen.yml: -------------------------------------------------------------------------------- 1 | title: uBlue First Boot 2 | properties: 3 | mode: "run-on-change" 4 | actions: 5 | pre: 6 | post: 7 | screens: 8 | first-screen: 9 | source: yafti.screen.title 10 | values: 11 | title: "That was pretty cool" 12 | icon: "/path/to/icon" 13 | description: | 14 | Time to play overwatch 15 | final-screen: 16 | source: yafti.screen.title 17 | values: 18 | title: "All done" 19 | icon: "/path/to/icon" 20 | links: 21 | - "Install More Applications": 22 | run: /usr/bin/gnome-software 23 | - "Website": 24 | run: /usr/bin/xdg-open https://ublue.it 25 | - "Join the Discord Community": 26 | run: /usr/bin/xdg-open https://discord.gg/XjG48C7VHx 27 | description: | 28 | Thanks for installing, join the community, next steps 29 | -------------------------------------------------------------------------------- /pkg/rpm.spec: -------------------------------------------------------------------------------- 1 | %global modname yafti 2 | 3 | Name: yafti 4 | Version: 0.10.2 5 | Release: 0%{?dist} 6 | Summary: Yet Another First Time Installer 7 | License: Apache-2.0 8 | URL: https://pypi.io/project/yafti 9 | Source0: https://pypi.io/packages/source/y/%{modname}/%{modname}-%{version}.tar.gz 10 | 11 | BuildArch: noarch 12 | 13 | Requires: libadwaita 14 | BuildRequires: pyproject-rpm-macros 15 | 16 | %generate_buildrequires 17 | %pyproject_buildrequires 18 | 19 | %?python_enable_dependency_generator 20 | 21 | %description 22 | 23 | Yet another first time installer 24 | 25 | $ yafti 26 | 27 | %prep 28 | %autosetup -n %{modname}-%{version} 29 | 30 | %build 31 | %pyproject_wheel 32 | 33 | %install 34 | %pyproject_install 35 | 36 | %check 37 | echo "OKAY" 38 | 39 | %files -n %{modname} 40 | %license LICENSE 41 | %{_bindir}/yafti 42 | %{python3_sitelib}/%{modname}/ 43 | %{python3_sitelib}/%{modname}-%{version}* 44 | -------------------------------------------------------------------------------- /tests/test_plugin_run.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import patch 3 | from subprocess import CompletedProcess 4 | from yafti.plugin.run import Run 5 | from pydantic import ValidationError 6 | 7 | 8 | @pytest.mark.asyncio 9 | @patch.object(Run, "exec") 10 | async def test_run_call_str(mock_exec): 11 | r = Run() 12 | mock_exec.return_value = CompletedProcess( 13 | "hello-world", returncode=0, stdout=b"", stderr=b"" 14 | ) 15 | await r("hello-world") 16 | mock_exec.assert_called_with("hello-world") 17 | 18 | 19 | @pytest.mark.asyncio 20 | @patch.object(Run, "exec") 21 | async def test_run_call_list(mock_exec): 22 | r = Run() 23 | mock_exec.return_value = CompletedProcess( 24 | "hello-world", returncode=0, stdout=b"", stderr=b"" 25 | ) 26 | await r(["hello", "world"]) 27 | mock_exec.assert_called_with("hello world") 28 | 29 | 30 | @pytest.mark.asyncio 31 | async def test_run_call_validation(): 32 | r = Run() 33 | with pytest.raises(ValidationError): 34 | await r({"hello": "world"}) 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_run_exec(): 39 | r = Run() 40 | await r.exec("ls") 41 | -------------------------------------------------------------------------------- /yafti/screen/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2023 Marco Ceppi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | 18 | class NoParentFound(Exception): 19 | """No parent matched""" 20 | 21 | 22 | def find_parent(obj, cls=None): 23 | """Traverse to the parent of a GTK4 component 24 | 25 | Args: 26 | obj: A GTK4 derived component 27 | cls: Parent component to find 28 | 29 | Returns: 30 | The instance of the parent component 31 | 32 | Raises: 33 | NoParentFound: if a cls is passed and all parents are traversed without a match 34 | """ 35 | 36 | p = obj.get_parent() 37 | if cls: 38 | if isinstance(p, cls): 39 | return p 40 | if p is None: 41 | raise NoParentFound(f"no matching parent found for {cls}") 42 | 43 | if p is None: 44 | return obj 45 | 46 | return find_parent(p, cls) 47 | -------------------------------------------------------------------------------- /.github/workflows/unit.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | pytest: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: 3.11 19 | - name: Install Poetry 20 | uses: snok/install-poetry@v1 21 | with: 22 | version: 1.4.0 23 | virtualenvs-create: true 24 | virtualenvs-in-project: true 25 | - name: System Deps 26 | run: | 27 | sudo apt update 28 | sudo apt install libgirepository1.0-dev libgtk-3-dev libadwaita-1-dev 29 | - name: Cache Dependencies 30 | id: cache-deps 31 | uses: actions/cache@v4 32 | with: 33 | path: .venv 34 | key: pydeps-${{ hashFiles('**/poetry.lock') }} 35 | - name: Install Dependencies 36 | run: poetry install --no-interaction --no-root 37 | if: steps.cache-deps.outputs.cache-hit != 'true' 38 | - name: Install Project 39 | run: poetry install --no-interaction 40 | - name: Check code formatting 41 | run: poetry run black --check yafti 42 | - name: Lint code 43 | run: poetry run ruff yafti 44 | - name: Run pytest 45 | run: poetry run pytest --cov=yafti --cov-report=term-missing 46 | -------------------------------------------------------------------------------- /yafti/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2023 Marco Ceppi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | import logging 18 | from typing import Annotated 19 | 20 | import typer 21 | import yaml 22 | 23 | import yafti.setup # noqa 24 | from yafti import log 25 | from yafti.app import Yafti 26 | from yafti.parser import Config 27 | 28 | 29 | def run( 30 | config: typer.FileText = typer.Argument("/etc/yafti.yml"), 31 | debug: bool = False, 32 | force_run: Annotated[ 33 | bool, typer.Option("-f", "--force", help="Ignore run mode and force run") 34 | ] = False, 35 | ): 36 | log.set_level(logging.DEBUG if debug else logging.INFO) 37 | log.debug("starting up", config=config, debug=debug) 38 | config = Config.parse_obj(yaml.safe_load(config)) 39 | app = Yafti(config) 40 | app.run(None, force_run=force_run) 41 | 42 | 43 | def app(): 44 | typer.run(run) 45 | 46 | 47 | if __name__ == "__main__": 48 | app() 49 | -------------------------------------------------------------------------------- /yafti/screen/package/state.py: -------------------------------------------------------------------------------- 1 | from pydantic import validate_call 2 | 3 | 4 | class PackageScreenState: 5 | __slots__ = ["state"] 6 | 7 | def __new__(cls, id: str): 8 | if not hasattr(cls, "instances"): 9 | cls.instances = {} 10 | if id not in cls.instances: 11 | cls.instances[id] = super(PackageScreenState, cls).__new__(cls) 12 | return cls.instances[id] 13 | 14 | def __init__(self, id: str): 15 | self.state = {} 16 | 17 | @validate_call 18 | def load(self, data: dict): 19 | for k, v in data.items(): 20 | self.set(k, v) 21 | 22 | @validate_call 23 | def remove(self, item: str) -> None: 24 | del self.state[item] 25 | 26 | @validate_call 27 | def on(self, item: str) -> None: 28 | self.set(item, True) 29 | 30 | @validate_call 31 | def off(self, item: str) -> None: 32 | self.set(item, False) 33 | 34 | @validate_call 35 | def toggle(self, item: str) -> bool: 36 | self.state[item] = not self.state[item] 37 | return self.get(item) 38 | 39 | @validate_call 40 | def set(self, item: str, state: bool) -> None: 41 | self.state[item] = state 42 | 43 | @validate_call 44 | def get_on(self, prefix: str = "") -> list[str]: 45 | return [ 46 | item 47 | for item, value in self.state.items() 48 | if item.startswith(prefix) and value is True 49 | ] 50 | 51 | def keys(self) -> list[str]: 52 | return list(self.state.keys()) 53 | 54 | @validate_call 55 | def get(self, item: str) -> bool: 56 | return self.state.get(item) 57 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "yafti" 3 | version = "0.10.2" 4 | description = "Yet another first time installer" 5 | authors = ["Marco Ceppi "] 6 | license = "Apache 2.0" 7 | readme = "README.md" 8 | homepage = "https://github.com/ublue-os/yafti" 9 | repository = "https://github.com/ublue-os/yafti" 10 | classifiers = [ 11 | "Environment :: X11 Applications :: GTK", 12 | "Framework :: AsyncIO", 13 | "Intended Audience :: End Users/Desktop", 14 | "Topic :: System :: Software Distribution", 15 | ] 16 | 17 | [tool.poetry.scripts] 18 | yafti = "yafti.__main__:app" 19 | 20 | [tool.poetry.dependencies] 21 | python = "^3.11" 22 | pydantic = "^2.8.2" 23 | pygobject = "^3.50.0" 24 | pyyaml = "^6.0" 25 | rich = "^13.3.2" 26 | typer = ">=0.7" 27 | 28 | [tool.poetry.plugins."yafti.plugin"] 29 | "yafti.plugin.flatpak" = "yafti.plugin.flatpak:Flatpak" 30 | "yafti.plugin.run" = "yafti.plugin.run:Run" 31 | "run" = "yafti.plugin.run:Run" 32 | 33 | [tool.poetry.plugins."yafti.screen"] 34 | "yafti.screen.title" = "yafti.screen.title:TitleScreen" 35 | "yafti.screen.package" = "yafti.screen.package:PackageScreen" 36 | "yafti.screen.console" = "yafti.screen.console:ConsoleScreen" 37 | "yafti.screen.consent" = "yafti.screen.consent:ConsentScreen" 38 | 39 | [tool.poetry.group.dev.dependencies] 40 | black = ">=23.1,<25.0" 41 | isort = "^5.12.0" 42 | pytest = ">=7.2.1,<9.0.0" 43 | ruff = ">=0.0.254,<0.3.5" 44 | coverage = "^7.2.1" 45 | pytest-cov = "^6.0.0" 46 | pytest-asyncio = ">=0.25.1,<0.26.0" 47 | 48 | [tool.isort] 49 | profile = "black" 50 | multi_line_output = 3 51 | 52 | [build-system] 53 | requires = ["poetry-core>=1.2.0"] 54 | build-backend = "poetry.core.masonry.api" 55 | -------------------------------------------------------------------------------- /tests/test_screen_package_utils.py: -------------------------------------------------------------------------------- 1 | from yafti.screen.package.utils import parse_packages 2 | 3 | 4 | def test_parse_packages_groups(): 5 | cfg = { 6 | "Core": { 7 | "description": "hello world", 8 | "packages": [ 9 | {"Calculator": "org.gnome.Calculator"}, 10 | { 11 | "Firefox": { 12 | "package": "org.mozilla.firefox", 13 | "system": True, 14 | "user": False, 15 | }, 16 | }, 17 | ], 18 | }, 19 | "Gaming": { 20 | "description": "hello games", 21 | "default": False, 22 | "packages": [ 23 | {"Steam": "com.valvesoftware.Steam"}, 24 | {"Games": "org.gnome.Games"}, 25 | ], 26 | }, 27 | } 28 | 29 | expected = { 30 | "group:Core": True, 31 | "pkg:org.gnome.Calculator": True, 32 | 'pkg:{"package": "org.mozilla.firefox", "system": true, "user": false}': True, 33 | "group:Gaming": True, 34 | "pkg:com.valvesoftware.Steam": True, 35 | "pkg:org.gnome.Games": True, 36 | } 37 | 38 | assert expected == parse_packages(cfg) 39 | 40 | 41 | def test_parse_packages_list(): 42 | cfg = [ 43 | {"Calculator": "org.gnome.Calculator"}, 44 | {"Firefox": "org.mozilla.firefox"}, 45 | {"Steam": "com.valvesoftware.Steam"}, 46 | {"Games": "org.gnome.Games"}, 47 | ] 48 | 49 | expected = { 50 | "pkg:org.gnome.Calculator": True, 51 | "pkg:org.mozilla.firefox": True, 52 | "pkg:com.valvesoftware.Steam": True, 53 | "pkg:org.gnome.Games": True, 54 | } 55 | 56 | assert expected == parse_packages(cfg) 57 | -------------------------------------------------------------------------------- /yafti/parser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2023 Marco Ceppi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | from enum import Enum 18 | from pathlib import Path 19 | from typing import Optional 20 | 21 | import yaml 22 | from pydantic import BaseModel 23 | 24 | 25 | class ActionConfig(BaseModel): 26 | pre: Optional[list[dict[str, str | dict]]] = None 27 | post: Optional[list[dict[str, str | dict]]] = None 28 | 29 | 30 | class ScreenConfig(BaseModel): 31 | source: str 32 | values: Optional[dict] = None 33 | 34 | 35 | class YaftiRunModes(str, Enum): 36 | changed = "run-on-change" 37 | ignore = "run-once" 38 | disable = "disabled" 39 | 40 | 41 | class YaftSaveState(str, Enum): 42 | always = "always" 43 | end = "last-screen" 44 | 45 | 46 | class YaftiProperties(BaseModel): 47 | path: Optional[Path] = Path("~/.config/yafti/last-run") 48 | mode: YaftiRunModes = YaftiRunModes.changed 49 | save_state: YaftSaveState = YaftSaveState.always 50 | 51 | 52 | class Config(BaseModel): 53 | title: str 54 | properties: YaftiProperties = YaftiProperties() 55 | actions: Optional[ActionConfig] = None 56 | screens: Optional[dict[str, ScreenConfig]] = None 57 | 58 | 59 | def parse(config_file: str) -> Config: 60 | """Parse the YAML or JSON file passed and return a rendered Config object""" 61 | with open(config_file) as f: 62 | cfg = yaml.safe_load(f) 63 | return Config.parse_obj(cfg) 64 | -------------------------------------------------------------------------------- /yafti/screen/dialog.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Adw, Gtk 2 | 3 | _xml = """\ 4 | 5 | 6 | 7 | 8 | 34 | 35 | """ 36 | 37 | 38 | @Gtk.Template(string=_xml) 39 | class DialogBox(Adw.Window): 40 | __gtype_name__ = "YaftiDialog" 41 | 42 | def __init__(self, parent=None, **kwargs): 43 | super().__init__(**kwargs) 44 | if parent: 45 | self.set_transient_for(parent) 46 | 47 | sc = Gtk.ShortcutController.new() 48 | sc.add_shortcut( 49 | Gtk.Shortcut.new( 50 | Gtk.ShortcutTrigger.parse_string("Escape"), 51 | Gtk.CallbackAction.new(lambda x: self.hide()), 52 | ) 53 | ) 54 | self.add_controller(sc) 55 | -------------------------------------------------------------------------------- /yafti/events.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2023 Marco Ceppi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | from typing import Any 18 | 19 | _listeners = {} 20 | 21 | 22 | class EventException(Exception): 23 | """Event system encountered a problem""" 24 | 25 | 26 | class EventAlreadyRegisteredError(EventException): 27 | """Event name already exists""" 28 | 29 | 30 | class EventNotRegisteredError(EventException): 31 | """Event name does not exist""" 32 | 33 | 34 | def register(event_name): 35 | if event_name in _listeners: 36 | raise EventAlreadyRegisteredError( 37 | "event is already registered", event=event_name 38 | ) 39 | _listeners[event_name] = [] 40 | 41 | 42 | def on(event_name: str, fn: Any): 43 | if event_name not in _listeners: 44 | raise EventNotRegisteredError("event name not registered", event=event_name) 45 | if fn in _listeners[event_name]: 46 | return 47 | _listeners[event_name].insert(0, fn) 48 | 49 | 50 | def detach(event_name: str, fn: Any): 51 | if event_name not in _listeners: 52 | raise EventNotRegisteredError("event name not registered", event=event_name) 53 | _listeners[event_name].remove(fn) 54 | 55 | 56 | async def emit(event_name, *args, **kwargs): 57 | if event_name not in _listeners: 58 | raise EventNotRegisteredError("event name not registered", event=event_name) 59 | 60 | for fn in _listeners[event_name]: 61 | result = await fn(*args, **kwargs) 62 | if result is not False: 63 | break 64 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | permissions: 7 | contents: write 8 | checks: write 9 | actions: read 10 | packages: write 11 | pull-requests: write 12 | 13 | name: Release Please 14 | jobs: 15 | release-please: 16 | runs-on: ubuntu-latest 17 | outputs: 18 | releases_created: ${{ steps.release-please.outputs.releases_created }} 19 | tag: ${{ steps.release-please.outputs.tag_name }} 20 | upload_url: ${{ steps.release-please.outputs.upload_url }} 21 | steps: 22 | - uses: google-github-actions/release-please-action@v4 23 | id: release-please 24 | with: 25 | release-type: python 26 | package-name: yafti 27 | package: 28 | name: Create RPM Release 29 | runs-on: ubuntu-latest 30 | needs: release-please 31 | if: needs.release-please.outputs.releases_created 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@v4 35 | - name: Setup Python 36 | uses: actions/setup-python@v5 37 | with: 38 | python-version: 3.11 39 | - name: Install Poetry 40 | uses: snok/install-poetry@v1 41 | with: 42 | version: 1.4.0 43 | virtualenvs-create: true 44 | virtualenvs-in-project: true 45 | - name: Build and upload to PyPI 46 | run: | 47 | poetry config pypi-token.pypi "$PYPI_UPLOAD_TOKEN" 48 | poetry build 49 | poetry publish 50 | env: 51 | PYPI_UPLOAD_TOKEN: ${{ secrets.PYPI_UPLOAD_TOKEN }} 52 | - name: Build RPM Package 53 | id: rpm_build 54 | uses: ublue-os/rpmbuild@master 55 | with: 56 | spec_file: "pkg/rpm.spec" 57 | - name: Upload release binaries 58 | uses: ublue-os/upload-assets@pass-in-tag 59 | env: 60 | GITHUB_TOKEN: ${{ github.token }} 61 | with: 62 | asset_paths: '["./${{ steps.rpm_build.outputs.source_rpm_path }}*", "./${{ steps.rpm_build.outputs.rpm_dir_path }}noarch/*"]' 63 | tag: ${{ needs.release-please.outputs.tag }} 64 | -------------------------------------------------------------------------------- /tests/example.yml: -------------------------------------------------------------------------------- 1 | title: uBlue First Boot 2 | properties: 3 | mode: "run-on-change" 4 | actions: 5 | pre: 6 | - run: /full/path/to/bin --with --params 7 | - run: /another/command run 8 | - yafti.plugin.flatpak: 9 | install: org.gnome.Calculator 10 | post: 11 | - run: /run/these/commands --after --all --screens 12 | screens: 13 | first-screen: 14 | source: yafti.screen.title 15 | values: 16 | title: "That was pretty cool" 17 | icon: "/path/to/icon" 18 | description: | 19 | Time to play overwatch 20 | can-we-modify-your-flatpaks: 21 | source: yafti.screen.consent 22 | values: 23 | title: Welcome traveler 24 | condition: 25 | run: flatpak remotes --system | grep fedora 26 | description: | 27 | This tool modifies your flatpaks and flatpak sources. If you do not want to do this exit the installer. 28 | For new users just do it (tm) 29 | actions: 30 | - run: flatpak remote-delete fedora --force 31 | - run: flatpak remove --system --noninteractive --all 32 | applications: 33 | source: yafti.screen.package 34 | values: 35 | title: Install flatpaks 36 | show_terminal: true 37 | package_manager: yafti.plugin.flatpak 38 | groups: 39 | Core: 40 | description: All the good stuff 41 | packages: 42 | - Calculator: org.gnome.Calculator 43 | - Firefox: org.mozilla.firefox 44 | Gaming: 45 | description: GAMES GAMES GAMES 46 | default: false 47 | packages: 48 | - Steam: com.valvesoftware.Steam 49 | - Games: org.gnome.Games 50 | Office: 51 | description: All the work stuff 52 | default: false 53 | packages: 54 | - LibreOffice: org.libreoffice.LibreOffice 55 | - Calendar: org.gnome.Calendar 56 | final-screen: 57 | source: yafti.screen.title 58 | values: 59 | title: "All done" 60 | icon: "/atph/to/icon" 61 | description: | 62 | Thanks for installing, join the community, next steps 63 | -------------------------------------------------------------------------------- /tests/example-save-state.yml: -------------------------------------------------------------------------------- 1 | title: uBlue First Boot 2 | properties: 3 | mode: "run-on-change" 4 | save_state: "last-screen" 5 | actions: 6 | pre: 7 | - run: /full/path/to/bin --with --params 8 | - run: /another/command run 9 | - yafti.plugin.flatpak: 10 | install: org.gnome.Calculator 11 | post: 12 | - run: /run/these/commands --after --all --screens 13 | screens: 14 | first-screen: 15 | source: yafti.screen.title 16 | values: 17 | title: "That was pretty cool" 18 | icon: "/path/to/icon" 19 | description: | 20 | Time to play overwatch 21 | can-we-modify-your-flatpaks: 22 | source: yafti.screen.consent 23 | values: 24 | title: Welcome traveler 25 | condition: 26 | run: flatpak remotes --system | grep fedora 27 | description: | 28 | This tool modifies your flatpaks and flatpak sources. If you do not want to do this exit the installer. 29 | For new users just do it (tm) 30 | actions: 31 | - run: flatpak remote-delete fedora --force 32 | - run: flatpak remove --system --noninteractive --all 33 | applications: 34 | source: yafti.screen.package 35 | values: 36 | title: Install flatpaks 37 | show_terminal: true 38 | package_manager: yafti.plugin.flatpak 39 | groups: 40 | Core: 41 | description: All the good stuff 42 | packages: 43 | - Calculator: org.gnome.Calculator 44 | - Firefox: org.mozilla.firefox 45 | Gaming: 46 | description: GAMES GAMES GAMES 47 | default: false 48 | packages: 49 | - Steam: com.valvesoftware.Steam 50 | - Games: org.gnome.Games 51 | Office: 52 | description: All the work stuff 53 | default: false 54 | packages: 55 | - LibreOffice: org.libreoffice.LibreOffice 56 | - Calendar: org.gnome.Calendar 57 | final-screen: 58 | source: yafti.screen.title 59 | values: 60 | title: "All done" 61 | icon: "/atph/to/icon" 62 | description: | 63 | Thanks for installing, join the community, next steps 64 | -------------------------------------------------------------------------------- /tests/example-system.yml: -------------------------------------------------------------------------------- 1 | title: uBlue First Boot 2 | properties: 3 | mode: "run-on-change" 4 | actions: 5 | pre: 6 | - run: /full/path/to/bin --with --params 7 | - run: /another/command run 8 | - yafti.plugin.flatpak: 9 | install: org.gnome.Calculator 10 | post: 11 | - run: /run/these/commands --after --all --screens 12 | screens: 13 | first-screen: 14 | source: yafti.screen.title 15 | values: 16 | title: "That was pretty cool" 17 | icon: "/path/to/icon" 18 | description: | 19 | Time to play overwatch 20 | can-we-modify-your-flatpaks: 21 | source: yafti.screen.consent 22 | values: 23 | title: Welcome traveler 24 | condition: 25 | run: flatpak remotes --system | grep fedora 26 | description: | 27 | This tool modifies your flatpaks and flatpak sources. If you do not want to do this exit the installer. 28 | For new users just do it (tm) 29 | actions: 30 | - run: flatpak remote-delete fedora --force 31 | - run: flatpak remove --system --noninteractive --all 32 | applications: 33 | source: yafti.screen.package 34 | values: 35 | title: Package Installation 36 | show_terminal: true 37 | package_manager: yafti.plugin.flatpak 38 | package_manager_defaults: 39 | user: false 40 | system: true 41 | groups: 42 | Core: 43 | description: All the good stuff 44 | packages: 45 | - Calculator: org.gnome.Calculator 46 | - Firefox: 47 | package: org.mozilla.firefox 48 | system: false 49 | user: true 50 | Gaming: 51 | description: GAMES GAMES GAMES 52 | default: false 53 | packages: 54 | - Steam: com.valvesoftware.Steam 55 | - Games: org.gnome.Games 56 | Office: 57 | description: All the work stuff 58 | default: false 59 | packages: 60 | - LibreOffice: org.libreoffice.LibreOffice 61 | - Calendar: org.gnome.Calendar 62 | final-screen: 63 | source: yafti.screen.title 64 | values: 65 | title: "All done" 66 | icon: "/atph/to/icon" 67 | description: | 68 | Thanks for installing, join the community, next steps 69 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | black: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: 3.11 19 | - name: Install Poetry 20 | uses: snok/install-poetry@v1 21 | with: 22 | version: 1.4.0 23 | virtualenvs-create: true 24 | virtualenvs-in-project: true 25 | - name: System Deps 26 | run: | 27 | sudo apt update 28 | sudo apt install libgirepository1.0-dev libgtk-3-dev libadwaita-1-dev 29 | - name: Cache Dependencies 30 | id: cache-deps 31 | uses: actions/cache@v4 32 | with: 33 | path: .venv 34 | key: pydeps-${{ hashFiles('**/poetry.lock') }} 35 | - name: Install Dependencies 36 | run: poetry install --no-interaction --no-root 37 | if: steps.cache-deps.outputs.cache-hit != 'true' 38 | - name: Install Project 39 | run: poetry install --no-interaction 40 | - name: Check code formatting 41 | run: poetry run black --check yafti 42 | ruff: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: Setup Python 47 | uses: actions/setup-python@v5 48 | with: 49 | python-version: 3.11 50 | - name: Install Poetry 51 | uses: snok/install-poetry@v1 52 | with: 53 | version: 1.4.0 54 | virtualenvs-create: true 55 | virtualenvs-in-project: true 56 | - name: System Deps 57 | run: | 58 | sudo apt update 59 | sudo apt install libgirepository1.0-dev libgtk-3-dev libadwaita-1-dev 60 | - name: Cache Dependencies 61 | id: cache-deps 62 | uses: actions/cache@v4 63 | with: 64 | path: .venv 65 | key: pydeps-${{ hashFiles('**/poetry.lock') }} 66 | - name: Install Dependencies 67 | run: poetry install --no-interaction --no-root 68 | if: steps.cache-deps.outputs.cache-hit != 'true' 69 | - name: Install Project 70 | run: poetry install --no-interaction 71 | - name: Lint code 72 | run: poetry run ruff yafti 73 | 74 | -------------------------------------------------------------------------------- /yafti/abc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2023 Marco Ceppi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | import asyncio 18 | from inspect import iscoroutinefunction 19 | from typing import Any, Optional 20 | 21 | from pydantic import BaseModel 22 | 23 | 24 | class YaftiPlugin: 25 | pass 26 | 27 | 28 | async def _show_screen(condition): 29 | from yafti.registry import PLUGINS 30 | 31 | plugin_name = list(condition.keys())[0] 32 | plugin = PLUGINS.get(plugin_name) 33 | result = await plugin(condition[plugin_name]) 34 | return result.code == 0 35 | 36 | 37 | class YaftiScreenConfig(BaseModel): 38 | condition: Optional[dict[str, str | dict]] = None 39 | 40 | 41 | class YaftiScreen: 42 | active = False 43 | 44 | class Config(YaftiScreenConfig): 45 | pass 46 | 47 | @classmethod 48 | async def from_config(cls, cfg: Any): 49 | c = cls.Config.parse_obj(cfg) 50 | show = True 51 | if c.condition: 52 | show = await _show_screen(c.condition) 53 | if not show: 54 | return None 55 | 56 | return cls(**c.dict(exclude={"condition"})) 57 | 58 | def activate(self): 59 | self.active = True 60 | if hasattr(self, "on_activate"): 61 | if iscoroutinefunction(self.on_activate): 62 | asyncio.ensure_future(self.on_activate()) 63 | else: 64 | self.on_activate() 65 | 66 | def deactivate(self): 67 | self.active = False 68 | if hasattr(self, "on_deactivate"): 69 | if iscoroutinefunction(self.on_deactivate): 70 | asyncio.ensure_future(self.on_deactivate()) 71 | else: 72 | self.on_deactivate() 73 | 74 | 75 | class YaftiPluginReturn(BaseModel): 76 | output: Optional[str | list[str]] = None 77 | errors: Optional[str | list[str]] = None 78 | code: int = 0 79 | -------------------------------------------------------------------------------- /tests/example-multi-package.yml: -------------------------------------------------------------------------------- 1 | title: uBlue First Boot 2 | properties: 3 | mode: "run-on-change" 4 | actions: 5 | pre: 6 | - run: /full/path/to/bin --with --params 7 | - run: /another/command run 8 | - yafti.plugin.flatpak: 9 | install: org.gnome.Calculator 10 | post: 11 | - run: /run/these/commands --after --all --screens 12 | screens: 13 | first-screen: 14 | source: yafti.screen.title 15 | values: 16 | title: "That was pretty cool" 17 | icon: "/path/to/icon" 18 | description: | 19 | Time to play overwatch 20 | can-we-modify-your-flatpaks: 21 | source: yafti.screen.consent 22 | values: 23 | title: Welcome traveler 24 | condition: 25 | run: flatpak remotes --system | grep fedora 26 | description: | 27 | This tool modifies your flatpaks and flatpak sources. If you do not want to do this exit the installer. 28 | For new users just do it (tm) 29 | actions: 30 | - run: flatpak remote-delete fedora --force 31 | - run: flatpak remove --system --noninteractive --all 32 | applications-three: 33 | source: yafti.screen.package 34 | values: 35 | title: Install more flatpaks 36 | show_terminal: true 37 | package_manager: yafti.plugin.flatpak 38 | packages: 39 | - Steam: com.valvesoftware.Steam 40 | - Games: org.gnome.Games 41 | applications: 42 | source: yafti.screen.package 43 | values: 44 | title: Install flatpaks 45 | show_terminal: true 46 | package_manager: yafti.plugin.flatpak 47 | groups: 48 | Core: 49 | description: All the good stuff 50 | packages: 51 | - Calculator: org.gnome.Calculator 52 | - Firefox: org.mozilla.firefox 53 | applications-two: 54 | source: yafti.screen.package 55 | values: 56 | title: Install more flatpaks 57 | show_terminal: true 58 | package_manager: yafti.plugin.flatpak 59 | groups: 60 | Office: 61 | description: All the work stuff 62 | default: false 63 | packages: 64 | - LibreOffice: org.libreoffice.LibreOffice 65 | - Calendar: org.gnome.Calendar 66 | final-screen: 67 | source: yafti.screen.title 68 | values: 69 | title: "All done" 70 | icon: "/atph/to/icon" 71 | description: | 72 | Thanks for installing, join the community, next steps 73 | -------------------------------------------------------------------------------- /tests/test_screen_package_state.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from yafti.screen.package.state import PackageScreenState 3 | from pydantic import ValidationError 4 | 5 | 6 | def test_state_set(): 7 | state = PackageScreenState("test_state_set") 8 | state.set("hello", True) 9 | assert state.get("hello") is True 10 | 11 | 12 | def test_state_set_fail(): 13 | state = PackageScreenState("test_state_set_fail") 14 | with pytest.raises(ValidationError): 15 | state.set("hello", "world") 16 | 17 | 18 | def test_state_load(): 19 | input = {"hello": True, "world": False} 20 | state = PackageScreenState("test_state_load") 21 | state.load(input) 22 | assert state.get("hello") is True 23 | assert state.get("world") is False 24 | 25 | 26 | def test_state_remove(): 27 | state = PackageScreenState("test_state_remove") 28 | state.set("kenobi", False) 29 | state.set("general", True) 30 | assert state.get("kenobi") is False 31 | assert state.get("general") is True 32 | state.remove("kenobi") 33 | assert state.get("kenobi") is None 34 | assert state.get("general") is True 35 | 36 | 37 | def test_state_on_off(): 38 | state = PackageScreenState("test_state_on_off") 39 | state.on("grievous") 40 | assert state.get("grievous") is True 41 | state.off("grievous") 42 | assert state.get("grievous") is False 43 | 44 | state.off("ani") 45 | assert state.get("ani") is False 46 | state.on("ani") 47 | assert state.get("ani") is True 48 | 49 | 50 | def test_state_toggle(): 51 | state = PackageScreenState("test_state_toggle") 52 | state.on("chewy") 53 | assert state.get("chewy") is True 54 | state.toggle("chewy") 55 | assert state.get("chewy") is False 56 | state.toggle("chewy") 57 | assert state.get("chewy") is True 58 | 59 | 60 | def test_state_toggle_error(): 61 | state = PackageScreenState("test_state_toggle_error") 62 | with pytest.raises(KeyError): 63 | state.toggle("barf") 64 | 65 | 66 | def test_state_get_on(): 67 | state = PackageScreenState("test_state_get_on") 68 | state.on("chewy") 69 | state.on("han") 70 | state.off("greedo") 71 | 72 | assert state.get_on() == ["chewy", "han"] 73 | assert state.get_on("ch") == ["chewy"] 74 | 75 | 76 | def test_state_keys(): 77 | state = PackageScreenState("test_state_keys") 78 | state.on("AA") 79 | state.on("BB") 80 | state.off("CC") 81 | 82 | assert state.keys() == ["AA", "BB", "CC"] 83 | -------------------------------------------------------------------------------- /yafti/screen/console.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk 2 | 3 | from yafti.abc import YaftiScreen 4 | 5 | _xml = """\ 6 | 7 | 8 | 9 | 27 | 28 | """ 29 | 30 | 31 | @Gtk.Template(string=_xml) 32 | class ConsoleScreen(YaftiScreen, Gtk.ScrolledWindow): 33 | __gtype_name__ = "YaftiConsoleScreen" 34 | 35 | console_output = Gtk.Template.Child() 36 | 37 | def stdout(self, text): 38 | if isinstance(text, bytes): 39 | t = text.decode() 40 | for line in t.split("\n"): 41 | if not line: 42 | continue 43 | self.stdout(Gtk.Text(text=line)) 44 | else: 45 | self.console_output.append(text) 46 | self.scroll_to_bottom() 47 | 48 | def stderr(self, text): 49 | if isinstance(text, bytes): 50 | t = text.decode() 51 | for line in t.split("\n"): 52 | if not line: 53 | continue 54 | self.stderr(Gtk.Text(text=line)) 55 | else: 56 | self.console_output.append(text) 57 | self.scroll_to_bottom() 58 | 59 | def scroll_to_bottom(self): 60 | adj = self.get_vadjustment() 61 | adj.set_value(adj.get_upper()) 62 | self.set_vadjustment(adj) 63 | 64 | def scroll_to_top(self): 65 | adj = self.get_vadjustment() 66 | adj.set_value(adj.get_lower()) 67 | 68 | def hide(self): 69 | self.set_visible(False) 70 | 71 | def show(self): 72 | self.set_visible(True) 73 | 74 | def toggle_visible(self): 75 | self.set_visible(self.get_visible() is False) 76 | -------------------------------------------------------------------------------- /yafti/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2023 Marco Ceppi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | import hashlib 18 | 19 | import yaml 20 | from gi.repository import Adw 21 | from pathlib import Path 22 | 23 | from yafti.parser import Config, YaftiRunModes, YaftSaveState 24 | from yafti.screen.window import Window 25 | 26 | 27 | class Yafti(Adw.Application): 28 | def __init__(self, cfg: Config = None): 29 | super().__init__(application_id="it.ublue.Yafti") 30 | self.config = cfg 31 | 32 | def run(self, *args, force_run: bool = False, **kwargs): 33 | configured_mode = self.config.properties.mode 34 | _p: Path = self.config.properties.path.expanduser() 35 | # TODO(GH-#103): Remove this prior to 1.0 release. Start. 36 | _old_p = Path("~/.config/yafti-last-run").expanduser() 37 | if _old_p.exists() and _old_p.resolve() != _p.resolve(): 38 | if not _p.parent.is_dir(): 39 | _p.parent.mkdir(mode=0o755, parents=True, exist_ok=True) 40 | if _p.is_file(): 41 | _p.unlink() 42 | _old_p.rename(_p) 43 | # TODO(GH-#103): End. 44 | if not force_run: 45 | if configured_mode == YaftiRunModes.disable: 46 | return 47 | 48 | if configured_mode == YaftiRunModes.changed: 49 | if _p.exists() and _p.read_text() == self.config_sha: 50 | return 51 | 52 | if configured_mode == YaftiRunModes.ignore and _p.exists(): 53 | return 54 | 55 | super().run(*args, **kwargs) 56 | 57 | def do_activate(self): 58 | self._win = Window(application=self) 59 | self._win.present() 60 | 61 | @property 62 | def config_sha(self): 63 | return hashlib.sha256(yaml.dump(self.config.dict()).encode()).hexdigest() 64 | 65 | def sync_last_run(self): 66 | p = self.config.properties.path.expanduser() 67 | if not p.parent.is_dir(): 68 | p.parent.mkdir(mode=0o755, parents=True, exist_ok=True) 69 | p.write_text(self.config_sha) 70 | 71 | def quit(self, *args, **kwargs): 72 | if self.config.properties.save_state == YaftSaveState.always: 73 | self.sync_last_run() 74 | 75 | if ( 76 | self.config.properties.save_state == YaftSaveState.end 77 | and self._win 78 | and self._win.is_last_page 79 | ): 80 | self.sync_last_run() 81 | 82 | super().quit() 83 | -------------------------------------------------------------------------------- /yafti/plugin/run.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2023 Marco Ceppi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | /f 17 | 18 | Run a command on the system 19 | 20 | Configuration usage example: 21 | 22 | commands: 23 | pre: 24 | # Simple config 25 | - run: /usr/bin/whoami 26 | # Explicit full path plugin 27 | - yafti.plugin.run: /usr/bin/whoami 28 | # Run with parameters 29 | - run: /bin/ls -lah 30 | - run: ["/bin/ls", "-lah"] 31 | - run: 32 | - /bin/ls 33 | - "-lah" 34 | 35 | 36 | Programmatic usage example: 37 | 38 | from yafti.plugin.run import Run 39 | r = Run() 40 | r.exec(["/usr/bin/whoami"]) 41 | f.exec(pkg="com.github.marcoceppi.PackageName", reinstall=True) 42 | 43 | r("/usr/bin/whoami") 44 | r(cmd="/usr/bin/whoami") 45 | r(cmd=["/usr/bin/whoami"]) 46 | 47 | """ 48 | 49 | import asyncio 50 | import shlex 51 | import subprocess 52 | from os.path import isfile 53 | from shutil import which 54 | 55 | from pydantic import validate_call 56 | 57 | from yafti import log 58 | from yafti.abc import YaftiPlugin, YaftiPluginReturn 59 | 60 | 61 | class Run(YaftiPlugin): 62 | async def exec(self, cmd: str) -> subprocess.CompletedProcess: 63 | log.debug("running command", cmd=cmd) 64 | 65 | # spawn command in host when running from container 66 | is_container = isfile("/run/.containerenv") or isfile("/.dockerenv") 67 | if not isfile(cmd) and is_container: 68 | if which("distrobox-host-exec"): 69 | cmd = f"distrobox-host-exec {cmd}" 70 | elif which("flatpak-spawn"): 71 | cmd = f"flatpak-spawn --host {cmd}" 72 | 73 | proc = await asyncio.create_subprocess_shell( 74 | cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE 75 | ) 76 | 77 | stdout, stderr = await proc.communicate() 78 | 79 | log.info("command complete", cmd=cmd, code=proc.returncode) 80 | log.debug( 81 | "command complete", 82 | cmd=cmd, 83 | code=proc.returncode, 84 | stdout=stdout, 85 | stderr=stderr, 86 | ) 87 | 88 | return subprocess.CompletedProcess( 89 | cmd, returncode=proc.returncode, stdout=stdout, stderr=stderr 90 | ) 91 | 92 | async def install(self, package: str) -> YaftiPluginReturn: 93 | """Execute a command on the host system 94 | 95 | Args: 96 | package: The command to execute 97 | 98 | Returns: 99 | An object containing the stdout and stderr from the command 100 | """ 101 | return await self.exec(package) 102 | 103 | @validate_call 104 | async def __call__(self, cmd: list[str] | str) -> YaftiPluginReturn: 105 | log.debug("run called", cmd=cmd) 106 | if isinstance(cmd, list): 107 | cmd = shlex.join(cmd) 108 | 109 | r = await self.exec(cmd) 110 | return YaftiPluginReturn(output=r.stdout, errors=r.stderr, code=r.returncode) 111 | -------------------------------------------------------------------------------- /yafti/screen/consent.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Marco Ceppi 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | Present the user with confirmation (consent) to proceed with modifications 6 | on their system 7 | 8 | Configuration usage example: 9 | 10 | screens: 11 | can-we-modify-your-flatpaks: 12 | source: yafti.screen.consent 13 | values: 14 | title: Welcome traveler 15 | condition: 16 | run: flatpak remotes --system | grep fedora 17 | description: | 18 | This tool modifies your flatpaks and flatpak sources. 19 | If you do not want to do this exit the installer. 20 | For new users just do it (tm) 21 | actions: 22 | - run: flatpak remote-delete fedora --force 23 | - run: flatpak remove --system --noninteractive --all 24 | 25 | Configuration: 26 | 27 | * title: Header of the screen 28 | * description: long form text 29 | * condition: dict of plugin: plugin config. Plugin must return a 0 code to display 30 | screen. Any other code will result in the screen being skipped 31 | * actions: list of plugins to execute once screen is accepted 32 | """ 33 | 34 | import asyncio 35 | from typing import Optional 36 | 37 | from gi.repository import Adw, Gtk 38 | 39 | import yafti.share 40 | from yafti import events 41 | from yafti.abc import YaftiScreen, YaftiScreenConfig 42 | from yafti.registry import PLUGINS 43 | 44 | _xml = """\ 45 | 46 | 47 | 48 | 49 | 62 | 63 | """ 64 | 65 | 66 | @Gtk.Template(string=_xml) 67 | class ConsentScreen(YaftiScreen, Adw.Bin): 68 | __gtype_name__ = "YaftiConsentScreen" 69 | 70 | status_page = Gtk.Template.Child() 71 | 72 | class Config(YaftiScreenConfig): 73 | title: str 74 | description: str 75 | actions: Optional[list[dict[str, str | dict]]] = None 76 | 77 | def __init__( 78 | self, 79 | title: str = None, 80 | description: str = None, 81 | actions: list = None, 82 | condition: dict = None, 83 | **kwargs 84 | ): 85 | super().__init__(**kwargs) 86 | self.status_page.set_title(title) 87 | self.status_page.set_description(description) 88 | self.actions = actions 89 | self.condition = condition 90 | self.already_run = False 91 | 92 | async def on_activate(self): 93 | events.on("btn_next", self.next) 94 | yafti.share.BTN_NEXT.set_label("Accept") 95 | 96 | async def on_deactivate(self): 97 | events.detach("btn_next", self.next) 98 | 99 | async def next(self, _): 100 | if self.already_run: 101 | return False 102 | 103 | to_run = [] 104 | for action in self.actions: 105 | plugin_name = list(action.keys())[0] 106 | plugin = PLUGINS.get(plugin_name) 107 | to_run.append(plugin(action[plugin_name])) 108 | await asyncio.gather(*to_run) 109 | self.already_run = True 110 | return False 111 | -------------------------------------------------------------------------------- /yafti/screen/title.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import hashlib 3 | from functools import partial 4 | from typing import List, Optional 5 | 6 | from gi.repository import Adw, Gtk 7 | 8 | from yafti import events 9 | from yafti.abc import YaftiScreen, YaftiScreenConfig 10 | from yafti.registry import PLUGINS 11 | 12 | _xml = """\ 13 | 14 | 15 | 16 | 17 | 31 | 32 | """ 33 | 34 | 35 | @Gtk.Template(string=_xml) 36 | class TitleScreen(YaftiScreen, Adw.Bin): 37 | __gtype_name__ = "YaftiTitleScreen" 38 | 39 | status_page = Gtk.Template.Child() 40 | 41 | class Config(YaftiScreenConfig): 42 | title: str 43 | description: str 44 | icon: Optional[str] = None 45 | links: List[dict[str, dict]] = None 46 | 47 | def __init__( 48 | self, 49 | title: str = None, 50 | description: str = None, 51 | icon: str = None, 52 | links: List[dict[str, str]] = None, 53 | **kwargs, 54 | ): 55 | super().__init__(**kwargs) 56 | self.status_page.set_title(title) 57 | self.status_page.set_description(description) 58 | 59 | if links: 60 | links_list_box = self.render_links_list_box() 61 | self.append_action_rows(links, links_list_box) 62 | 63 | def render_links_list_box(self): 64 | links_list_box = Gtk.ListBox() 65 | links_list_box.set_selection_mode(Gtk.SelectionMode.NONE) 66 | links_list_box.add_css_class("boxed-list") 67 | self.status_page.set_child(links_list_box) 68 | return links_list_box 69 | 70 | def append_action_rows(self, links, links_list_box): 71 | for link in links: 72 | title, action = list(link.items())[0] 73 | plugin, config = list(action.items())[0] 74 | hash_title = hashlib.md5( 75 | title.encode("utf-8"), usedforsecurity=False 76 | ).hexdigest() 77 | event_name = f"on_action_row_open_{hash_title}" 78 | event_fn = partial( 79 | TitleScreen.on_action_row_open, plugin=plugin, config=config 80 | ) 81 | 82 | events.register(event_name) 83 | events.on(event_name, event_fn) 84 | 85 | def do_emit(*args, **kwargs): 86 | asyncio.create_task(events.emit(*args, **kwargs)) 87 | 88 | _on_clicked = partial(do_emit, event_name) 89 | 90 | link_action_row = Adw.ActionRow() 91 | 92 | action_btn = Gtk.Button() 93 | action_btn.set_label("Open") 94 | action_btn.set_valign(Gtk.Align.CENTER) 95 | action_btn.connect("clicked", _on_clicked) 96 | 97 | link_action_row.set_title(title) 98 | link_action_row.add_suffix(action_btn) 99 | 100 | links_list_box.append(link_action_row) 101 | 102 | @staticmethod 103 | async def on_action_row_open(*args, plugin=None, config=None): 104 | if not plugin and not config: 105 | return 106 | await PLUGINS.get(plugin)(config) 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | !pkg/*.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/#use-with-ide 111 | .pdm.toml 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | #.idea/ 162 | .vscode/ -------------------------------------------------------------------------------- /yafti/screen/package/screen/package.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from gi.repository import Adw, Gtk 4 | 5 | import yafti.share 6 | from yafti import events 7 | from yafti.abc import YaftiScreen, YaftiScreenConfig 8 | from yafti.screen.package.models import PackageConfig, PackageGroupConfig 9 | from yafti.screen.package.screen import PackageInstallScreen, PackagePickerScreen 10 | from yafti.screen.package.state import PackageScreenState 11 | from yafti.screen.package.utils import parse_packages, generate_fingerprint 12 | 13 | _xml = """\ 14 | 15 | 16 | 17 | 31 | 32 | """ 33 | 34 | 35 | @Gtk.Template(string=_xml) 36 | class PackageScreen(YaftiScreen, Adw.Bin): 37 | __gtype_name__ = "YaftiPackageScreen" 38 | 39 | pkg_carousel = Gtk.Template.Child() 40 | 41 | class Config(YaftiScreenConfig): 42 | title: str 43 | show_terminal: bool = True 44 | package_manager: str 45 | groups: Optional[PackageGroupConfig] = None 46 | packages: Optional[list[PackageConfig]] = None 47 | package_manager_defaults: Optional[dict] = None 48 | 49 | def __init__( 50 | self, 51 | title: str = "Package Installation", 52 | package_manager: str = "yafti.plugin.flatpak", 53 | packages: list[PackageConfig] = None, 54 | groups: PackageGroupConfig = None, 55 | show_terminal: bool = True, 56 | package_manager_defaults: Optional[dict] = None, 57 | **kwargs, 58 | ): 59 | super().__init__(**kwargs) 60 | self.title = title 61 | self.packages = groups or packages 62 | self.show_terminal = show_terminal 63 | self.package_manager = package_manager 64 | self.package_manager_defaults = package_manager_defaults 65 | self.fingerprint = generate_fingerprint(self.packages) 66 | self.state = PackageScreenState(self.fingerprint) 67 | self.state.load(parse_packages(self.packages)) 68 | self.pkg_carousel.connect("page-changed", self.changed) 69 | self.draw() 70 | 71 | def draw(self): 72 | self.pkg_carousel.append( 73 | PackagePickerScreen( 74 | state=self.state, title=self.title, packages=self.packages 75 | ) 76 | ) 77 | self.pkg_carousel.append( 78 | PackageInstallScreen( 79 | title=self.title, 80 | state=self.state, 81 | package_manager=self.package_manager, 82 | package_manager_defaults=self.package_manager_defaults, 83 | ) 84 | ) 85 | 86 | def on_activate(self): 87 | events.on("btn_next", self.next) 88 | events.on("btn_back", self.back) 89 | yafti.share.BTN_NEXT.set_label("Install") 90 | 91 | def on_deactivate(self): 92 | events.detach("btn_next", self.next) 93 | events.detach("btn_back", self.back) 94 | 95 | @property 96 | def idx(self): 97 | return self.pkg_carousel.get_position() 98 | 99 | @property 100 | def total(self): 101 | return self.pkg_carousel.get_n_pages() 102 | 103 | def goto(self, page: int, animate: bool = True): 104 | if page < 0: 105 | page = 0 106 | 107 | if page >= self.pkg_carousel.get_n_pages(): 108 | page = self.pkg_carousel.get_n_pages() 109 | 110 | current_screen = self.pkg_carousel.get_nth_page(self.idx) 111 | next_screen = self.pkg_carousel.get_nth_page(page) 112 | 113 | current_screen.deactivate() 114 | self.pkg_carousel.scroll_to(next_screen, animate) 115 | 116 | def changed(self, *args): 117 | current_screen = self.pkg_carousel.get_nth_page(self.idx) 118 | current_screen.activate() 119 | 120 | async def next(self, _): 121 | if not self.active: 122 | return False 123 | if self.idx + 1 == self.total: 124 | return False 125 | self.goto(self.idx + 1) 126 | 127 | async def back(self, _): 128 | if not self.active: 129 | return False 130 | 131 | if self.idx - 1 < 0: 132 | return False 133 | self.goto(self.idx - 1) 134 | -------------------------------------------------------------------------------- /yafti/screen/package/screen/install.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | 4 | from gi.repository import Gtk 5 | from typing import Optional 6 | 7 | import yafti.share 8 | from yafti import events 9 | from yafti import log 10 | from yafti.abc import YaftiScreen 11 | from yafti.screen.console import ConsoleScreen 12 | from yafti.screen.package.state import PackageScreenState 13 | 14 | _xml = """\ 15 | 16 | 17 | 18 | 63 | 64 | """ 65 | 66 | 67 | @Gtk.Template(string=_xml) 68 | class PackageInstallScreen(YaftiScreen, Gtk.Box): 69 | __gtype_name__ = "YaftiPackageInstallScreen" 70 | 71 | status_page = Gtk.Template.Child() 72 | pkg_progress = Gtk.Template.Child() 73 | btn_console = Gtk.Template.Child() 74 | started = False 75 | already_run = False 76 | pulse = True 77 | 78 | def __init__( 79 | self, 80 | state: PackageScreenState, 81 | title: str = "Package Installation", 82 | package_manager: str = "yafti.plugin.flatpak", 83 | package_manager_defaults: Optional[dict] = None, 84 | **kwargs, 85 | ): 86 | super().__init__(**kwargs) 87 | from yafti.registry import PLUGINS 88 | 89 | self.status_page.set_title(title) 90 | self.package_manager = PLUGINS.get(package_manager) 91 | self.package_manager_defaults = package_manager_defaults or {} 92 | self.btn_console.connect("clicked", self.toggle_console) 93 | self.state = state 94 | 95 | async def on_activate(self): 96 | if self.started or self.already_run: 97 | return 98 | self.console = ConsoleScreen() 99 | self.started = True 100 | events.on("btn_next", self.next) 101 | await self.draw() 102 | 103 | async def next(self, _): 104 | return self.started 105 | 106 | def toggle_console(self, btn): 107 | btn.set_label("Show Console" if self.console.get_visible() else "Hide Console") 108 | self.console.toggle_visible() 109 | 110 | async def do_pulse(self): 111 | self.pkg_progress.set_pulse_step(1.0) 112 | while self.pulse: 113 | self.pkg_progress.pulse() 114 | await asyncio.sleep(0.5) 115 | 116 | def draw(self): 117 | self.console.hide() 118 | self.append(self.console) 119 | packages = [item.replace("pkg:", "") for item in self.state.get_on("pkg:")] 120 | asyncio.create_task(self.do_pulse()) 121 | return self.install(packages) 122 | 123 | def run_package_manager(self, packge_config): 124 | try: 125 | config = json.loads(packge_config) 126 | except json.decoder.JSONDecodeError as e: 127 | log.debug("could not parse", config=packge_config, e=e) 128 | config = {"package": packge_config} 129 | 130 | log.debug("parsed packages config", config=config) 131 | opts = self.package_manager_defaults.copy() 132 | opts.update(config) 133 | return self.package_manager.install(**opts) 134 | 135 | async def install(self, packages: list): 136 | total = len(packages) 137 | yafti.share.BTN_NEXT.set_label("Installing...") 138 | yafti.share.BTN_BACK.set_visible(False) 139 | for idx, pkg in enumerate(packages): 140 | results = await self.run_package_manager(pkg) 141 | self.console.stdout(results.stdout) 142 | self.console.stderr(results.stderr) 143 | self.pulse = False 144 | self.pkg_progress.set_fraction((idx + 1) / total) 145 | 146 | self.console.stdout(b"Installation Complete!") 147 | 148 | self.started = False 149 | self.already_run = True 150 | yafti.share.BTN_NEXT.set_label("Next") 151 | yafti.share.BTN_BACK.set_visible(True) 152 | -------------------------------------------------------------------------------- /yafti/screen/window.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from functools import partial 3 | 4 | from gi.repository import Adw, Gtk 5 | 6 | import yafti.share 7 | from yafti import events 8 | from yafti.registry import SCREENS 9 | 10 | _xml = """\ 11 | 12 | 13 | 14 | 15 | 69 | 70 | """ 71 | 72 | 73 | @Gtk.Template(string=_xml) 74 | class Window(Adw.ApplicationWindow): 75 | __gtype_name__ = "YaftiWindow" 76 | 77 | carousel_indicator = Gtk.Template.Child() 78 | carousel = Gtk.Template.Child() 79 | headerbar = Gtk.Template.Child() 80 | btn_back = Gtk.Template.Child() 81 | btn_next = Gtk.Template.Child() 82 | toasts = Gtk.Template.Child() 83 | 84 | def __init__(self, **kwargs): 85 | super().__init__(**kwargs) 86 | 87 | self.app = kwargs.get("application") 88 | events.register("btn_next") 89 | events.register("btn_back") 90 | events.on("btn_next", self.next) 91 | events.on("btn_back", self.back) 92 | 93 | # TODO(GH-2): not a huge fan of this 94 | yafti.share.BTN_BACK = self.btn_back 95 | yafti.share.BTN_NEXT = self.btn_next 96 | 97 | def do_emit(*args, **kwargs): 98 | asyncio.create_task(events.emit(*args, **kwargs)) 99 | 100 | _next = partial(do_emit, "btn_next") 101 | _back = partial(do_emit, "btn_back") 102 | 103 | self.connect("show", self.draw) 104 | self.connect("close-request", self.app.quit) 105 | self.btn_next.connect("clicked", _next) 106 | self.btn_back.connect("clicked", _back) 107 | self.carousel.connect("page-changed", self.changed) 108 | 109 | def draw(self, _) -> None: 110 | asyncio.ensure_future(self.build_screens()) 111 | 112 | async def build_screens(self): 113 | screens = self.app.config.screens 114 | for name, details in screens.items(): 115 | if details.source not in SCREENS: 116 | continue 117 | screen = SCREENS.get(details.source) 118 | s = await screen.from_config(details.values) 119 | if s is None: 120 | continue 121 | self.carousel.append(s) 122 | 123 | @property 124 | def idx(self) -> float: 125 | return self.carousel.get_position() 126 | 127 | def goto(self, page: int, animate: bool = True) -> None: 128 | if page < 0: 129 | page = 0 130 | 131 | if page >= self.carousel.get_n_pages(): 132 | page = self.carousel.get_n_pages() 133 | 134 | current_screen = self.carousel.get_nth_page(self.idx) 135 | next_screen = self.carousel.get_nth_page(page) 136 | 137 | current_screen.deactivate() 138 | self.carousel.scroll_to(next_screen, animate) 139 | 140 | @property 141 | def is_last_page(self): 142 | return self.idx + 1 >= self.carousel.get_n_pages() 143 | 144 | async def next(self, _) -> None: 145 | if self.idx + 1 >= self.carousel.get_n_pages(): 146 | self.app.quit() 147 | else: 148 | self.goto(self.idx + 1) 149 | 150 | async def back(self, _) -> None: 151 | self.goto(self.idx - 1) 152 | 153 | def changed(self, *args) -> None: 154 | self.btn_back.set_visible(self.idx > 0) 155 | current_screen = self.carousel.get_nth_page(self.idx) 156 | if self.idx + 1 >= self.carousel.get_n_pages(): 157 | self.btn_next.set_label("Done") 158 | self.btn_back.set_visible(False) 159 | else: 160 | self.btn_next.set_label("Next") 161 | current_screen.activate() 162 | -------------------------------------------------------------------------------- /yafti/screen/package/screen/picker.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | import json 3 | 4 | from gi.repository import Adw, Gtk 5 | from pydantic import BaseModel 6 | 7 | from yafti import log 8 | from yafti.abc import YaftiScreen 9 | from yafti.screen.dialog import DialogBox 10 | from yafti.screen.package.state import PackageScreenState 11 | from yafti.screen.utils import find_parent 12 | 13 | _xml = """\ 14 | 15 | 16 | 17 | 44 | 45 | """ 46 | 47 | 48 | @Gtk.Template(string=_xml) 49 | class PackagePickerScreen(YaftiScreen, Adw.Bin): 50 | __gtype_name__ = "YaftiPackagePickerScreen" 51 | 52 | status_page = Gtk.Template.Child() 53 | package_list = Gtk.Template.Child() 54 | 55 | class Config(BaseModel): 56 | title: str = "Package Installation" 57 | packages: list | dict 58 | 59 | def __init__( 60 | self, 61 | state: PackageScreenState, 62 | title: str, 63 | packages: list | dict, 64 | **kwargs, 65 | ): 66 | super().__init__(**kwargs) 67 | self.status_page.set_title(title) 68 | self.packages = packages 69 | self.state = state 70 | self.draw() 71 | 72 | def draw(self): 73 | if isinstance(self.packages, list): 74 | for item in self._build_apps(self.packages): 75 | self.package_list.add(item) 76 | return 77 | 78 | for name, details in self.packages.items(): 79 | action_row = Adw.ActionRow(title=name, subtitle=details.get("description")) 80 | 81 | def state_set(group, _, value): 82 | self.state.set(f"group:{group}", value) 83 | d = self.packages.get(group) 84 | for pkg in d.get("packages", []): 85 | for pkg_name in pkg.values(): 86 | if isinstance(pkg_name, dict): 87 | pkg_name = json.dumps(pkg_name) 88 | self.state.set(f"pkg:{pkg_name}", value) 89 | 90 | state_set(name, None, details.get("default", True)) 91 | _switcher = Gtk.Switch() 92 | _switcher.set_active(self.state.get(f"group:{name}")) 93 | _switcher.set_valign(Gtk.Align.CENTER) 94 | 95 | state_set_fn = partial(state_set, name) 96 | _switcher.connect("state-set", state_set_fn) 97 | action_row.add_suffix(_switcher) 98 | 99 | _customize = Gtk.Button() 100 | _customize.set_icon_name("go-next-symbolic") 101 | _customize.set_valign(Gtk.Align.CENTER) 102 | _customize.add_css_class("flat") 103 | action_row.add_suffix(_customize) 104 | picker_fn = partial(self._build_picker, details.get("packages", [])) 105 | _customize.connect("clicked", picker_fn) 106 | self.package_list.add(action_row) 107 | 108 | def _build_picker(self, packages: list, *args): 109 | dialog = DialogBox(find_parent(self, Gtk.Window)) 110 | 111 | btn_cancel = Gtk.Button() 112 | btn_save = Gtk.Button() 113 | btn_cancel.set_label("Cancel") 114 | btn_save.set_label("Save") 115 | btn_save.add_css_class("suggested-action") 116 | 117 | header = Adw.HeaderBar() 118 | header.pack_start(btn_cancel) 119 | header.pack_end(btn_save) 120 | header.set_show_end_title_buttons(False) 121 | header.set_show_start_title_buttons(False) 122 | 123 | item_list = Adw.PreferencesGroup() 124 | item_list.set_description( 125 | "The following list includes only applications available in your preferred " 126 | "package manager." 127 | ) 128 | page = Adw.PreferencesPage() 129 | page.add(item_list) 130 | 131 | box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) 132 | box.append(header) 133 | box.append(page) 134 | 135 | dialog.set_content(box) 136 | dialog.set_default_size(500, 600) 137 | 138 | for item in self._build_apps(packages): 139 | item_list.add(item) 140 | 141 | btn_cancel.connect("clicked", lambda x: dialog.close()) 142 | btn_save.connect("clicked", lambda x: dialog.close()) 143 | dialog.show() 144 | 145 | def _build_apps(self, packages: list): 146 | for item in packages: 147 | for name, pkg in item.items(): 148 | _apps_action_row = Adw.ActionRow( 149 | title=name, 150 | ) 151 | _app_switcher = Gtk.Switch() 152 | if isinstance(pkg, dict): 153 | pkg = json.dumps(pkg) 154 | _app_switcher.set_active(self.state.get(f"pkg:{pkg}")) 155 | _app_switcher.set_valign(Gtk.Align.CENTER) 156 | 157 | def set_state(pkg, btn, value): 158 | log.debug("state-set", pkg=pkg, value=value) 159 | self.state.set(f"pkg:{pkg}", value) 160 | 161 | set_state_func = partial(set_state, pkg) 162 | _app_switcher.connect("state-set", set_state_func) 163 | _apps_action_row.add_suffix(_app_switcher) 164 | yield _apps_action_row 165 | -------------------------------------------------------------------------------- /yafti/plugin/flatpak.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2023 Marco Ceppi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | \f 17 | 18 | Install, remove, list, and manage flatpaks 19 | 20 | Configuration usage example: 21 | 22 | commands: 23 | pre: 24 | # Install a Flatpak package 25 | - yafti.plugin.flatpak: com.github.marcoceppi.PackageName 26 | # Explicit install 27 | - yafti.plugin.flatpak: 28 | install: com.github.marcoceppi.PackageName 29 | # Install with options 30 | - yafti.plugin.flatpak: 31 | install: 32 | pkg: com.github.marcoceppi.PackageName 33 | update: true 34 | user: true 35 | # Remove a flatpak package 36 | - yafti.plugin.flatpak: 37 | remove: com.github.marcoceppi.PackageName 38 | # Remove with options 39 | - yafti.plugin.flatpak: 40 | remove: 41 | pkg: com.github.marcoceppi.PackageName 42 | force: true 43 | 44 | Programmatic usage example: 45 | 46 | from yafti.plugin.flatpak import Flatpak 47 | f = Flatpak() 48 | f.install("com.github.marcoceppi.PackageName") 49 | f.install(pkg="com.github.marcoceppi.PackageName", reinstall=True) 50 | 51 | f("com.github.marcoceppi.PackageName") 52 | f(install="com.github.marcoceppi.PackageName") 53 | f(install={"pkg": "com.github.marcoceppi.PackageName", "reinstall": True}) 54 | 55 | f.remove("com.github.marcoceppi.PackageName") 56 | f.remove(pkg="com.github.marcoceppi.PackageName", force=True) 57 | 58 | f(remove="com.github.marcoceppi.PackageName") 59 | f(remove={"pkg": "com.github.marcoceppi.PackageName", "force": True}) 60 | """ 61 | 62 | import asyncio 63 | from typing import Any, Optional 64 | 65 | from pydantic import BaseModel, ValidationError, field_validator 66 | 67 | from yafti.abc import YaftiPluginReturn 68 | from yafti.plugin.run import Run 69 | 70 | 71 | class ApplicationDetail(BaseModel): 72 | """Flatpak application information""" 73 | 74 | id: str 75 | name: str 76 | version: str 77 | branch: str 78 | installation: str 79 | 80 | 81 | class FlatpakException(Exception): 82 | """Flatpak binary encountered a problem""" 83 | 84 | 85 | class FlatpakInstallError(FlatpakException): 86 | """Flatpak package install failed""" 87 | 88 | 89 | class FlatpakRemoveError(FlatpakException): 90 | """Flatpak package removal failed""" 91 | 92 | 93 | class Flatpak(Run): 94 | """ 95 | Install, remove, list, and manage flatpaks 96 | 97 | Attributes: 98 | bin: The full POSIX path to the flatpak binary on disk 99 | """ 100 | 101 | class Scheme(BaseModel): 102 | """Flatpak plugin configuration validation""" 103 | 104 | install: Optional[str | dict] = None 105 | remove: Optional[str | dict] = None 106 | 107 | @field_validator("install", "remove") 108 | def must_have_atleast_one(cls, values): 109 | """Validate one, and only one, key is passed 110 | 111 | Returns: 112 | A dict of the already parsed values from Pydantic 113 | 114 | Raises: 115 | ValueError: A violation of the validation rule. 116 | """ 117 | if values.get("install") is None and values.get("remove") is None: 118 | raise ValueError("Either install or remove is required") 119 | if values.get("install") is not None and values.get("remove") is not None: 120 | raise ValueError("Only a single install or remove can be passed") 121 | return values 122 | 123 | def __init__(self): 124 | """Verify that flatpak binary exists on the host machine""" 125 | self.bin = "/usr/bin/flatpak" 126 | 127 | def validate(self, options: Any) -> Scheme: 128 | """Sanitize and validate inputs 129 | 130 | Args: 131 | options: Plugin arguments to be validated and sanitized 132 | 133 | Returns: 134 | An object of the parsed input options 135 | 136 | Raises: 137 | ValidationError: Input could not be sanitized and did not conform to 138 | validation rules 139 | """ 140 | if isinstance(options, str): 141 | options = {"install": options} 142 | 143 | return self.Scheme.parse_obj(options) 144 | 145 | def _parse_args(self, **kwargs): 146 | """Map a series of boolean keyword arguments to command-line flags""" 147 | arg_map = {"update": "or-update"} 148 | 149 | return [f"--{arg_map.get(k, k)}" for k, v in kwargs.items() if v is True] 150 | 151 | async def install( 152 | self, 153 | package: str, 154 | user: bool = True, 155 | system: bool = False, 156 | assumeyes: bool = True, 157 | reinstall: bool = False, 158 | noninteractive: bool = True, 159 | update: bool = True, 160 | ) -> YaftiPluginReturn: 161 | """Install flatpak package on the host system 162 | 163 | Args: 164 | package: Name of the flatpak package to install 165 | user: Install on the user installation 166 | system: Install on the system-wide installation 167 | assumeyes: 168 | 169 | Returns: 170 | An object containing the stdout and stderr from the flatpak command 171 | 172 | Raises: 173 | FlatpakInstallError: An error occurred trying to install the Flatpak 174 | """ 175 | args = self._parse_args( 176 | user=user, 177 | system=system, 178 | assumeyes=assumeyes, 179 | reinstall=reinstall, 180 | update=update, 181 | noninteractive=noninteractive, 182 | ) 183 | cmd = [self.bin, "install"] 184 | cmd.extend(args) 185 | cmd.append(package) 186 | return await self.exec(" ".join(cmd)) 187 | 188 | async def remove( 189 | self, 190 | package: str, 191 | user: bool = False, 192 | system: bool = True, 193 | force: bool = False, 194 | noninteractive: bool = True, 195 | ) -> YaftiPluginReturn: 196 | """Remove flatpak package on the host system""" 197 | args = self._parse_args( 198 | user=user, system=system, force=force, noninteractive=noninteractive 199 | ) 200 | cmd = [self.bin, "remove"] 201 | cmd.extend(args) 202 | cmd.append(package) 203 | return self.exec(cmd) 204 | 205 | def ls(self) -> list[ApplicationDetail]: 206 | pass 207 | 208 | def __call__(self, options) -> YaftiPluginReturn: 209 | try: 210 | params = self.validate(options) 211 | except ValidationError as e: 212 | return YaftiPluginReturn(errors=str(e), code=1) 213 | 214 | # TODO: when a string is passed, make sure it maps to the "pkg" key. 215 | if params.install: 216 | if isinstance(params.install, str): 217 | params.install = {"package": params.install} 218 | r = asyncio.ensure_future(self.install(**params.install)) 219 | else: 220 | if isinstance(params.remove, str): 221 | params.remove = {"package": params.remove} 222 | r = asyncio.ensure_future(self.remove(**params.install)) 223 | return YaftiPluginReturn(output=r.stdout, errors=r.stderr, code=r.returncode) 224 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yet Another First Time Installer 2 | 3 | This application is responsible for installing Flatpaks on first boot after a user finishes installation. 4 | It is intended as a replacement for custom zenity dialogs. 5 | 6 | ## Project goals 7 | 8 | * Config file driven via JSON/YAML 9 | * Support for arbitrary pre and post-install commands 10 | * Configuration driven screens 11 | * Screen independent state management with ability to set defaults 12 | * Extensible with drop-in Python classes / plugins to extend functionality 13 | 14 | ## Core features 15 | 16 | These are goals for each feature of the first time installer: 17 | 18 | ### Title Screen 19 | 20 | The Title screen will be comprised of three primary elements. An image/icon, a header/primary text, and a paragraph description text. 21 | 22 | ``` 23 | 24 | ICON 25 | TITLE TEXT 26 | 27 | this is a description 28 | to accompany the title 29 | screen. 30 | ``` 31 | 32 | ### Packages screen 33 | 34 | Display several groups of packages to install, allow for expansion of each group to individually select discrete packages or toggle the entire group on/off. 35 | 36 | eg: 37 | ``` 38 | Core [/] > 39 | Gaming [/] > 40 | Office [/] > 41 | ``` 42 | 43 | Expanding Core would reveal 44 | 45 | ``` 46 | Core [/] v 47 | firefox [x] 48 | calculator [x] 49 | text editor [x] 50 | clocks [x] 51 | fonts [x] 52 | Gaming [/] > 53 | Office [/] > 54 | ``` 55 | 56 | The application then installs the Flatpaks. Plugins for other packages systems may/can be developed. 57 | 58 | ### Configuration 59 | 60 | ```yaml 61 | title: uBlue First Boot 62 | properties: 63 | mode: "run-on-change" 64 | path: "~/.config/yafti/last-run" 65 | actions: 66 | pre: 67 | - run: /full/path/to/bin --with --params 68 | - run: /another/command run 69 | - yafti.plugin.flatpak: 70 | install: org.gnome.Calculator 71 | post: 72 | - run: /run/these/commands --after --all --screens 73 | screens: 74 | first-screen: 75 | source: yafti.screen.title 76 | values: 77 | title: "That was pretty cool" 78 | icon: "/path/to/icon" 79 | description: | 80 | Time to play overwatch 81 | applications: 82 | source: yafti.screen.package 83 | values: 84 | title: Package Installation 85 | show_terminal: true 86 | package_manager: yafti.plugin.flatpak 87 | groups: 88 | Core: 89 | description: All the good stuff 90 | packages: 91 | - Calculator: org.gnome.Calculator 92 | - Firefox: org.mozilla.firefox 93 | Gaming: 94 | description: GAMES GAMES GAMES 95 | packages: 96 | - Steam: com.valvesoftware.Steam 97 | - Games: org.gnome.Games 98 | Office: 99 | description: All the work stuff 100 | packages: 101 | - LibreOffice: org.libreoffice.LibreOffice 102 | - Calendar: org.gnome.Calendar 103 | final-screen: 104 | source: yafti.screen.title 105 | values: 106 | title: "All done" 107 | icon: "/path/to/icon" 108 | description: | 109 | Thanks for installing, join the community, next steps 110 | ``` 111 | 112 | ## Development 113 | 114 | This project uses Poetry and Python 3.11. Make sure you have Python 3.11 and [Poetry installed](https://python-poetry.org/docs/). Checkout the repository and navigate to root project directory. 115 | 116 | ### Prerequisites 117 | 118 | If you're on a Ublue / immutable OS, you'll need to run these and the poetry install in a toolbox. 119 | 120 | ``` 121 | sudo dnf install python3-devel cairo-devel gobject-introspection-devel cairo-gobject-devel 122 | poetry install 123 | ``` 124 | 125 | ### Running 126 | 127 | ``` 128 | poetry run python -m yafti tests/example.yml 129 | ``` 130 | 131 | This will launch the Yafti window. 132 | 133 | #### Running from a Containerfile 134 | 135 | One of yafti's main use cases is to be used in Containerfiles to handle installation of Flatpaks on first boot. 136 | Add this to your Containerfile to add yafti to your image: 137 | 138 | RUN pip install --prefix=/usr yafti 139 | 140 | Additionally, you need a script to copy over the .desktop file to the user's home directory: 141 | - [Example firstboot script](https://github.com/ublue-os/bluefin/blob/main/etc/profile.d/bluefin-firstboot.sh) 142 | - [Example firstboot .desktop file](https://github.com/ublue-os/bluefin/blob/main/etc/skel.d/.config/autostart/bluefin-firstboot.desktop) 143 | 144 | Then add a file in `/etc/yafti.yml` with your customizations. Check the [example file](https://github.com/ublue-os/yafti/blob/main/tests/example.yml) for ideas. 145 | 146 | ### Testing 147 | 148 | This project uses pytest, black, isort, and ruff for testing and linting. 149 | 150 | ``` 151 | poetry run pytest --cov=yafti --cov-report=term-missing 152 | poetry run black yafti 153 | poetry run isort yafti 154 | poetry run ruff yafti 155 | ``` 156 | 157 | ## Contributing 158 | 159 | This project follows a fork and pull request syle of contribution. 160 | 161 | ### Creating a Fork 162 | 163 | Just head over to the GitHub page and [click the "Fork" button](https://help.github.com/articles/fork-a-repo). Once you've done that, you can use your favorite git client to clone your repo or just head straight to the command line: 164 | 165 | ```shell 166 | # Clone your fork to your local machine 167 | git clone git@github.com:USERNAME/FORKED-PROJECT.git 168 | ``` 169 | 170 | ### Keeping Your Fork Up to Date 171 | 172 | While this isn't an absolutely necessary step, if you plan on doing anything more than just a tiny quick fix, you'll want to make sure you keep your fork up to date by tracking the original "upstream" repo that you forked. You can do this by using [the Github UI](https://help.github.com/articles/syncing-a-fork) or locally by adding this repo as an upstream. 173 | 174 | ```shell 175 | # Add 'upstream' repo to list of remotes 176 | git remote add upstream https://github.com/ublue-os/yafti.git 177 | 178 | # Verify the new remote named 'upstream' 179 | git remote -v 180 | ``` 181 | 182 | Whenever you want to update your fork with the latest upstream changes, you'll need to first fetch the upstream repo's branches and latest commits to bring them into your repository: 183 | 184 | ```shell 185 | # Fetch from upstream remote 186 | git fetch upstream 187 | 188 | # View all branches, including those from upstream 189 | git branch -va 190 | ``` 191 | 192 | Now, checkout your own main branch and merge the upstream repo's main branch: 193 | 194 | ```shell 195 | # Checkout your main branch and merge upstream 196 | git checkout main 197 | git merge --ff-only upstream/main 198 | ``` 199 | 200 | If there are no unique commits on the local main branch, git will simply perform a fast-forward. However, if you have been making changes on main (in the vast majority of cases you probably shouldn't be - [see the next section](#doing-your-work), you may have to deal with conflicts. When doing so, be careful to respect the changes made upstream. 201 | 202 | Now, your local main branch is up-to-date with everything modified upstream. 203 | 204 | ### Doing Your Work 205 | 206 | #### Create a Branch 207 | 208 | Whenever you begin work on a new feature or bugfix, it's important that you create a new branch. Not only is it proper git workflow, but it also keeps your changes organized and separated from the main branch so that you can easily submit and manage multiple pull requests for every task you complete. 209 | 210 | To create a new branch and start working on it: 211 | 212 | ```shell 213 | # Checkout the main branch - you want your new branch to come from main 214 | git checkout main 215 | 216 | # Create a new branch named newfeature (give your branch its own simple informative name) 217 | git checkbout -b newfeature 218 | ``` 219 | 220 | Now, go to town hacking away and making whatever changes you want to. 221 | 222 | #### Commit Messages 223 | 224 | We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) and enforce them with a bot to keep the changelogs tidy: 225 | 226 | ``` 227 | chore: add Oyster build script 228 | docs: explain hat wobble 229 | feat: add beta sequence 230 | fix: remove broken confirmation message 231 | refactor: share logic between 4d3d3d3 and flarhgunnstow 232 | style: convert tabs to spaces 233 | test: ensure Tayne retains clothing 234 | ``` 235 | 236 | If you have multiple commits, when [submitting your chages](#submitting-a-pull-request), make sure to use a conventional commit style PR title as this project does squash merges and that will be used as your contribution. 237 | 238 | ### Submitting a Pull Request 239 | 240 | #### Cleaning Up Your Work 241 | 242 | Prior to submitting your pull request, you might want to do a few things to clean up your branch and make it as simple as possible for the original repo's maintainer to test, accept, and merge your work. 243 | 244 | If any commits have been made to the upstream main branch, you should rebase your feature branch so that merging it will be a simple fast-forward that won't require any conflict resolution work. 245 | 246 | ```shell 247 | # Fetch upstream main and merge with your repo's main branch 248 | git fetch upstream 249 | git checkout main 250 | git merge upstream/main 251 | 252 | # If there were any new commits, rebase your feature branch 253 | git checkout newfeature 254 | git rebase main 255 | ``` 256 | 257 | #### Submitting 258 | 259 | Once you've committed and pushed all of your changes to GitHub, go to the page for your fork on GitHub, select your feature branch, and click the pull request button. If you need to make any adjustments to your pull request, just push the updates to GitHub. Your pull request will automatically track the changes on your feature branch and update. 260 | 261 | ### Accepting and Merging a Pull Request 262 | 263 | Take note that unlike the previous sections which were written from the perspective of someone that created a fork and generated a pull request, this section is written from the perspective of the original repository owner who is handling an incoming pull request. Thus, where the "forker" was referring to the original repository as `upstream`, we're now looking at it as the owner of that original repository and the standard `origin` remote. 264 | 265 | #### Checking Out and Testing Pull Requests 266 | 267 | There are multiple ways to [check out a pull request locally](https://help.github.com/articles/checking-out-pull-requests-locally). This way uses standard git operations to complete. Open up the `.git/config` file and add a new line under `[remote "origin"]`: 268 | 269 | ``` 270 | fetch = +refs/pull/*/head:refs/pull/origin/* 271 | ``` 272 | 273 | Now you can fetch and checkout any pull request so that you can test them: 274 | 275 | ```shell 276 | # Fetch all pull request branches 277 | git fetch origin 278 | 279 | # Checkout out a given pull request branch based on its number 280 | git checkout -b 9001 pull/origin/9001 281 | ``` 282 | 283 | Keep in mind that these branches will be read only and you won't be able to push any changes. 284 | 285 | #### Automatically Merging a Pull Request 286 | In cases where the merge would be a simple fast-forward, you can automatically do the merge by clicking the button on the pull request page on GitHub. 287 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.10.2](https://github.com/ublue-os/yafti/compare/v0.10.1...v0.10.2) (2025-04-18) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * remove GBulb, require PyGObject 3.50.0+ ([#328](https://github.com/ublue-os/yafti/issues/328)) ([650a8c2](https://github.com/ublue-os/yafti/commit/650a8c2ce50784d1416cf650af3820c7c6a3998e)) 9 | 10 | ## [0.10.1](https://github.com/ublue-os/yafti/compare/v0.10.0...v0.10.1) (2025-04-15) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * **deps:** update dependency gbulb to v0.6.6 ([#271](https://github.com/ublue-os/yafti/issues/271)) ([1da10fa](https://github.com/ublue-os/yafti/commit/1da10fa38e64c80f4308ad52f6813147c550c0c2)) 16 | * **deps:** update dependency gbulb to v0.6.6 ([#285](https://github.com/ublue-os/yafti/issues/285)) ([f3b6bf7](https://github.com/ublue-os/yafti/commit/f3b6bf7f473fe30b98a1782096d16c4f670f54d2)) 17 | * **deps:** update dependency gbulb to v0.6.6 ([#287](https://github.com/ublue-os/yafti/issues/287)) ([0e1025b](https://github.com/ublue-os/yafti/commit/0e1025b89ee89b3c5b90855eeaddd42e83b7753c)) 18 | * **deps:** update dependency gbulb to v0.6.6 ([#290](https://github.com/ublue-os/yafti/issues/290)) ([27b615d](https://github.com/ublue-os/yafti/commit/27b615df597a50b71d824b6e884e6c99b6ddce13)) 19 | * **deps:** update dependency pydantic to v2.10.4 ([#276](https://github.com/ublue-os/yafti/issues/276)) ([d62556a](https://github.com/ublue-os/yafti/commit/d62556abeefaff30c827ef4aee7a6e05e2f5ea67)) 20 | * **deps:** update dependency pygobject to v3.50.0 ([#277](https://github.com/ublue-os/yafti/issues/277)) ([7036505](https://github.com/ublue-os/yafti/commit/70365058f1d5ab9cec60ecf16e2061fc8adfbbb9)) 21 | * **deps:** update dependency pygobject to v3.50.0 ([#286](https://github.com/ublue-os/yafti/issues/286)) ([3a7ee30](https://github.com/ublue-os/yafti/commit/3a7ee30be4a2b50dba55c580549fb5cd7b24f0a5)) 22 | * **deps:** update dependency pygobject to v3.50.0 ([#288](https://github.com/ublue-os/yafti/issues/288)) ([52ad97d](https://github.com/ublue-os/yafti/commit/52ad97da9201f11eacccd42e69f58bf00003e7c2)) 23 | * **deps:** update dependency pygobject to v3.52.3 ([#292](https://github.com/ublue-os/yafti/issues/292)) ([139bd71](https://github.com/ublue-os/yafti/commit/139bd716140808ce85f8181edce5759936c6da24)) 24 | * **deps:** update dependency rich to v13.9.4 ([#272](https://github.com/ublue-os/yafti/issues/272)) ([47e43a2](https://github.com/ublue-os/yafti/commit/47e43a25ddbc7211ad97330df9125f087c9cfde6)) 25 | * **deps:** update dependency typer to v0.15.1 ([#278](https://github.com/ublue-os/yafti/issues/278)) ([fb5ff38](https://github.com/ublue-os/yafti/commit/fb5ff385a8acc01802d1a2dc3077e49cae44cd59)) 26 | 27 | ## [0.10.0](https://github.com/ublue-os/yafti/compare/v0.9.0...v0.10.0) (2024-10-19) 28 | 29 | 30 | ### Features 31 | 32 | * Split up CI tests ([#263](https://github.com/ublue-os/yafti/issues/263)) ([e949d3a](https://github.com/ublue-os/yafti/commit/e949d3a50cf16f5331acf6cc4d2d8a38e910ac58)) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * [[#149](https://github.com/ublue-os/yafti/issues/149)] Remove quit option ([#262](https://github.com/ublue-os/yafti/issues/262)) ([1fcd8c4](https://github.com/ublue-os/yafti/commit/1fcd8c4d95011029417621d31074d05e9e256ed7)) 38 | * allow any pydantic version from 2.8.2 to 3.0.0 (exclusive) for F41 compat ([#266](https://github.com/ublue-os/yafti/issues/266)) ([f978a48](https://github.com/ublue-os/yafti/commit/f978a4803a53ebda622e9b64942b19280e425624)) 39 | * RPM spec file misses `libadwaita` as an dependency ([#265](https://github.com/ublue-os/yafti/issues/265)) ([6f8de5c](https://github.com/ublue-os/yafti/commit/6f8de5c5a35641d620ff83907580a284bc5af3b3)) 40 | 41 | ## [0.9.0](https://github.com/ublue-os/yafti/compare/v0.8.0...v0.9.0) (2024-08-05) 42 | 43 | 44 | ### Features 45 | 46 | * add PKGBUILD for Arch packaging ([#163](https://github.com/ublue-os/yafti/issues/163)) ([a827362](https://github.com/ublue-os/yafti/commit/a8273626e84e707e400212ca3d1d412cb85df019)) 47 | * update pydantic to v2 ([#260](https://github.com/ublue-os/yafti/issues/260)) ([8da8264](https://github.com/ublue-os/yafti/commit/8da82645e028ef80651325ccc4223ebb2adbc4d9)) 48 | * **window:** Move "Next" button to top right ([#156](https://github.com/ublue-os/yafti/issues/156)) ([4abb8d4](https://github.com/ublue-os/yafti/commit/4abb8d42cde5cdc6b1eb13790cdd17a1772662ef)) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * Correct a spacing issue between preferred and package in text ([#183](https://github.com/ublue-os/yafti/issues/183)) ([57bcee5](https://github.com/ublue-os/yafti/commit/57bcee571a06c1bcaef3da5f8836a4f3e106c23c)) 54 | * Revert pydantic upgrade until required fixes can be made ([#184](https://github.com/ublue-os/yafti/issues/184)) ([1ff59d3](https://github.com/ublue-os/yafti/commit/1ff59d36ff81b2e71f774b404f66df4b256b1194)) 55 | 56 | ## [0.8.0](https://github.com/ublue-os/yafti/compare/v0.7.1...v0.8.0) (2023-08-07) 57 | 58 | 59 | ### Features 60 | 61 | * add save_state config property for changing when last-save state is recorded ([#145](https://github.com/ublue-os/yafti/issues/145)) ([0bc73af](https://github.com/ublue-os/yafti/commit/0bc73afe72d5b0a42a0e872efd07e1dbe2d6fb97)) 62 | * support for packages key for packages screen ([#148](https://github.com/ublue-os/yafti/issues/148)) ([381f73e](https://github.com/ublue-os/yafti/commit/381f73edbfff46a47cdf864593a2e762443738da)) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * pass state explicitly instead of overwriting id builtin ([#147](https://github.com/ublue-os/yafti/issues/147)) ([9fd3d79](https://github.com/ublue-os/yafti/commit/9fd3d7995d72b0364f4edfedfb147b764a40ac17)) 68 | 69 | ## [0.7.1](https://github.com/ublue-os/yafti/compare/v0.7.0...v0.7.1) (2023-08-07) 70 | 71 | 72 | ### Bug Fixes 73 | 74 | * support for multiple package screens in one config ([#139](https://github.com/ublue-os/yafti/issues/139)) ([c9db948](https://github.com/ublue-os/yafti/commit/c9db948fb84838676cc304023be43c97a215d6ba)) 75 | 76 | ## [0.7.0](https://github.com/ublue-os/yafti/compare/v0.6.2...v0.7.0) (2023-07-05) 77 | 78 | 79 | ### Features 80 | 81 | * Allow run plugin to be used as a package manager ([#128](https://github.com/ublue-os/yafti/issues/128)) ([7912e09](https://github.com/ublue-os/yafti/commit/7912e0984b6431f25482e681604944f2dc603215)) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * add application title ([#113](https://github.com/ublue-os/yafti/issues/113)) ([7f6b475](https://github.com/ublue-os/yafti/commit/7f6b4757632b4885eb882156c8e1f8e79711b070)) 87 | 88 | 89 | ### Reverts 90 | 91 | * "fix: add application title" ([#116](https://github.com/ublue-os/yafti/issues/116)) ([fb3c8ee](https://github.com/ublue-os/yafti/commit/fb3c8eee34548428efec7bf4f25883d39e76e23d)) 92 | 93 | ## [0.6.2](https://github.com/ublue-os/yafti/compare/v0.6.1...v0.6.2) (2023-05-30) 94 | 95 | 96 | ### Bug Fixes 97 | 98 | * move user data to sub-folder and ensure folder existence ([#102](https://github.com/ublue-os/yafti/issues/102)) ([7fce7fb](https://github.com/ublue-os/yafti/commit/7fce7fb7e1f1f4e82d359c4653494fcd0c4b593a)) 99 | 100 | ## [0.6.1](https://github.com/ublue-os/yafti/compare/v0.6.0...v0.6.1) (2023-04-17) 101 | 102 | 103 | ### Bug Fixes 104 | 105 | * multiple links in "final-screen" values results in error ([#84](https://github.com/ublue-os/yafti/issues/84)) ([457ffe8](https://github.com/ublue-os/yafti/commit/457ffe83865079c61101d0a2b705cd1485a7340b)) 106 | * yafti.screen.package title doesn't show up ([#85](https://github.com/ublue-os/yafti/issues/85)) ([8641b56](https://github.com/ublue-os/yafti/commit/8641b5614d89e28b912ae5a6fe0ba646f22c70ee)) 107 | 108 | ## [0.6.0](https://github.com/ublue-os/yafti/compare/v0.5.0...v0.6.0) (2023-04-12) 109 | 110 | 111 | ### Features 112 | 113 | * enable both user and system flatpak installs ([#82](https://github.com/ublue-os/yafti/issues/82)) ([8413bee](https://github.com/ublue-os/yafti/commit/8413beeadb5604407fa8a31e643c92fed29fbd7e)) 114 | * show a bouncing progress bar during package installation ([#74](https://github.com/ublue-os/yafti/issues/74)) ([e1fdd65](https://github.com/ublue-os/yafti/commit/e1fdd65d6cec405f1926524ed1e54c7319a8a147)) 115 | 116 | ## [0.5.0](https://github.com/ublue-os/yafti/compare/v0.4.1...v0.5.0) (2023-03-27) 117 | 118 | 119 | ### Features 120 | 121 | * extend title screen to include additional actions ([#66](https://github.com/ublue-os/yafti/issues/66)) ([a2fa984](https://github.com/ublue-os/yafti/commit/a2fa9848258b91c3f833a11751af1bf4c1a5bae2)) 122 | 123 | ## [0.4.1](https://github.com/ublue-os/yafti/compare/v0.4.0...v0.4.1) (2023-03-24) 124 | 125 | 126 | ### Bug Fixes 127 | 128 | * make sure bin entrypoint matches -m entrypoint ([#64](https://github.com/ublue-os/yafti/issues/64)) ([b2423b8](https://github.com/ublue-os/yafti/commit/b2423b81355496dde8b930ddeaaeca5d2eaf0baa)) 129 | 130 | ## [0.4.0](https://github.com/ublue-os/yafti/compare/v0.3.1...v0.4.0) (2023-03-22) 131 | 132 | 133 | ### Features 134 | 135 | * add logging mechanics & actual CLI ([#59](https://github.com/ublue-os/yafti/issues/59)) ([9df69c6](https://github.com/ublue-os/yafti/commit/9df69c6225f6d6b285182cdd5a65eb959fac8604)) 136 | 137 | 138 | ### Documentation 139 | 140 | * explain how to use yafti in a containerfile ([#58](https://github.com/ublue-os/yafti/issues/58)) ([8be1d27](https://github.com/ublue-os/yafti/commit/8be1d27965e1f5f6bb84a95f310f51fa83881cae)) 141 | * fix RUN command ([#61](https://github.com/ublue-os/yafti/issues/61)) ([8ed4232](https://github.com/ublue-os/yafti/commit/8ed4232dfc6ddd6759dd149bb17739fa3d196f19)) 142 | 143 | ## [0.3.1](https://github.com/ublue-os/yafti/compare/v0.3.0...v0.3.1) (2023-03-20) 144 | 145 | 146 | ### Bug Fixes 147 | 148 | * quit app when close requested ([#55](https://github.com/ublue-os/yafti/issues/55)) ([924a8aa](https://github.com/ublue-os/yafti/commit/924a8aaf1332e9dfe91157f352a35b55448d860f)) 149 | 150 | ## [0.3.0](https://github.com/ublue-os/yafti/compare/v0.2.8...v0.3.0) (2023-03-20) 151 | 152 | 153 | ### Features 154 | 155 | * add "first run" protections and configuration ([#46](https://github.com/ublue-os/yafti/issues/46)) ([290b06e](https://github.com/ublue-os/yafti/commit/290b06ee836421673410a6313234c7d2d45e15c1)) 156 | * add consent screen and screen conditions ([#47](https://github.com/ublue-os/yafti/issues/47)) ([4ff07c4](https://github.com/ublue-os/yafti/commit/4ff07c484f8f451978870c54a2db224dae8eb0f9)) 157 | * customize which groups are enabled by default ([#53](https://github.com/ublue-os/yafti/issues/53)) ([dfa363a](https://github.com/ublue-os/yafti/commit/dfa363a8a72f7b50ceb20870a7dc6890723bd3e2)) 158 | 159 | 160 | ### Bug Fixes 161 | 162 | * **deps:** lock pydantic dep to match fedora 38 ([#38](https://github.com/ublue-os/yafti/issues/38)) ([7dc7fac](https://github.com/ublue-os/yafti/commit/7dc7fac7a32d9edc54148e69abf02c35f569302e)) 163 | * **packaging:** use pyproject-rpm-macros ([#40](https://github.com/ublue-os/yafti/issues/40)) ([3885b89](https://github.com/ublue-os/yafti/commit/3885b892456036bca93a1cfb629754a4e772cf35)) 164 | 165 | ## [0.2.8](https://github.com/ublue-os/yafti/compare/v0.2.7...v0.2.8) (2023-03-15) 166 | 167 | 168 | ### Bug Fixes 169 | 170 | * **release:** restore upload-assets action ([#36](https://github.com/ublue-os/yafti/issues/36)) ([d021ae3](https://github.com/ublue-os/yafti/commit/d021ae339346980251c3f1f0f19fdde9c070e877)) 171 | 172 | ## [0.2.7](https://github.com/ublue-os/yafti/compare/v0.2.6...v0.2.7) (2023-03-15) 173 | 174 | 175 | ### Bug Fixes 176 | 177 | * **release:** rpm version needs to be dynamic ([#34](https://github.com/ublue-os/yafti/issues/34)) ([c3c06d4](https://github.com/ublue-os/yafti/commit/c3c06d4210472d441a77ffd3718217d370fb5be9)) 178 | 179 | ## [0.2.6](https://github.com/ublue-os/yafti/compare/v0.2.5...v0.2.6) (2023-03-15) 180 | 181 | 182 | ### Bug Fixes 183 | 184 | * **release:** replace release upload action ([#32](https://github.com/ublue-os/yafti/issues/32)) ([e5ae2d2](https://github.com/ublue-os/yafti/commit/e5ae2d2132982d4185960d33331ab08bf769f1c2)) 185 | 186 | ## [0.2.5](https://github.com/ublue-os/yafti/compare/v0.2.4...v0.2.5) (2023-03-15) 187 | 188 | 189 | ### Bug Fixes 190 | 191 | * **release:** adjust asset path to fit current model ([#30](https://github.com/ublue-os/yafti/issues/30)) ([dcf364b](https://github.com/ublue-os/yafti/commit/dcf364b7ad3300238c51790ca857587e88138d75)) 192 | 193 | ## [0.2.4](https://github.com/ublue-os/yafti/compare/v0.2.3...v0.2.4) (2023-03-15) 194 | 195 | 196 | ### Bug Fixes 197 | 198 | * **release:** supply file paths for artifacts ([#28](https://github.com/ublue-os/yafti/issues/28)) ([7b1e9ba](https://github.com/ublue-os/yafti/commit/7b1e9bac84acc35a5a05562cfc4b959cc2a4a695)) 199 | 200 | ## [0.2.3](https://github.com/ublue-os/yafti/compare/v0.2.2...v0.2.3) (2023-03-15) 201 | 202 | 203 | ### Bug Fixes 204 | 205 | * **release:** pass in tag reference on build ([#26](https://github.com/ublue-os/yafti/issues/26)) ([88d8850](https://github.com/ublue-os/yafti/commit/88d885062edb4c1f194869f9fcd77563276c11a9)) 206 | 207 | ## [0.2.2](https://github.com/ublue-os/yafti/compare/v0.2.1...v0.2.2) (2023-03-15) 208 | 209 | 210 | ### Bug Fixes 211 | 212 | * **release:** consolidate release process ([#24](https://github.com/ublue-os/yafti/issues/24)) ([6450bce](https://github.com/ublue-os/yafti/commit/6450bceb5669f73c6105297932b70a4699a8c58c)) 213 | 214 | ## [0.2.1](https://github.com/ublue-os/yafti/compare/v0.2.0...v0.2.1) (2023-03-15) 215 | 216 | 217 | ### Bug Fixes 218 | 219 | * **ci:** typo on workflow trigger ([#22](https://github.com/ublue-os/yafti/issues/22)) ([9e9c6a8](https://github.com/ublue-os/yafti/commit/9e9c6a833cf0834af43dd8d64138c8cec8706386)) 220 | 221 | ## [0.2.0](https://github.com/ublue-os/yafti/compare/v0.1.0...v0.2.0) (2023-03-15) 222 | 223 | 224 | ### Features 225 | 226 | * add RPM spec ([#12](https://github.com/ublue-os/yafti/issues/12)) ([3047cb5](https://github.com/ublue-os/yafti/commit/3047cb5ce6484a5df7348117951e33ab26661224)) 227 | 228 | ## 0.1.0 (2023-03-13) 229 | 230 | 231 | ### Features 232 | 233 | * add console screen component ([34f25fa](https://github.com/ublue-os/yafti/commit/34f25fae0c2f7534299043d25a6edf73f0582013)) 234 | * allow for package installation ([64e49a9](https://github.com/ublue-os/yafti/commit/64e49a9f424a9a4b8cc8ed0e395c19e008b15441)) 235 | * development and contrib documentation ([322ca9f](https://github.com/ublue-os/yafti/commit/322ca9f76e72ced437672d9648ed0d5da134774a)) 236 | * expland YaftiScreen definition ([1080107](https://github.com/ublue-os/yafti/commit/10801071c925cb2719ea8c5826ab62e6e16c7c7f)) 237 | * implement async support and some async methods ([eeb55ff](https://github.com/ublue-os/yafti/commit/eeb55ff97ae7696f43f1e8ae2be331c3ba604717)) 238 | * initial commit ([d85ab8a](https://github.com/ublue-os/yafti/commit/d85ab8af779649a0e0d95e53591f56c8b6e02a99)) 239 | * proper shutdown / closure ([7514d3a](https://github.com/ublue-os/yafti/commit/7514d3aeb2d92447470d156b02a4b93d4542b087)) 240 | * signals/observer and global button state ([3c45bb3](https://github.com/ublue-os/yafti/commit/3c45bb3897fd11a3bc95626a1272353e615f5028)) 241 | * start of unit tests ([135c934](https://github.com/ublue-os/yafti/commit/135c93449aec24a2e6d4b6db7be1424931570788)) 242 | * track screen state for packages ([2204e1d](https://github.com/ublue-os/yafti/commit/2204e1d4b23f8da5ade290e67451dca4d9afffa7)) 243 | 244 | 245 | ### Bug Fixes 246 | 247 | * add ruff for enhanced linting ([75daf97](https://github.com/ublue-os/yafti/commit/75daf970e9a5f79662e7963b74e0659223ca01c6)) 248 | * flatpack exec/install/remove ([2cefa20](https://github.com/ublue-os/yafti/commit/2cefa207ff9e177cc09a5f5ad806219e357fbc96)) 249 | 250 | 251 | ### Documentation 252 | 253 | * copyediting and formatting on the readme ([#6](https://github.com/ublue-os/yafti/issues/6)) ([d973098](https://github.com/ublue-os/yafti/commit/d9730989433446e78ebbb7d34817bf10deb5787a)) 254 | * improve developer instructions ([88a3a5e](https://github.com/ublue-os/yafti/commit/88a3a5ea0d9a0d3a41d802cd996eac4085cc3433)) 255 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.7.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 11 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 12 | ] 13 | 14 | [[package]] 15 | name = "black" 16 | version = "24.10.0" 17 | description = "The uncompromising code formatter." 18 | optional = false 19 | python-versions = ">=3.9" 20 | files = [ 21 | {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, 22 | {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, 23 | {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, 24 | {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, 25 | {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, 26 | {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, 27 | {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, 28 | {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, 29 | {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, 30 | {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, 31 | {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, 32 | {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, 33 | {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, 34 | {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, 35 | {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, 36 | {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, 37 | {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, 38 | {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, 39 | {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, 40 | {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, 41 | {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, 42 | {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, 43 | ] 44 | 45 | [package.dependencies] 46 | click = ">=8.0.0" 47 | mypy-extensions = ">=0.4.3" 48 | packaging = ">=22.0" 49 | pathspec = ">=0.9.0" 50 | platformdirs = ">=2" 51 | 52 | [package.extras] 53 | colorama = ["colorama (>=0.4.3)"] 54 | d = ["aiohttp (>=3.10)"] 55 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 56 | uvloop = ["uvloop (>=0.15.2)"] 57 | 58 | [[package]] 59 | name = "click" 60 | version = "8.1.7" 61 | description = "Composable command line interface toolkit" 62 | optional = false 63 | python-versions = ">=3.7" 64 | files = [ 65 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 66 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 67 | ] 68 | 69 | [package.dependencies] 70 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 71 | 72 | [[package]] 73 | name = "colorama" 74 | version = "0.4.6" 75 | description = "Cross-platform colored terminal text." 76 | optional = false 77 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 78 | files = [ 79 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 80 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 81 | ] 82 | 83 | [[package]] 84 | name = "coverage" 85 | version = "7.6.10" 86 | description = "Code coverage measurement for Python" 87 | optional = false 88 | python-versions = ">=3.9" 89 | files = [ 90 | {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, 91 | {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, 92 | {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, 93 | {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, 94 | {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, 95 | {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, 96 | {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, 97 | {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, 98 | {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, 99 | {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, 100 | {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, 101 | {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, 102 | {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, 103 | {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, 104 | {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, 105 | {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, 106 | {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, 107 | {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, 108 | {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, 109 | {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, 110 | {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, 111 | {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, 112 | {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, 113 | {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, 114 | {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, 115 | {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, 116 | {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, 117 | {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, 118 | {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, 119 | {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, 120 | {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, 121 | {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, 122 | {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, 123 | {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, 124 | {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, 125 | {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, 126 | {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, 127 | {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, 128 | {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, 129 | {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, 130 | {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, 131 | {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, 132 | {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, 133 | {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, 134 | {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, 135 | {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, 136 | {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, 137 | {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, 138 | {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, 139 | {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, 140 | {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, 141 | {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, 142 | {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, 143 | {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, 144 | {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, 145 | {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, 146 | {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, 147 | {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, 148 | {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, 149 | {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, 150 | {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, 151 | {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, 152 | ] 153 | 154 | [package.extras] 155 | toml = ["tomli"] 156 | 157 | [[package]] 158 | name = "gbulb" 159 | version = "0.6.5" 160 | description = "GLib event loop for Python asyncio" 161 | optional = false 162 | python-versions = ">=3.8" 163 | files = [ 164 | {file = "gbulb-0.6.5-py3-none-any.whl", hash = "sha256:5d029fceeffa3694b058002a265bd50e55e52f1fb26f0d4336dff32b33a4ae2b"}, 165 | {file = "gbulb-0.6.5.tar.gz", hash = "sha256:1571634f34b7a98f04adfb3072f97a8bef4bdf62a36a3816b94b402569b12f9b"}, 166 | ] 167 | 168 | [package.dependencies] 169 | pygobject = ">=3.14.0" 170 | 171 | [package.extras] 172 | dev = ["coverage[toml] (==7.5.0)", "pre-commit (==3.5.0)", "pre-commit (==3.7.0)", "pytest (==8.1.1)", "tox (==4.15.0)"] 173 | 174 | [[package]] 175 | name = "iniconfig" 176 | version = "2.0.0" 177 | description = "brain-dead simple config-ini parsing" 178 | optional = false 179 | python-versions = ">=3.7" 180 | files = [ 181 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 182 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 183 | ] 184 | 185 | [[package]] 186 | name = "isort" 187 | version = "5.13.2" 188 | description = "A Python utility / library to sort Python imports." 189 | optional = false 190 | python-versions = ">=3.8.0" 191 | files = [ 192 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 193 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 194 | ] 195 | 196 | [package.extras] 197 | colors = ["colorama (>=0.4.6)"] 198 | 199 | [[package]] 200 | name = "markdown-it-py" 201 | version = "3.0.0" 202 | description = "Python port of markdown-it. Markdown parsing, done right!" 203 | optional = false 204 | python-versions = ">=3.8" 205 | files = [ 206 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 207 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 208 | ] 209 | 210 | [package.dependencies] 211 | mdurl = ">=0.1,<1.0" 212 | 213 | [package.extras] 214 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 215 | code-style = ["pre-commit (>=3.0,<4.0)"] 216 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 217 | linkify = ["linkify-it-py (>=1,<3)"] 218 | plugins = ["mdit-py-plugins"] 219 | profiling = ["gprof2dot"] 220 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 221 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 222 | 223 | [[package]] 224 | name = "mdurl" 225 | version = "0.1.2" 226 | description = "Markdown URL utilities" 227 | optional = false 228 | python-versions = ">=3.7" 229 | files = [ 230 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 231 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 232 | ] 233 | 234 | [[package]] 235 | name = "mypy-extensions" 236 | version = "1.0.0" 237 | description = "Type system extensions for programs checked with the mypy type checker." 238 | optional = false 239 | python-versions = ">=3.5" 240 | files = [ 241 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 242 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 243 | ] 244 | 245 | [[package]] 246 | name = "packaging" 247 | version = "24.1" 248 | description = "Core utilities for Python packages" 249 | optional = false 250 | python-versions = ">=3.8" 251 | files = [ 252 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 253 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 254 | ] 255 | 256 | [[package]] 257 | name = "pathspec" 258 | version = "0.12.1" 259 | description = "Utility library for gitignore style pattern matching of file paths." 260 | optional = false 261 | python-versions = ">=3.8" 262 | files = [ 263 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 264 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 265 | ] 266 | 267 | [[package]] 268 | name = "platformdirs" 269 | version = "4.3.6" 270 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 271 | optional = false 272 | python-versions = ">=3.8" 273 | files = [ 274 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 275 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 276 | ] 277 | 278 | [package.extras] 279 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 280 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 281 | type = ["mypy (>=1.11.2)"] 282 | 283 | [[package]] 284 | name = "pluggy" 285 | version = "1.5.0" 286 | description = "plugin and hook calling mechanisms for python" 287 | optional = false 288 | python-versions = ">=3.8" 289 | files = [ 290 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 291 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 292 | ] 293 | 294 | [package.extras] 295 | dev = ["pre-commit", "tox"] 296 | testing = ["pytest", "pytest-benchmark"] 297 | 298 | [[package]] 299 | name = "pycairo" 300 | version = "1.27.0" 301 | description = "Python interface for cairo" 302 | optional = false 303 | python-versions = ">=3.9" 304 | files = [ 305 | {file = "pycairo-1.27.0-cp310-cp310-win32.whl", hash = "sha256:e20f431244634cf244ab6b4c3a2e540e65746eed1324573cf291981c3e65fc05"}, 306 | {file = "pycairo-1.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:03bf570e3919901572987bc69237b648fe0de242439980be3e606b396e3318c9"}, 307 | {file = "pycairo-1.27.0-cp311-cp311-win32.whl", hash = "sha256:9a9b79f92a434dae65c34c830bb9abdbd92654195e73d52663cbe45af1ad14b2"}, 308 | {file = "pycairo-1.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:d40a6d80b15dacb3672dc454df4bc4ab3988c6b3f36353b24a255dc59a1c8aea"}, 309 | {file = "pycairo-1.27.0-cp312-cp312-win32.whl", hash = "sha256:e2239b9bb6c05edae5f3be97128e85147a155465e644f4d98ea0ceac7afc04ee"}, 310 | {file = "pycairo-1.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:27cb4d3a80e3b9990af552818515a8e466e0317063a6e61585533f1a86f1b7d5"}, 311 | {file = "pycairo-1.27.0-cp313-cp313-win32.whl", hash = "sha256:01505c138a313df2469f812405963532fc2511fb9bca9bdc8e0ab94c55d1ced8"}, 312 | {file = "pycairo-1.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:b0349d744c068b6644ae23da6ada111c8a8a7e323b56cbce3707cba5bdb474cc"}, 313 | {file = "pycairo-1.27.0-cp39-cp39-win32.whl", hash = "sha256:f9ca8430751f1fdcd3f072377560c9e15608b9a42d61375469db853566993c9b"}, 314 | {file = "pycairo-1.27.0-cp39-cp39-win_amd64.whl", hash = "sha256:1b1321652a6e27c4de3069709b1cae22aed2707fd8c5e889c04a95669228af2a"}, 315 | {file = "pycairo-1.27.0.tar.gz", hash = "sha256:5cb21e7a00a2afcafea7f14390235be33497a2cce53a98a19389492a60628430"}, 316 | ] 317 | 318 | [[package]] 319 | name = "pydantic" 320 | version = "2.10.4" 321 | description = "Data validation using Python type hints" 322 | optional = false 323 | python-versions = ">=3.8" 324 | files = [ 325 | {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, 326 | {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, 327 | ] 328 | 329 | [package.dependencies] 330 | annotated-types = ">=0.6.0" 331 | pydantic-core = "2.27.2" 332 | typing-extensions = ">=4.12.2" 333 | 334 | [package.extras] 335 | email = ["email-validator (>=2.0.0)"] 336 | timezone = ["tzdata"] 337 | 338 | [[package]] 339 | name = "pydantic-core" 340 | version = "2.27.2" 341 | description = "Core functionality for Pydantic validation and serialization" 342 | optional = false 343 | python-versions = ">=3.8" 344 | files = [ 345 | {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, 346 | {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, 347 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, 348 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, 349 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, 350 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, 351 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, 352 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, 353 | {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, 354 | {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, 355 | {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, 356 | {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, 357 | {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, 358 | {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, 359 | {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, 360 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, 361 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, 362 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, 363 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, 364 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, 365 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, 366 | {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, 367 | {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, 368 | {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, 369 | {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, 370 | {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, 371 | {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, 372 | {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, 373 | {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, 374 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, 375 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, 376 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, 377 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, 378 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, 379 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, 380 | {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, 381 | {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, 382 | {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, 383 | {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, 384 | {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, 385 | {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, 386 | {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, 387 | {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, 388 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, 389 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, 390 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, 391 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, 392 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, 393 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, 394 | {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, 395 | {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, 396 | {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, 397 | {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, 398 | {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, 399 | {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, 400 | {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, 401 | {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, 402 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, 403 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, 404 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, 405 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, 406 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, 407 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, 408 | {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, 409 | {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, 410 | {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, 411 | {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, 412 | {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, 413 | {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, 414 | {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, 415 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, 416 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, 417 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, 418 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, 419 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, 420 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, 421 | {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, 422 | {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, 423 | {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, 424 | {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, 425 | {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, 426 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, 427 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, 428 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, 429 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, 430 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, 431 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, 432 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, 433 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, 434 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, 435 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, 436 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, 437 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, 438 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, 439 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, 440 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, 441 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, 442 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, 443 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, 444 | {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, 445 | ] 446 | 447 | [package.dependencies] 448 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 449 | 450 | [[package]] 451 | name = "pygments" 452 | version = "2.18.0" 453 | description = "Pygments is a syntax highlighting package written in Python." 454 | optional = false 455 | python-versions = ">=3.8" 456 | files = [ 457 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 458 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 459 | ] 460 | 461 | [package.extras] 462 | windows-terminal = ["colorama (>=0.4.6)"] 463 | 464 | [[package]] 465 | name = "pygobject" 466 | version = "3.52.3" 467 | description = "Python bindings for GObject Introspection" 468 | optional = false 469 | python-versions = "<4.0,>=3.9" 470 | files = [ 471 | {file = "pygobject-3.52.3.tar.gz", hash = "sha256:00e427d291e957462a8fad659a9f9c8be776ff82a8b76bdf402f1eaeec086d82"}, 472 | ] 473 | 474 | [package.dependencies] 475 | pycairo = ">=1.16" 476 | 477 | [[package]] 478 | name = "pytest" 479 | version = "8.3.4" 480 | description = "pytest: simple powerful testing with Python" 481 | optional = false 482 | python-versions = ">=3.8" 483 | files = [ 484 | {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, 485 | {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, 486 | ] 487 | 488 | [package.dependencies] 489 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 490 | iniconfig = "*" 491 | packaging = "*" 492 | pluggy = ">=1.5,<2" 493 | 494 | [package.extras] 495 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 496 | 497 | [[package]] 498 | name = "pytest-asyncio" 499 | version = "0.25.2" 500 | description = "Pytest support for asyncio" 501 | optional = false 502 | python-versions = ">=3.9" 503 | files = [ 504 | {file = "pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075"}, 505 | {file = "pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"}, 506 | ] 507 | 508 | [package.dependencies] 509 | pytest = ">=8.2,<9" 510 | 511 | [package.extras] 512 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] 513 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] 514 | 515 | [[package]] 516 | name = "pytest-cov" 517 | version = "6.0.0" 518 | description = "Pytest plugin for measuring coverage." 519 | optional = false 520 | python-versions = ">=3.9" 521 | files = [ 522 | {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, 523 | {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, 524 | ] 525 | 526 | [package.dependencies] 527 | coverage = {version = ">=7.5", extras = ["toml"]} 528 | pytest = ">=4.6" 529 | 530 | [package.extras] 531 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 532 | 533 | [[package]] 534 | name = "pyyaml" 535 | version = "6.0.2" 536 | description = "YAML parser and emitter for Python" 537 | optional = false 538 | python-versions = ">=3.8" 539 | files = [ 540 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 541 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 542 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 543 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 544 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 545 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 546 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 547 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 548 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 549 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 550 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 551 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 552 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 553 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 554 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 555 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 556 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 557 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 558 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 559 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 560 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 561 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 562 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 563 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 564 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 565 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 566 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 567 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 568 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 569 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 570 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 571 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 572 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 573 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 574 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 575 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 576 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 577 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 578 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 579 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 580 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 581 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 582 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 583 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 584 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 585 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 586 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 587 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 588 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 589 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 590 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 591 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 592 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 593 | ] 594 | 595 | [[package]] 596 | name = "rich" 597 | version = "13.9.4" 598 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 599 | optional = false 600 | python-versions = ">=3.8.0" 601 | files = [ 602 | {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, 603 | {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, 604 | ] 605 | 606 | [package.dependencies] 607 | markdown-it-py = ">=2.2.0" 608 | pygments = ">=2.13.0,<3.0.0" 609 | 610 | [package.extras] 611 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 612 | 613 | [[package]] 614 | name = "ruff" 615 | version = "0.3.4" 616 | description = "An extremely fast Python linter and code formatter, written in Rust." 617 | optional = false 618 | python-versions = ">=3.7" 619 | files = [ 620 | {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"}, 621 | {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"}, 622 | {file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"}, 623 | {file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"}, 624 | {file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"}, 625 | {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"}, 626 | {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"}, 627 | {file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"}, 628 | {file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"}, 629 | {file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"}, 630 | {file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"}, 631 | {file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"}, 632 | {file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"}, 633 | {file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"}, 634 | {file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"}, 635 | {file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"}, 636 | {file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"}, 637 | ] 638 | 639 | [[package]] 640 | name = "shellingham" 641 | version = "1.5.4" 642 | description = "Tool to Detect Surrounding Shell" 643 | optional = false 644 | python-versions = ">=3.7" 645 | files = [ 646 | {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, 647 | {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, 648 | ] 649 | 650 | [[package]] 651 | name = "typer" 652 | version = "0.15.1" 653 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 654 | optional = false 655 | python-versions = ">=3.7" 656 | files = [ 657 | {file = "typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847"}, 658 | {file = "typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a"}, 659 | ] 660 | 661 | [package.dependencies] 662 | click = ">=8.0.0" 663 | rich = ">=10.11.0" 664 | shellingham = ">=1.3.0" 665 | typing-extensions = ">=3.7.4.3" 666 | 667 | [[package]] 668 | name = "typing-extensions" 669 | version = "4.12.2" 670 | description = "Backported and Experimental Type Hints for Python 3.8+" 671 | optional = false 672 | python-versions = ">=3.8" 673 | files = [ 674 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 675 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 676 | ] 677 | 678 | [metadata] 679 | lock-version = "2.0" 680 | python-versions = "^3.11" 681 | content-hash = "af1f237fe4bee75163bae8a49e75ebc765f62e999386ff4092503adcffec10c6" 682 | --------------------------------------------------------------------------------