diff --git a/libpacstall/cache/__init__.py b/libpacstall/cache/__init__.py new file mode 100644 index 0000000..1190a86 --- /dev/null +++ b/libpacstall/cache/__init__.py @@ -0,0 +1,226 @@ +# __ _ __ ____ __ ____ +# / / (_) /_ / __ \____ ___________/ /_____ _/ / / +# / / / / __ \/ /_/ / __ `/ ___/ ___/ __/ __ `/ / / +# / /___/ / /_/ / ____/ /_/ / /__(__ ) /_/ /_/ / / / +# /_____/_/_.___/_/ \__,_/\___/____/\__/\__,_/_/_/ +# +# Copyright (C) 2022-present +# +# This file is part of LibPacstall. +# +# LibPacstall is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# LibPacstall is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# LibPacstall. If not, see . + +"""Caching system.""" + +from datetime import datetime +from enum import Enum, auto +from typing import Dict, List, Optional + +from sqlalchemy.types import JSON +from sqlalchemy.types import Enum as SQLAlchemyEnum +from sqlmodel import Column, Field, Relationship, SQLModel + + +class InstalledStatus(Enum): + """ + Status of an installed package. + + Attributes + ---------- + DIRECT + The dependency was directly installed by the user. + INDIRECT + The dependency was installed by another dependency. + """ + + DIRECT = auto() + INDIRECT = auto() + + +class DependencyType(Enum): + """ + The type of a dependency. + + Attributes + ---------- + REQUIRED + The dependency is by the pacscript. + OPTIONAL + The dependency is not required by the pacscript. It's an optional + dependency. + """ + + REQUIRED = auto() + OPTIONAL = auto() + + +class APTDependencyPacscriptLink(SQLModel, table=True): + """ + Link between an APT dependency and a pacscript. + + Attributes + ---------- + dependency_name + The name of the dependency. + pacscript_name + The name of the pacscript. + """ + + dependency_name: Optional[int] = Field( + default=None, foreign_key="aptdependency.name", primary_key=True + ) + pacscript_name: Optional[str] = Field( + default=None, foreign_key="pacscript.name", primary_key=True + ) + + +class APTDependency(SQLModel, table=True): + """ + SQLModel of an APT dependency for a pacscript. + + Attributes + ---------- + name + Name of the dependency. + dependents + List of pacscripts that depend on this dependency. + type + Type of the dependency. + """ + + name: str = Field(primary_key=True) + dependents: List["Pacscript"] = Relationship( + back_populates="apt_dependencies", link_model=APTDependencyPacscriptLink + ) + + type: DependencyType = Field( + sa_column=Column(SQLAlchemyEnum(DependencyType)), primary_key=True + ) + + +class PacscriptDependencyLink(SQLModel, table=True): + """ + Link between a pacscript dependency and a pacscript. + + Attributes + ---------- + pacscript_name + Name of the pacscript. + dependency_name + Name of the dependency. + """ + + pacscript_name: Optional[str] = Field( + default=None, foreign_key="pacscript.name", primary_key=True + ) + dependency_name: Optional[str] = Field( + default=None, foreign_key="pacscriptdependency.name", primary_key=True + ) + + +class PacscriptDependency(SQLModel, table=True): + """ + SQLModel of a pacscript dependency of a pacscript. + + Attributes + ---------- + name + Name of the dependency. + dependents + List of pacscripts that depend on this dependency. + type + Type of dependency. + """ + + name: str = Field(primary_key=True) + dependents: List["Pacscript"] = Relationship( + back_populates="pacscript_dependencies", link_model=PacscriptDependencyLink + ) + type: DependencyType = Field( + sa_column=Column(SQLAlchemyEnum(DependencyType)), primary_key=True + ) + + +class Pacscript(SQLModel, table=True): + """ + SQLModel to access and write to the Pacscript database. + + There are two types of pacscripts stored in this table, one whose + `is_installed` is True, and the other whose `is_installed` is False. + `is_installed` is True for pacscripts that are installed, and False for + pacscripts that are not installed, which are from the sources. + + Attributes + ---------- + name + The name of the pacscript. + version + The version of the pacscript. + url + The URL of the pacscript. + homepage + The homepage of the pacscript. + description + The description of the pacscript. + installed_size + The installed size of the pacscript's package in bytes. + download_size + The downloaded size of the pacscript's package in bytes. + date + The date the pacscript was last updated. + installed_status + The installed status of the pacscript. + apt_dependencies + The list of apt dependencies of the pacscript. + apt_optional_dependencies + The list of apt optional dependencies of the pacscript. + pacscript_dependencies + The list of pacscript dependencies of the pacscript. + pacscript_optional_dependencies + The list of pacscript optional dependencies of the pacscript. + repology + The repology filters for the pacscript. + maintainer + The maintainer of the pacscript. + is_installed + Boolean to signify if the pacscript is installed. + """ + + name: str = Field(index=True, primary_key=True) + version: str + url: str + homepage: Optional[str] = None + description: str + installed_size: Optional[int] = None + download_size: int + date: datetime + installed_status: Optional[InstalledStatus] = Field( + default=None, sa_column=Column(SQLAlchemyEnum(InstalledStatus)) + ) + apt_dependencies: Optional[List[APTDependency]] = Relationship( + back_populates="dependents", + link_model=APTDependencyPacscriptLink, + ) + apt_optional_dependencies: Optional[Dict[str, str]] = Field( + default=None, sa_column=Column(JSON) + ) + pacscript_dependencies: Optional[List[PacscriptDependency]] = Relationship( + back_populates="dependents", + link_model=PacscriptDependencyLink, + ) + pacscript_optional_dependencies: Optional[Dict[str, str]] = Field( + default=None, sa_column=Column(JSON) + ) + repology: Optional[Dict[str, str]] = Field(default=None, sa_column=Column(JSON)) + maintainer: Optional[str] = None + is_installed: bool = Field(primary_key=True) diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 0000000..e2ddc21 --- /dev/null +++ b/tests/test_cache.py @@ -0,0 +1,268 @@ +# __ _ __ ____ __ ____ +# / / (_) /_ / __ \____ ___________/ /_____ _/ / / +# / / / / __ \/ /_/ / __ `/ ___/ ___/ __/ __ `/ / / +# / /___/ / /_/ / ____/ /_/ / /__(__ ) /_/ /_/ / / / +# /_____/_/_.___/_/ \__,_/\___/____/\__/\__,_/_/_/ +# +# Copyright (C) 2022-present +# +# This file is part of LibPacstall. +# +# LibPacstall is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# LibPacstall is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# LibPacstall. If not, see . + +"""Test suit for the cache module.""" + +from datetime import datetime +from typing import Generator + +import pytest +from sqlalchemy.exc import IntegrityError # type: ignore[misc] +from sqlmodel import Session, SQLModel, create_engine, select + +from libpacstall.cache import ( + APTDependency, + DependencyType, + InstalledStatus, + Pacscript, + PacscriptDependency, +) + + +@pytest.fixture +def session() -> Generator[Session, None, None]: + """ + Create a session for the tests. + + Yields + ------ + Session + The database session. + """ + + engine = create_engine("sqlite://", echo=True) + SQLModel.metadata.create_all(engine) + yield Session(engine) + + +class TestWrites: + """Test that writing to the database works.""" + + def test_full_write(self, session: Session) -> None: + """ + Test that we can write a full records to the database. + + Parameters + ---------- + session + The session database to use (Fixture) + """ + + with session: + session.add( + Pacscript( + name="foo", + version="1.0", + url="https://foo.bar", + homepage="https://foo.bar", + description="baz", + installed_size=420, + download_size=69, + date=datetime.now(), + apt_dependencies=[ + APTDependency(name="foo", type=DependencyType.REQUIRED) + ], + apt_optional_dependencies={"bar": "baz"}, + pacscript_dependencies=[ + PacscriptDependency(name="foo", type=DependencyType.REQUIRED) + ], + pacscript_optional_dependencies={"bit": "bat"}, + installed_status=InstalledStatus.DIRECT, + repology={"project": "foo", "visiblename": "bar"}, + maintainer="baz ", + is_installed=True, + ) + ) + + session.commit() + + def test_partial_write(self, session: Session) -> None: + """ + Test that partial writes to the database are possible. + + Parameters + ---------- + session + The session to use (Fixture) + """ + + with session: + session.add( + Pacscript( + name="foo", + version="1.0", + url="https://foo.bar", + description="baz", + download_size=69, + date=datetime.now(), + is_installed=True, + ) + ) + session.commit() + + def test_session_integrity(self, session: Session) -> None: + """ + Test that we can't add two pacscripts of the same name and is_installed + status. + + Parameters + ---------- + session + The session to use (Fixture) + """ + + with session: + session.add( + Pacscript( + name="foo", + version="1.0", + url="https://foo.bar", + homepage="https://foo.bar", + description="baz", + installed_size=420, + download_size=69, + date=datetime.now(), + apt_dependencies=[ + APTDependency(name="foo", type=DependencyType.REQUIRED) + ], + apt_optional_dependencies={"bar": "baz"}, + installed_status=InstalledStatus.DIRECT, + repology={"project": "foo", "visiblename": "bar"}, + maintainer="baz ", + is_installed=True, + ) + ) + + session.add( + Pacscript( + name="foo", + version="1.0", + url="https://foo.bar", + homepage="https://foo.bar", + description="baz", + installed_size=420, + download_size=69, + date=datetime.now(), + apt_dependencies=[ + APTDependency(name="foo", type=DependencyType.REQUIRED) + ], + apt_optional_dependencies={"bar": "baz"}, + installed_status=InstalledStatus.DIRECT, + repology={"project": "foo", "visiblename": "bar"}, + maintainer="baz ", + is_installed=True, + ) + ) + with pytest.raises(IntegrityError): + session.commit() + + +class TestReads: + """Test that reading from the database works.""" + + def test_read_all(self, session: Session) -> None: + """ + Test that we can read all pacscripts from the database. + + Parameters + ---------- + session + The session to use (Fixture) + """ + + installed_pacsript = Pacscript( + name="foo", + version="1.0", + url="https://foo.bar", + description="baz", + download_size=69, + date=datetime.now(), + is_installed=True, + ) + + source_pacscript = Pacscript( + name="foo", + version="1.0", + url="https://foo.bar", + description="baz", + download_size=69, + date=datetime.now(), + is_installed=False, + ) + + with session: + session.add(installed_pacsript) + session.add(source_pacscript) + session.commit() + + installed_pacscript_list = session.exec( + select(Pacscript).where(Pacscript.is_installed == True) + ).all() + + source_pacscript_list = session.exec( + select(Pacscript).where(Pacscript.is_installed == False) + ).all() + + assert len(session.exec(select(Pacscript)).all()) == 2 + + assert len(installed_pacscript_list) == 1 + assert len(source_pacscript_list) == 1 + + def test_read_by_name(self, session: Session) -> None: + """ + Test that we can read a pacscript by name from the database. + + Parameters + ---------- + session + The session to use (Fixture) + """ + + with session: + session.add( + Pacscript( + name="foo", + version="1.0", + url="https://foo.bar", + description="baz", + download_size=69, + date=datetime.now(), + is_installed=True, + ) + ) + + session.add( + Pacscript( + name="bar", + version="1.0", + url="https://foo.bar", + description="baz", + download_size=69, + date=datetime.now(), + is_installed=False, + ) + ) + + session.commit() + + assert len(session.query(Pacscript).filter(Pacscript.name == "bar").all()) == 1 + assert len(session.query(Pacscript).filter(Pacscript.name == "foo").all()) == 1