Compare commits
3 commits
9c4441ec63
...
f6a8e260d5
Author | SHA1 | Date | |
---|---|---|---|
f6a8e260d5 | |||
3eb045cd4a | |||
a5c99c2d06 |
19 changed files with 422 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -162,3 +162,4 @@ cython_debug/
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
**/test.env
|
||||||
|
|
35
pyproject.toml
Normal file
35
pyproject.toml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
[project]
|
||||||
|
name = "Booker"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"pydantic>=2.8.2,<3.0.0",
|
||||||
|
"pydantic-settings>=2.4.0,<3.0.0",
|
||||||
|
"pyyaml>=6.0,<7.0",
|
||||||
|
"pendulum>=3.0.0,<4.0.0",
|
||||||
|
"requests>=2.32.3,<3.0.0",
|
||||||
|
"typer>=0.12.5,<0.13.0"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"black>=24.4.2,<25.0.0",
|
||||||
|
"isort>=5.13.2,<6.0.0",
|
||||||
|
"ruff>=0.5.4,<0.6.0",
|
||||||
|
"pytest>=8.3.1,<9.0.0",
|
||||||
|
"pytest-icdiff>=0.9,<1.0",
|
||||||
|
"pytest-sugar>=1.0.0,<2.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 88
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
pythonpath = [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
log_cli = 1
|
||||||
|
log_cli_level = "DEBUG"
|
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
0
src/losoup/__init__.py
Normal file
0
src/losoup/__init__.py
Normal file
0
src/losoup/cvs/__init__.py
Normal file
0
src/losoup/cvs/__init__.py
Normal file
41
src/losoup/cvs/github.py
Normal file
41
src/losoup/cvs/github.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from losoup.models import Asset, GithubSoftware
|
||||||
|
from losoup.cvs.github_settings import GithubSettings
|
||||||
|
|
||||||
|
|
||||||
|
class GithubConnector:
|
||||||
|
def __init__(self, settings: GithubSettings):
|
||||||
|
self.settings = settings
|
||||||
|
|
||||||
|
def get_release_assets_list(self, release_url: str) -> list[Asset]:
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"Authorization": f"token {self.settings.token}",
|
||||||
|
"X-Github-Api-Version": "2022-11-28",
|
||||||
|
}
|
||||||
|
response = requests.get(release_url, headers=headers)
|
||||||
|
assets = []
|
||||||
|
for asset in response.json().get("assets"):
|
||||||
|
assets.append(Asset(name=asset.get("name"), url=asset.get("url")))
|
||||||
|
return assets
|
||||||
|
|
||||||
|
def get_release_asset(self, software: GithubSoftware) -> Asset:
|
||||||
|
for asset in self.get_release_assets_list(software.release_url):
|
||||||
|
if asset.name == software.filename:
|
||||||
|
return asset
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
def download_release_asset(self, asset: Asset) -> bytes:
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/octet-stream",
|
||||||
|
"Authorization": f"token {self.settings.token}",
|
||||||
|
"X-Github-Api-Version": "2022-11-28",
|
||||||
|
}
|
||||||
|
response = requests.get(asset.url, headers=headers)
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
def download_software(self, software: GithubSoftware) -> bytes:
|
||||||
|
release_asset = self.get_release_asset(software)
|
||||||
|
return self.download_release_asset(release_asset)
|
8
src/losoup/cvs/github_settings.py
Normal file
8
src/losoup/cvs/github_settings.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class GithubSettings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(env_prefix="GITHUB_")
|
||||||
|
|
||||||
|
token: str
|
||||||
|
url: str = "https://api.github.com/repos"
|
0
src/losoup/cvs/gitlab.py
Normal file
0
src/losoup/cvs/gitlab.py
Normal file
0
src/losoup/file_operations/__init__.py
Normal file
0
src/losoup/file_operations/__init__.py
Normal file
24
src/losoup/file_operations/local_files.py
Normal file
24
src/losoup/file_operations/local_files.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from losoup.models import Software, Asset
|
||||||
|
|
||||||
|
|
||||||
|
class LocalFile:
|
||||||
|
def __init__(self, software: Software):
|
||||||
|
self.software = software
|
||||||
|
|
||||||
|
def get_files_in_folder(self) -> list[str]:
|
||||||
|
return [file.name for file in Path(self.software.folder).glob("*")]
|
||||||
|
|
||||||
|
def is_file_present(self):
|
||||||
|
files = self.get_files_in_folder()
|
||||||
|
return self.software.filename in files
|
||||||
|
|
||||||
|
def is_file_absent(self):
|
||||||
|
return not self.is_file_present()
|
||||||
|
|
||||||
|
def write_file(self, file: Asset) -> None:
|
||||||
|
self.software.absolute_path.write_bytes(file.content)
|
||||||
|
|
||||||
|
def delete_file(self):
|
||||||
|
self.software.absolute_path.unlink()
|
28
src/losoup/main.py
Normal file
28
src/losoup/main.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from losoup.file_operations import local_files
|
||||||
|
from losoup.models import Software, GitlabSoftware, GithubSoftware
|
||||||
|
|
||||||
|
|
||||||
|
def load_software_list(software_file: Path) -> list[Software]:
|
||||||
|
software_yaml = yaml.safe_load(software_file.open())
|
||||||
|
software_list = []
|
||||||
|
for software in software_yaml.get("software", []):
|
||||||
|
match software.get("cvs").lower():
|
||||||
|
case "github":
|
||||||
|
software_list.append(GithubSoftware(**software))
|
||||||
|
case "gitlab":
|
||||||
|
software_list.append(GitlabSoftware(**software))
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
return software_list
|
||||||
|
|
||||||
|
|
||||||
|
def is_software_to_update(software: Software) -> bool:
|
||||||
|
return local_files.LocalFile(software).is_file_absent()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pass
|
52
src/losoup/models.py
Normal file
52
src/losoup/models.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
|
|
||||||
|
class Software(BaseModel):
|
||||||
|
name: str
|
||||||
|
owner: str
|
||||||
|
repo: str
|
||||||
|
version: str
|
||||||
|
folder: str
|
||||||
|
filename_format: str = Field(..., alias="filenameFormat")
|
||||||
|
|
||||||
|
@field_validator("folder")
|
||||||
|
@classmethod
|
||||||
|
def replace_tilde(cls, value: str):
|
||||||
|
return value.replace("~", Path.home().as_posix())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename(self) -> str:
|
||||||
|
return self.filename_format.replace("{{version}}", self.version)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def absolute_path(self) -> Path:
|
||||||
|
return Path(self.folder, self.filename)
|
||||||
|
|
||||||
|
|
||||||
|
class GithubSoftware(Software):
|
||||||
|
base_url: str = "https://api.github.com"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def release_url(self):
|
||||||
|
if self.version == "latest":
|
||||||
|
release_path = self.version
|
||||||
|
else:
|
||||||
|
release_path = f"tags/{self.version}"
|
||||||
|
|
||||||
|
return f"{self.base_url}/repos/{self.owner}/{self.repo}/releases/{release_path}"
|
||||||
|
|
||||||
|
|
||||||
|
class GitlabSoftware(Software):
|
||||||
|
base_url: str = Field(..., alias="baseUrl")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def release_url(self):
|
||||||
|
return f"{self.base_url}/{self.owner}/{self.repo}/-/releases/{self.version}"
|
||||||
|
|
||||||
|
|
||||||
|
class Asset(BaseModel):
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
|
content: bytes | str | None = None
|
0
test/__init__.py
Normal file
0
test/__init__.py
Normal file
0
test/losoup/__init__.py
Normal file
0
test/losoup/__init__.py
Normal file
121
test/losoup/conftest.py
Normal file
121
test/losoup/conftest.py
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from losoup.file_operations.local_files import LocalFile
|
||||||
|
from losoup.models import GithubSoftware, Asset
|
||||||
|
from losoup.cvs.github import GithubConnector
|
||||||
|
from losoup.cvs.github_settings import GithubSettings
|
||||||
|
|
||||||
|
current_dir = Path(__file__).parent.absolute()
|
||||||
|
dotenv = current_dir / "test.env"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def github_settings():
|
||||||
|
return GithubSettings(_env_file=dotenv)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def github_connector(github_settings):
|
||||||
|
return GithubConnector(github_settings)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def keepassxc():
|
||||||
|
return GithubSoftware(
|
||||||
|
name="KeepassXC",
|
||||||
|
owner="keepassxreboot",
|
||||||
|
repo="keepassxc",
|
||||||
|
version="2.7.9",
|
||||||
|
folder="~/Softwares",
|
||||||
|
filenameFormat="KeePassXC-{{version}}-x86_64.AppImage",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def latest_keepassxc():
|
||||||
|
return GithubSoftware(
|
||||||
|
name="KeepassXC",
|
||||||
|
owner="keepassxreboot",
|
||||||
|
repo="keepassxc",
|
||||||
|
version="latest",
|
||||||
|
folder="~/Softwares",
|
||||||
|
filenameFormat="KeePassXC-{{version}}-x86_64.AppImage",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def keepassxc_not_present():
|
||||||
|
return GithubSoftware(
|
||||||
|
name="KeepassXC",
|
||||||
|
owner="keepassxreboot",
|
||||||
|
repo="keepassxc",
|
||||||
|
version="2.79",
|
||||||
|
folder="~/Softwares",
|
||||||
|
filenameFormat="KeePassXC-{{version}}-x86_64.AppImage",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def local_file(keepassxc):
|
||||||
|
return LocalFile(keepassxc)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def local_file_not_present(keepassxc_not_present):
|
||||||
|
return LocalFile(keepassxc_not_present)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def software_to_update():
|
||||||
|
return GithubSoftware(
|
||||||
|
name="KeepassXC",
|
||||||
|
owner="keepassxreboot",
|
||||||
|
repo="keepassxc",
|
||||||
|
version="2.8.1",
|
||||||
|
folder="~/Softwares",
|
||||||
|
filenameFormat="KeePassXC-{{version}}-x86_64.AppImage",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def software_up_to_date(keepassxc):
|
||||||
|
return keepassxc
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def asset():
|
||||||
|
return Asset(
|
||||||
|
name="KeePassXC-2.7.9-x86_64.AppImage.DIGEST",
|
||||||
|
url="https://api.github.com/repos/keepassxreboot/keepassxc/releases/assets/1747"
|
||||||
|
"89149",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def keepassxc_digest():
|
||||||
|
return GithubSoftware(
|
||||||
|
name="KeepassXC digest",
|
||||||
|
owner="keepassxreboot",
|
||||||
|
repo="keepassxc",
|
||||||
|
version="2.7.7",
|
||||||
|
folder="~/Softwares",
|
||||||
|
filenameFormat="KeePassXC-{{version}}-x86_64.AppImage.DIGEST",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def keepassxc_digest_file(keepassxc_digest):
|
||||||
|
return LocalFile(keepassxc_digest)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def asset_to_write():
|
||||||
|
return Asset(
|
||||||
|
name="KeePassXC-2.7.7-x86_64.AppImage.DIGEST",
|
||||||
|
url="https://api.github.com/repos/keepassxreboot/keepassxc/releases/assets/1747"
|
||||||
|
"89149",
|
||||||
|
content=b"2a868b681a8ec4e381ac14203aec3d80ff6fa7a535fa102265a3ec9329b4b846 Kee"
|
||||||
|
b"PassXC-2.7.9-x86_64.AppImage\n",
|
||||||
|
)
|
16
test/losoup/software.yaml
Normal file
16
test/losoup/software.yaml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
software:
|
||||||
|
- name: NextCloud
|
||||||
|
cvs: github
|
||||||
|
owner: nextcloud-releases
|
||||||
|
repo: desktop
|
||||||
|
version: latest
|
||||||
|
folder: /home/stan/Softwares
|
||||||
|
filenameFormat: KeePassXC-{{version}}-x86_64.AppImage
|
||||||
|
|
||||||
|
- name: KeePassXC
|
||||||
|
cvs: giTHub
|
||||||
|
owner: keepassxreboot
|
||||||
|
repo: keepassxc
|
||||||
|
version: latest
|
||||||
|
folder: /home/stan/Softwares
|
||||||
|
filenameFormat: Nextcloud-{{version}}-x86_64.AppImage
|
40
test/losoup/test_github_connector.py
Normal file
40
test/losoup/test_github_connector.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
def test_github_connector_is_initialized(github_connector):
|
||||||
|
assert github_connector is not None
|
||||||
|
assert github_connector.settings.token is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_latest_release(github_connector, latest_keepassxc):
|
||||||
|
release_assets = github_connector.get_release_assets_list(
|
||||||
|
latest_keepassxc.release_url
|
||||||
|
)
|
||||||
|
assert len(release_assets) == 27
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_some_release(github_connector, keepassxc):
|
||||||
|
release_asset_list = github_connector.get_release_assets_list(keepassxc.release_url)
|
||||||
|
assert len(release_asset_list) == 27
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_release_asset(github_connector, keepassxc):
|
||||||
|
release_asset = github_connector.get_release_asset(keepassxc)
|
||||||
|
assert release_asset.name == keepassxc.filename
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_release_asset(github_connector, asset):
|
||||||
|
release_asset_file = github_connector.download_release_asset(asset)
|
||||||
|
assert len(release_asset_file) == 98
|
||||||
|
assert (
|
||||||
|
release_asset_file
|
||||||
|
== b"2a868b681a8ec4e381ac14203aec3d80ff6fa7a535fa102265a3ec9329b4b846 KeePassX"
|
||||||
|
b"C-2.7.9-x86_64.AppImage\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_software(github_connector, keepassxc_digest):
|
||||||
|
release_asset_file = github_connector.download_software(keepassxc_digest)
|
||||||
|
assert len(release_asset_file) == 98
|
||||||
|
assert (
|
||||||
|
release_asset_file
|
||||||
|
== b"796c4c0ad20b124476195dacd7f86c75be51cace1c734174ec293c46f41c3d05 KeePassX"
|
||||||
|
b"C-2.7.7-x86_64.AppImage\n"
|
||||||
|
)
|
27
test/losoup/test_local_files.py
Normal file
27
test/losoup/test_local_files.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
from losoup.file_operations.local_files import LocalFile
|
||||||
|
from losoup.models import Asset
|
||||||
|
import typer
|
||||||
|
|
||||||
|
|
||||||
|
def test_local_files_init(keepassxc):
|
||||||
|
local_file = LocalFile(keepassxc)
|
||||||
|
assert local_file.software == keepassxc
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_local_files(local_file):
|
||||||
|
assert "KeePassXC-2.7.9-x86_64.AppImage" in local_file.get_files_in_folder()
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_file_present(local_file):
|
||||||
|
assert local_file.is_file_present()
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_file_absent(local_file_not_present):
|
||||||
|
assert local_file_not_present.is_file_absent()
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_file(keepassxc_digest_file: LocalFile, asset_to_write: Asset):
|
||||||
|
assert keepassxc_digest_file.is_file_absent()
|
||||||
|
keepassxc_digest_file.write_file(asset_to_write)
|
||||||
|
assert keepassxc_digest_file.is_file_present()
|
||||||
|
keepassxc_digest_file.delete_file()
|
29
test/losoup/test_main.py
Normal file
29
test/losoup/test_main.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from losoup import main
|
||||||
|
from losoup.models import GithubSoftware
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_files():
|
||||||
|
cur_dir = Path(__file__).parent
|
||||||
|
software_file = cur_dir / "software.yaml"
|
||||||
|
|
||||||
|
files = main.load_software_list(software_file)
|
||||||
|
|
||||||
|
assert len(files) == 2
|
||||||
|
|
||||||
|
nextcloud = files[0]
|
||||||
|
assert nextcloud.name == "NextCloud"
|
||||||
|
assert isinstance(nextcloud, GithubSoftware)
|
||||||
|
|
||||||
|
keepassxc = files[1]
|
||||||
|
assert keepassxc.name == "KeePassXC"
|
||||||
|
assert isinstance(nextcloud, GithubSoftware)
|
||||||
|
|
||||||
|
|
||||||
|
def test_software_needs_to_be_updated(software_to_update):
|
||||||
|
assert main.is_software_to_update(software_to_update)
|
||||||
|
|
||||||
|
|
||||||
|
def test_software_does_not_need_to_be_updated(software_up_to_date):
|
||||||
|
assert not main.is_software_to_update(software_up_to_date)
|
Loading…
Add table
Add a link
Reference in a new issue