diff options
author | Wojtek Kosior <koszko@koszko.org> | 2022-08-25 16:37:53 +0200 |
---|---|---|
committer | Wojtek Kosior <koszko@koszko.org> | 2022-09-28 12:54:54 +0200 |
commit | 367ea85057368047a50ae98a3510e0113eadd744 (patch) | |
tree | 5aecfd3a2e44377e9d331ca77346666ca193006c | |
parent | c1f6a379b3a85303f487e1b366e96d9db90cd4e3 (diff) | |
download | haketilo-hydrilla-367ea85057368047a50ae98a3510e0113eadd744.tar.gz haketilo-hydrilla-367ea85057368047a50ae98a3510e0113eadd744.zip |
[proxy] make it possible to uninstall a package
This commit also brings some more refactoring under state_impl/.
-rw-r--r-- | src/hydrilla/proxy/state.py | 2 | ||||
-rw-r--r-- | src/hydrilla/proxy/state_impl/_operations/__init__.py | 2 | ||||
-rw-r--r-- | src/hydrilla/proxy/state_impl/_operations/load_packages.py | 13 | ||||
-rw-r--r-- | src/hydrilla/proxy/state_impl/_operations/prune_orphans.py (renamed from src/hydrilla/proxy/state_impl/_operations/prune_packages.py) | 12 | ||||
-rw-r--r-- | src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py | 58 | ||||
-rw-r--r-- | src/hydrilla/proxy/state_impl/base.py | 28 | ||||
-rw-r--r-- | src/hydrilla/proxy/state_impl/concrete_state.py | 16 | ||||
-rw-r--r-- | src/hydrilla/proxy/state_impl/mappings.py | 82 | ||||
-rw-r--r-- | src/hydrilla/proxy/state_impl/repos.py | 26 | ||||
-rw-r--r-- | src/hydrilla/proxy/web_ui/packages.py | 7 | ||||
-rw-r--r-- | src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja | 5 |
11 files changed, 159 insertions, 92 deletions
diff --git a/src/hydrilla/proxy/state.py b/src/hydrilla/proxy/state.py index fbd546e..803e727 100644 --- a/src/hydrilla/proxy/state.py +++ b/src/hydrilla/proxy/state.py @@ -236,7 +236,7 @@ class MappingVersionRef(Ref): ... @abstractmethod - def uninstall(self) -> None: + def uninstall(self) -> t.Optional['MappingVersionRef']: ... @abstractmethod diff --git a/src/hydrilla/proxy/state_impl/_operations/__init__.py b/src/hydrilla/proxy/state_impl/_operations/__init__.py index ff34b0b..359e2f5 100644 --- a/src/hydrilla/proxy/state_impl/_operations/__init__.py +++ b/src/hydrilla/proxy/state_impl/_operations/__init__.py @@ -4,7 +4,7 @@ # # Available under the terms of Creative Commons Zero v1.0 Universal. -from .prune_packages import prune_packages +from .prune_orphans import prune_orphans from .pull_missing_files import pull_missing_files from .load_packages import _load_packages_no_state_update from .recompute_dependencies import _recompute_dependencies_no_state_update diff --git a/src/hydrilla/proxy/state_impl/_operations/load_packages.py b/src/hydrilla/proxy/state_impl/_operations/load_packages.py index 107e8d6..f8fddfa 100644 --- a/src/hydrilla/proxy/state_impl/_operations/load_packages.py +++ b/src/hydrilla/proxy/state_impl/_operations/load_packages.py @@ -45,7 +45,7 @@ from .... import item_infos from ... import state from .recompute_dependencies import _recompute_dependencies_no_state_update, \ FileResolver -from .prune_packages import prune_packages +from .prune_orphans import prune_orphans def make_repo_iteration(cursor: sqlite3.Cursor, repo_id: int) -> int: cursor.execute( @@ -401,11 +401,16 @@ def _load_packages_no_state_update( repo_id = repo_id ) - prune_packages(cursor) + if repo_id != 1: + # In case of local semirepo (repo_id = 1) all packages from previous + # iteration are already orphans and can be assumed to be in a pruned + # state no matter what. + prune_orphans(cursor) _recompute_dependencies_no_state_update( - cursor = cursor, - semirepo_file_resolver = MalcontentFileResolver(malcontent_path) + cursor = cursor, + unlocked_required_mappings = [], + semirepo_file_resolver = MalcontentFileResolver(malcontent_path) ) return repo_iteration_id diff --git a/src/hydrilla/proxy/state_impl/_operations/prune_packages.py b/src/hydrilla/proxy/state_impl/_operations/prune_orphans.py index 6f4b3e7..f4ebd52 100644 --- a/src/hydrilla/proxy/state_impl/_operations/prune_packages.py +++ b/src/hydrilla/proxy/state_impl/_operations/prune_orphans.py @@ -38,12 +38,12 @@ from pathlib import Path _remove_item_versions_sqls = [ ''' - CREATE TEMPORARY TABLE removed_versions( + CREATE TEMPORARY TABLE __removed_versions( item_version_id INTEGER PRIMARY KEY ); ''', ''' INSERT INTO - removed_versions + __removed_versions SELECT iv.item_version_id FROM @@ -57,14 +57,14 @@ _remove_item_versions_sqls = [ SET active_version_id = NULL WHERE - active_version_id IN removed_versions; + active_version_id IN __removed_versions; ''', ''' DELETE FROM item_versions WHERE - item_version_id IN removed_versions; + item_version_id IN __removed_versions; ''', ''' - DROP TABLE removed_versions; + DROP TABLE __removed_versions; ''' ] @@ -134,7 +134,7 @@ WHERE repo_id IN removed_repos; ''' -def prune_packages(cursor: sqlite3.Cursor) -> None: +def prune_orphans(cursor: sqlite3.Cursor) -> None: assert cursor.connection.in_transaction for sql in _remove_item_versions_sqls: diff --git a/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py b/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py index d4c4d45..327a195 100644 --- a/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py +++ b/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py @@ -36,6 +36,7 @@ import typing as t from .... import item_infos from ... import simple_dependency_satisfying as sds +from .. import base from .pull_missing_files import pull_missing_files, FileResolver, \ DummyFileResolver @@ -73,6 +74,41 @@ def _get_infos_of_type(cursor: sqlite3.Cursor, info_type: t.Type[AnyInfoVar],) \ return result +def _get_current_required_state( + cursor: sqlite3.Cursor, + unlocked_required_mappings: t.Sequence[int] +) -> list[sds.MappingRequirement]: + # For mappings explicitly enabled by the user (+ all mappings they + # recursively depend on) let's make sure that their exact same versions will + # be enabled after the change. Make exception for mappings specified by the + # caller. + with base.temporary_ids_table( + cursor = cursor, + ids = unlocked_required_mappings, + table_name = '__unlocked_ids' + ): + cursor.execute( + ''' + SELECT + definition, repo, repo_iteration + FROM + item_versions_extra + WHERE + item_id NOT IN __unlocked_ids AND active = 'R'; + ''', + ) + + rows = cursor.fetchall() + + requirements: list[sds.MappingRequirement] = [] + + for definition, repo, iteration in rows: + info = item_infos.MappingInfo.load(definition, repo, iteration) + req = sds.MappingVersionRequirement(info.identifier, info) + requirements.append(req) + + return requirements + def _mark_version_installed(cursor: sqlite3.Cursor, version_id: int) -> None: cursor.execute( ''' @@ -87,8 +123,8 @@ def _mark_version_installed(cursor: sqlite3.Cursor, version_id: int) -> None: ) def _recompute_dependencies_no_state_update_no_pull_files( - cursor: sqlite3.Cursor, - extra_requirements: t.Iterable[sds.MappingRequirement] + cursor: sqlite3.Cursor, + unlocked_required_mappings: base.NoLockArg = [], ) -> None: cursor.execute('DELETE FROM payloads;') @@ -98,7 +134,13 @@ def _recompute_dependencies_no_state_update_no_pull_files( resources_to_ids = dict((info, id) for id, info in ids_to_resources.items()) mappings_to_ids = dict((info, id) for id, info in ids_to_mappings.items()) - requirements = [*extra_requirements] + if unlocked_required_mappings == 'all_mappings_unlocked': + requirements = [] + else: + requirements = _get_current_required_state( + cursor = cursor, + unlocked_required_mappings = unlocked_required_mappings + ) cursor.execute( ''' @@ -276,13 +318,13 @@ def _recompute_dependencies_no_state_update_no_pull_files( def _recompute_dependencies_no_state_update( - cursor: sqlite3.Cursor, - extra_requirements: t.Iterable[sds.MappingRequirement] = (), - semirepo_file_resolver: FileResolver = DummyFileResolver() + cursor: sqlite3.Cursor, + unlocked_required_mappings: base.NoLockArg = [], + semirepo_file_resolver: FileResolver = DummyFileResolver() ) -> None: _recompute_dependencies_no_state_update_no_pull_files( - cursor, - extra_requirements + cursor = cursor, + unlocked_required_mappings = unlocked_required_mappings ) pull_missing_files(cursor, semirepo_file_resolver) diff --git a/src/hydrilla/proxy/state_impl/base.py b/src/hydrilla/proxy/state_impl/base.py index a889e71..e5a9898 100644 --- a/src/hydrilla/proxy/state_impl/base.py +++ b/src/hydrilla/proxy/state_impl/base.py @@ -49,6 +49,25 @@ from .. import state as st from .. import policies +@contextmanager +def temporary_ids_table( + cursor: sqlite3.Cursor, + ids: t.Iterable[int], + table_name: str = '__helper_ids' +) -> t.Iterator[None]: + cursor.execute( + f'CREATE TEMPORARY TABLE "{table_name}"(id INTEGER PRIMARY KEY);' + ) + + try: + for id in ids: + cursor.execute(f'INSERT INTO "{table_name}" VALUES(?);', (id,)) + + yield + finally: + cursor.execute(f'DROP TABLE "{table_name}";') + + @dc.dataclass(frozen=True) class PolicyTree(pattern_tree.PatternTree[policies.PolicyFactory]): SelfType = t.TypeVar('SelfType', bound='PolicyTree') @@ -105,6 +124,7 @@ def mark_failed_file_installs( (file_sha256, repo_id) ) +NoLockArg = t.Union[t.Sequence[int], t.Literal['all_mappings_unlocked']] PayloadsData = t.Mapping[st.PayloadRef, st.PayloadData] @@ -200,12 +220,14 @@ class HaketiloStateWithFields(st.HaketiloState): ... @abstractmethod + def prune_orphans(self) -> None: + ... + + @abstractmethod def recompute_dependencies( self, - requirements: t.Iterable[sds.MappingRequirement] = [], - prune_orphans: bool = False + unlocked_required_mappings: NoLockArg = [] ) -> None: - """....""" ... @abstractmethod diff --git a/src/hydrilla/proxy/state_impl/concrete_state.py b/src/hydrilla/proxy/state_impl/concrete_state.py index 0de67e0..f180ec6 100644 --- a/src/hydrilla/proxy/state_impl/concrete_state.py +++ b/src/hydrilla/proxy/state_impl/concrete_state.py @@ -140,20 +140,22 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): self.rebuild_structures() + def prune_orphans(self) -> None: + with self.cursor() as cursor: + assert self.connection.in_transaction + + _operations.prune_orphans(cursor) + def recompute_dependencies( self, - extra_requirements: t.Iterable[sds.MappingRequirement] = [], - prune_orphans: bool = False, + unlocked_required_mappings: base.NoLockArg = [] ) -> None: with self.cursor() as cursor: assert self.connection.in_transaction - if prune_orphans: - _operations.prune_packages(cursor) - _operations._recompute_dependencies_no_state_update( - cursor = cursor, - extra_requirements = extra_requirements + cursor = cursor, + unlocked_required_mappings = unlocked_required_mappings ) self.rebuild_structures() diff --git a/src/hydrilla/proxy/state_impl/mappings.py b/src/hydrilla/proxy/state_impl/mappings.py index 8a401b8..eb8b4d2 100644 --- a/src/hydrilla/proxy/state_impl/mappings.py +++ b/src/hydrilla/proxy/state_impl/mappings.py @@ -183,11 +183,8 @@ class ConcreteMappingStore(st.MappingStore): class ConcreteMappingVersionRef(st.MappingVersionRef): state: base.HaketiloStateWithFields = dc.field(hash=False, compare=False) - def _set_installed_status( - self, - cursor: sqlite3.Cursor, - new_status: st.InstalledStatus - ) -> None: + def _set_installed_status(self, cursor: sqlite3.Cursor, new_status: str) \ + -> None: cursor.execute( ''' UPDATE @@ -197,50 +194,63 @@ class ConcreteMappingVersionRef(st.MappingVersionRef): WHERE item_version_id = ?; ''', - (new_status.value, self.id,) + (new_status, self.id,) ) - def install(self) -> None: - with self.state.cursor(transaction=True) as cursor: - cursor.execute( - ''' - SELECT - installed - FROM - item_versions - WHERE - item_version_id = ?; - ''', - (self.id,) - ) + def _get_statuses(self, cursor: sqlite3.Cursor) -> t.Tuple[str, str]: + cursor.execute( + ''' + SELECT + installed, active + FROM + item_versions + WHERE + item_version_id = ?; + ''', + (self.id,) + ) - rows = cursor.fetchall() + rows = cursor.fetchall() - if rows == []: - raise st.MissingItemError() + if rows == []: + raise st.MissingItemError() + + (installed_status, active_status), = rows - (installed_status,), = rows + return installed_status, active_status + + def install(self) -> None: + with self.state.cursor(transaction=True) as cursor: + installed_status, _ = self._get_statuses(cursor) if installed_status == 'I': return - self._set_installed_status(cursor, st.InstalledStatus.INSTALLED) + self._set_installed_status(cursor, 'I') self.state.pull_missing_files() def uninstall(self) -> None: - raise NotImplementedError() - # with self.state.cursor(transaction=True) as cursor: - # info = self.get_display_info() - - # if info.installed == st.InstalledStatus.NOT_INSTALLED: - # return - - # if info.installed == st.InstalledStatus.FAILED_TO_INSTALL: - # self._set_installed_status(st.InstalledStatus.UNINSTALLED) - # return - # - # .... + with self.state.cursor(transaction=True) as cursor: + installed_status, active_status = self._get_statuses(cursor) + + if installed_status == 'N': + return + + self._set_installed_status(cursor, 'N') + + self.state.prune_orphans() + + if active_status == 'R': + self.state.recompute_dependencies() + + cursor.execute( + 'SELECT COUNT(*) FROM item_versions WHERE item_version_id = ?;', + (self.id,) + ) + + (version_still_present,), = cursor.fetchall() + return self if version_still_present else None def get_all_version_display_infos(self) \ -> t.Sequence[st.MappingVersionDisplayInfo]: diff --git a/src/hydrilla/proxy/state_impl/repos.py b/src/hydrilla/proxy/state_impl/repos.py index 838698c..8a3fe64 100644 --- a/src/hydrilla/proxy/state_impl/repos.py +++ b/src/hydrilla/proxy/state_impl/repos.py @@ -193,28 +193,8 @@ class ConcreteRepoRef(st.RepoRef): (self.id,) ) - # For mappings explicitly enabled by the user (+ all mappings they - # recursively depend on) let's make sure that their exact same - # versions will be enabled after the change. - cursor.execute( - ''' - SELECT - definition, repo, repo_iteration - FROM - item_versions_extra - WHERE - active = 'R'; - ''' - ) - - requirements = [] - - for definition, repo, iteration in cursor.fetchall(): - info = item_infos.MappingInfo.load(definition, repo, iteration) - req = sds.MappingVersionRequirement(info.identifier, info) - requirements.append(req) - - self.state.recompute_dependencies(requirements, prune_orphans=True) + self.state.prune_orphans() + self.state.recompute_dependencies() def update( self, @@ -255,7 +235,7 @@ class ConcreteRepoRef(st.RepoRef): except sqlite3.IntegrityError: raise st.RepoNameTaken() - self.state.recompute_dependencies() + self.state.rebuild_structures() def refresh(self) -> None: with self.state.cursor(transaction=True) as cursor: diff --git a/src/hydrilla/proxy/web_ui/packages.py b/src/hydrilla/proxy/web_ui/packages.py index 1edddc0..31d3dbb 100644 --- a/src/hydrilla/proxy/web_ui/packages.py +++ b/src/hydrilla/proxy/web_ui/packages.py @@ -175,8 +175,11 @@ def alter_package_version(mapping_version_id: str) -> werkzeug.Response: if action == 'install_package': mapping_version_ref.install() elif action == 'uninstall_package': - mapping_version_ref.uninstall() - return flask.redirect(flask.url_for('.packages')) + mapping_version_ref = mapping_version_ref.uninstall() + if mapping_version_ref is None: + return flask.redirect(flask.url_for('.packages')) + else: + return show_package_version(mapping_version_id) else: raise ValueError() except st.FileInstallationError: diff --git a/src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja index f3ed206..2c6863b 100644 --- a/src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja @@ -48,7 +48,10 @@ in a proprietary work, I am not going to enforce this in court. <div> {{ _('web_ui.packages.single_version.package_is_installed') }} </div> - {% if uninstall_disallowed is not defined %} + {% + if uninstall_disallowed is not defined and + display_info.active != ActiveStatus.REQUIRED + %} <form method="POST"> <input name="action" value="uninstall_package" type="hidden"> <button class="green-button"> |