aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2022-08-25 16:37:53 +0200
committerWojtek Kosior <koszko@koszko.org>2022-09-28 12:54:54 +0200
commit367ea85057368047a50ae98a3510e0113eadd744 (patch)
tree5aecfd3a2e44377e9d331ca77346666ca193006c
parentc1f6a379b3a85303f487e1b366e96d9db90cd4e3 (diff)
downloadhaketilo-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.py2
-rw-r--r--src/hydrilla/proxy/state_impl/_operations/__init__.py2
-rw-r--r--src/hydrilla/proxy/state_impl/_operations/load_packages.py13
-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.py58
-rw-r--r--src/hydrilla/proxy/state_impl/base.py28
-rw-r--r--src/hydrilla/proxy/state_impl/concrete_state.py16
-rw-r--r--src/hydrilla/proxy/state_impl/mappings.py82
-rw-r--r--src/hydrilla/proxy/state_impl/repos.py26
-rw-r--r--src/hydrilla/proxy/web_ui/packages.py7
-rw-r--r--src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja5
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">