From 5fefb11ffd50dcda826cd5a256c8b3f650221050 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Tue, 23 Aug 2022 09:26:16 +0200 Subject: introduce package installation state enum --- src/hydrilla/proxy/state.py | 63 +++++--- .../_operations/recompute_dependencies.py | 7 +- src/hydrilla/proxy/state_impl/concrete_state.py | 2 +- src/hydrilla/proxy/state_impl/mappings.py | 178 ++++++++++++++++----- src/hydrilla/proxy/state_impl/repos.py | 5 +- src/hydrilla/proxy/tables.sql | 30 ++-- src/hydrilla/proxy/web_ui/packages.py | 35 +++- src/hydrilla/proxy/web_ui/root.py | 2 + .../proxy/web_ui/templates/packages.html.jinja | 19 +-- .../templates/packages__show_single.html.jinja | 35 +++- .../packages__show_single_version.html.jinja | 38 +++++ 11 files changed, 314 insertions(+), 100 deletions(-) create mode 100644 src/hydrilla/proxy/web_ui/templates/packages__show_single_version.html.jinja (limited to 'src/hydrilla') diff --git a/src/hydrilla/proxy/state.py b/src/hydrilla/proxy/state.py index d975d2f..0c6dff3 100644 --- a/src/hydrilla/proxy/state.py +++ b/src/hydrilla/proxy/state.py @@ -65,6 +65,24 @@ class EnabledStatus(Enum): AUTO_ENABLED = 'A' NO_MARK = 'N' + +class InstalledStatus(Enum): + """ + INSTALLED - Mapping's all files are present and mapping data is not going to + be automatically removed. + + NOT_INSTALLED - Some of the mapping's files might be absent. Mapping can be + automatically removed if it is orphaned. + + FAILED_TO_INSTALL - Same as "NOT_INSTALLED" but we additionally know that + the last automatic attempt to install mapping's files from repository + was unsuccessful. + """ + INSTALLED = 'I' + NOT_INSTALLED = 'N' + FAILED_TO_INSTALL = 'F' + + @dc.dataclass(frozen=True, unsafe_hash=True) class Ref: """....""" @@ -145,7 +163,7 @@ class RepoDisplayInfo: class RepoStore(Store[RepoRef]): @abstractmethod def get_display_infos(self, include_deleted: bool = False) -> \ - t.Iterable[RepoDisplayInfo]: + t.Sequence[RepoDisplayInfo]: ... @abstractmethod @@ -159,21 +177,37 @@ class RepoIterationRef(Ref): pass +@dc.dataclass(frozen=True) +class MappingDisplayInfo: + ref: 'MappingRef' + identifier: str + enabled: EnabledStatus + active_version_ref: t.Optional['MappingVersionRef'] + active_version_info: t.Optional[item_infos.MappingInfo] + +@dc.dataclass(frozen=True) +class MappingVersionDisplayInfo: + ref: 'MappingVersionRef' + info: item_infos.MappingInfo + installed: InstalledStatus + is_active: bool + is_orphan: bool + is_local: bool + mapping_enabled: EnabledStatus + @dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] class MappingRef(Ref): """....""" @abstractmethod - def disable(self, state: 'HaketiloState') -> None: - """....""" + def get_version_display_infos(self) \ + -> t.Sequence[MappingVersionDisplayInfo]: ... - @abstractmethod - def forget_enabled(self, state: 'HaketiloState') -> None: - """....""" - ... class MappingStore(Store[MappingRef]): - pass + @abstractmethod + def get_display_infos(self) -> t.Sequence[MappingDisplayInfo]: + ... @dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] @@ -185,20 +219,11 @@ class MappingVersionRef(Ref): ... @abstractmethod - def get_display_info(self) -> MappingDisplayInfo: + def get_display_info(self) -> MappingVersionDisplayInfo: ... -@dc.dataclass(frozen=True) -class MappingDisplayInfo: - ref: MappingVersionRef - info: item_infos.MappingInfo - enabled: EnabledStatus - is_orphan: bool - class MappingVersionStore(Store[MappingVersionRef]): - @abstractmethod - def get_display_infos(self) -> t.Iterable[MappingDisplayInfo]: - ... + pass @dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] diff --git a/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py b/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py index f83eb09..2b18a51 100644 --- a/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py +++ b/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py @@ -31,9 +31,12 @@ # Enable using with Python 3.7. from __future__ import annotations +import sqlite3 import typing as t -import sqlite3 +from urllib.parse import urlparse, urljoin + +import requests from .... import item_infos from ... import simple_dependency_satisfying as sds @@ -260,7 +263,7 @@ def _pull_missing_files(cursor: sqlite3.Cursor) -> None: rows = cursor.fetchall() - for file_id, sha56, repo_id, repo_url in rows: + for file_id, sha256, repo_id, repo_url in rows: try: response = requests.get(urljoin(repo_url, f'file/sha256/{sha256}')) assert response.ok diff --git a/src/hydrilla/proxy/state_impl/concrete_state.py b/src/hydrilla/proxy/state_impl/concrete_state.py index fb4e2ca..ec16e11 100644 --- a/src/hydrilla/proxy/state_impl/concrete_state.py +++ b/src/hydrilla/proxy/state_impl/concrete_state.py @@ -147,7 +147,7 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): return ConcreteRepoIterationRef(repo_iteration_id) def mapping_store(self) -> st.MappingStore: - raise NotImplementedError() + return mappings.ConcreteMappingStore(self) def mapping_version_store(self) -> st.MappingVersionStore: return mappings.ConcreteMappingVersionStore(self) diff --git a/src/hydrilla/proxy/state_impl/mappings.py b/src/hydrilla/proxy/state_impl/mappings.py index cce2a36..e5b324d 100644 --- a/src/hydrilla/proxy/state_impl/mappings.py +++ b/src/hydrilla/proxy/state_impl/mappings.py @@ -41,29 +41,28 @@ from . import base @dc.dataclass(frozen=True, unsafe_hash=True) -class ConcreteMappingVersionRef(st.MappingVersionRef): - """....""" +class ConcreteMappingRef(st.MappingRef): state: base.HaketiloStateWithFields = dc.field(hash=False, compare=False) - def update_status(self, new_status: st.EnabledStatus) -> None: - """....""" - assert new_status != st.EnabledStatus.AUTO_ENABLED - raise NotImplementedError() - - def get_display_info(self) -> st.MappingDisplayInfo: + def get_version_display_infos(self) \ + -> t.Sequence[st.MappingVersionDisplayInfo]: with self.state.cursor() as cursor: cursor.execute( ''' SELECT - enabled, + item_version_id, definition, repo, repo_iteration, - is_orphan + installed, + is_active, + is_orphan, + is_local, + enabled FROM mapping_display_infos WHERE - item_version_id = ?; + item_id = ?; ''', (self.id,) ) @@ -73,61 +72,154 @@ class ConcreteMappingVersionRef(st.MappingVersionRef): if rows == []: raise st.MissingItemError() - (status_letter, definition, repo, repo_iteration, is_orphan), = rows + result = [] - item_info = item_infos.MappingInfo.load( - definition, - repo, - repo_iteration - ) + for (item_version_id, definition, repo, repo_iteration, installed, + is_active, is_orphan, is_local, status_letter) in rows: + ref = ConcreteMappingVersionRef(str(item_version_id), self.state) - status = st.EnabledStatus(status_letter) + item_info = item_infos.MappingInfo.load( + definition, + repo, + repo_iteration + ) - return st.MappingDisplayInfo(self, item_info, status, is_orphan) + display_info = st.MappingVersionDisplayInfo( + ref = ref, + info = item_info, + installed = st.InstalledStatus(installed), + is_active = is_active, + is_orphan = is_orphan, + is_local = is_local, + mapping_enabled = st.EnabledStatus(status_letter) + ) + result.append(display_info) + + return sorted(result, key=(lambda di: di.info)) @dc.dataclass(frozen=True) -class ConcreteMappingVersionStore(st.MappingVersionStore): +class ConcreteMappingStore(st.MappingStore): state: base.HaketiloStateWithFields - def get(self, id: str) -> st.MappingVersionRef: - return ConcreteMappingVersionRef(id, self.state) + def get(self, id: str) -> st.MappingRef: + return ConcreteMappingRef(id, self.state) - def get_display_infos(self) -> t.Iterable[st.MappingDisplayInfo]: + def get_display_infos(self) -> t.Sequence[st.MappingDisplayInfo]: + with self.state.cursor() as cursor: + cursor.execute( + ''' + SELECT DISTINCT + item_id, + identifier, + CASE WHEN enabled IN ('A', 'E') THEN item_version_id + ELSE NULL END, + CASE WHEN enabled IN ('A', 'E') THEN definition + ELSE NULL END, + CASE WHEN enabled IN ('A', 'E') THEN repo + ELSE NULL END, + CASE WHEN enabled IN ('A', 'E') THEN repo_iteration + ELSE NULL END, + enabled + FROM + mapping_display_infos + WHERE + is_active OR item_version_id IS NULL; + ''' + ) + + rows = cursor.fetchall() + + result = [] + + for (item_id, identifier, item_version_id, definition, repo, + repo_iteration, status_letter) in rows: + ref = ConcreteMappingRef(str(item_id), self.state) + + version_ref: t.Optional[st.MappingVersionRef] = None + item_info: t.Optional[item_infos.MappingInfo] = None + + if item_version_id is not None: + active_version_ref = ConcreteMappingVersionRef( + id = str(item_version_id), + state = self.state + ) + item_info = item_infos.MappingInfo.load( + definition, + repo, + repo_iteration + ) + + display_info = st.MappingDisplayInfo( + ref = ref, + identifier = identifier, + enabled = st.EnabledStatus(status_letter), + active_version_ref = active_version_ref, + active_version_info = item_info + ) + + result.append(display_info) + + return result + +@dc.dataclass(frozen=True, unsafe_hash=True) +class ConcreteMappingVersionRef(st.MappingVersionRef): + state: base.HaketiloStateWithFields = dc.field(hash=False, compare=False) + + def update_status(self, new_status: st.EnabledStatus) -> None: + """....""" + assert new_status != st.EnabledStatus.AUTO_ENABLED + raise NotImplementedError() + + def get_display_info(self) -> st.MappingVersionDisplayInfo: with self.state.cursor() as cursor: cursor.execute( ''' SELECT - enabled, - item_version_id, definition, repo, repo_iteration, - is_orphan + installed, + is_orphan, + is_active, + is_local, + enabled FROM - mapping_display_infos; - ''' + mapping_display_infos + WHERE + item_version_id = ?; + ''', + (self.id,) ) - all_rows = cursor.fetchall() + rows = cursor.fetchall() - result = [] + if rows == []: + raise st.MissingItemError() - for row in all_rows: - (status_letter, item_version_id, definition, repo, repo_iteration, - is_orphan) = row + (definition, repo, repo_iteration, installed, is_orphan, is_active, + is_local, status_letter), = rows - ref = ConcreteMappingVersionRef(str(item_version_id), self.state) + item_info = item_infos.MappingInfo.load( + definition, + repo, + repo_iteration + ) - item_info = item_infos.MappingInfo.load( - definition, - repo, - repo_iteration - ) + return st.MappingVersionDisplayInfo( + ref = self, + info = item_info, + installed = st.InstalledStatus(installed), + is_active = is_active, + is_orphan = is_orphan, + is_local = is_local, + mapping_enabled = st.EnabledStatus(status_letter) + ) - status = st.EnabledStatus(status_letter) - info = st.MappingDisplayInfo(ref, item_info, status, is_orphan) - result.append(info) +@dc.dataclass(frozen=True) +class ConcreteMappingVersionStore(st.MappingVersionStore): + state: base.HaketiloStateWithFields - return sorted(result, key=(lambda di: di.info)) + def get(self, id: str) -> st.MappingVersionRef: + return ConcreteMappingVersionRef(id, self.state) diff --git a/src/hydrilla/proxy/state_impl/repos.py b/src/hydrilla/proxy/state_impl/repos.py index f4c7c71..663a450 100644 --- a/src/hydrilla/proxy/state_impl/repos.py +++ b/src/hydrilla/proxy/state_impl/repos.py @@ -35,7 +35,6 @@ from __future__ import annotations import re import json import tempfile -import requests import sqlite3 import typing as t import dataclasses as dc @@ -44,6 +43,8 @@ from urllib.parse import urlparse, urljoin from datetime import datetime from pathlib import Path +import requests + from ... import json_instances from ... import item_infos from ... import versions @@ -349,7 +350,7 @@ class ConcreteRepoStore(st.RepoStore): return ConcreteRepoRef(str(repo_id), self.state) def get_display_infos(self, include_deleted: bool = False) \ - -> t.Iterable[st.RepoDisplayInfo]: + -> t.Sequence[st.RepoDisplayInfo]: with self.state.cursor() as cursor: condition: str = 'TRUE' if include_deleted: diff --git a/src/hydrilla/proxy/tables.sql b/src/hydrilla/proxy/tables.sql index 3b3506d..3b84741 100644 --- a/src/hydrilla/proxy/tables.sql +++ b/src/hydrilla/proxy/tables.sql @@ -238,28 +238,30 @@ CREATE TABLE payloads( CREATE VIEW mapping_display_infos AS SELECT + i.item_id, i.identifier, CASE WHEN - ms.enabled = 'N' AND COUNT(p.payload_id) > 0 + ms.enabled = 'N' AND + (ms.required OR COUNT(p.payload_id) > 0) THEN 'A' -- AUTO_ENABLED mapping ELSE ms.enabled END AS enabled, - iv.item_version_id, iv.definition, + iv.item_version_id, iv.definition, iv.installed, r.name AS repo, ri.iteration AS repo_iteration, - COALESCE( - r.active_iteration_id != ri.repo_iteration_id, - TRUE - ) AND r.repo_id != 1 AS is_orphan + COALESCE(r.active_iteration_id, -1) != ri.repo_iteration_id AND + r.repo_id != 1 AS is_orphan, + r.repo_id = 1 AS is_local, + COALESCE(ms.active_version_id, -1) = iv.item_version_id AS is_active FROM - item_versions AS iv - LEFT JOIN payloads AS p - ON iv.item_version_id = p.mapping_item_id - JOIN items AS i - USING (item_id) + items AS i JOIN mapping_statuses AS ms USING (item_id) + JOIN item_versions AS iv + USING (item_id) + LEFT JOIN payloads AS p + ON iv.item_version_id = p.mapping_item_id JOIN repo_iterations AS ri USING (repo_iteration_id) JOIN repos AS r @@ -267,9 +269,9 @@ FROM WHERE i.type = 'M' GROUP BY - ms.enabled, - iv.item_version_id, iv.definition, - r.name, ri.iteration; + ms.enabled, ms.required, ms.active_version_id, + iv.item_version_id, iv.definition, iv.installed, + r.repo_id, r.active_iteration_id, r.name, ri.iteration; CREATE TABLE resolved_depended_resources( payload_id INTEGER, diff --git a/src/hydrilla/proxy/web_ui/packages.py b/src/hydrilla/proxy/web_ui/packages.py index 90876aa..d4146a5 100644 --- a/src/hydrilla/proxy/web_ui/packages.py +++ b/src/hydrilla/proxy/web_ui/packages.py @@ -94,7 +94,7 @@ def load_from_disk_post() -> werkzeug.Response: @bp.route('/packages') def packages() -> werkzeug.Response: - store = _app.get_haketilo_state().mapping_version_store() + store = _app.get_haketilo_state().mapping_store() html = flask.render_template( 'packages.html.jinja', @@ -105,11 +105,40 @@ def packages() -> werkzeug.Response: @bp.route('/packages/view/') def show_package(mapping_id: str) -> werkzeug.Response: try: - store = _app.get_haketilo_state().mapping_version_store() - display_info = store.get(mapping_id).get_display_info() + store = _app.get_haketilo_state().mapping_store() + mapping_ref = store.get(str(mapping_id)) + version_display_infos = mapping_ref.get_version_display_infos() + + active_list = [info for info in version_display_infos if info.is_active] + + active_version_ref = None if active_list == [] else active_list[0].ref + active_version_info = None if active_list == [] else active_list[0].info + + display_info = st.MappingDisplayInfo( + ref = mapping_ref, + identifier = version_display_infos[0].info.identifier, + enabled = version_display_infos[0].mapping_enabled, + active_version_ref = active_version_ref, + active_version_info = active_version_info + ) html = flask.render_template( 'packages__show_single.html.jinja', + display_info = display_info, + version_display_infos = version_display_infos + ) + return flask.make_response(html, 200) + except st.MissingItemError: + flask.abort(404) + +@bp.route('/packages/viewversion/') +def show_package_version(mapping_version_id: str) -> werkzeug.Response: + try: + store = _app.get_haketilo_state().mapping_version_store() + display_info = store.get(mapping_version_id).get_display_info() + + html = flask.render_template( + 'packages__show_single_version.html.jinja', display_info = display_info ) return flask.make_response(html, 200) diff --git a/src/hydrilla/proxy/web_ui/root.py b/src/hydrilla/proxy/web_ui/root.py index 0f42981..67cf6ba 100644 --- a/src/hydrilla/proxy/web_ui/root.py +++ b/src/hydrilla/proxy/web_ui/root.py @@ -74,6 +74,8 @@ class WebUIAppImpl(_app.WebUIApp): ] } + self.jinja_env.globals['versions'] = versions + self.before_request(authenticate_by_referrer) for blueprint in [repos.bp, packages.bp]: diff --git a/src/hydrilla/proxy/web_ui/templates/packages.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages.html.jinja index bcb8dea..bac774e 100644 --- a/src/hydrilla/proxy/web_ui/templates/packages.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/packages.html.jinja @@ -30,23 +30,18 @@ in a proprietary work, I am not going to enforce this in court.

{{ _('web_ui.packages.heading') }}

    {% for info in display_infos %} - {% if info.info.repo == '' -%} - {%- set entry_classes = 'package-entry-local' -%} - {%- else -%} - {%- set entry_classes = '' -%} - {%- endif -%}
  • - {{ info.info.long_name }} + {{ info.identifier }}
    -
    - {{ info.info.versioned_identifier }} - {% if info.info.repo != '' %} + {% if info.active_version_ref is not none %} +
    + {{ versions.version_string(info.active_version_info.version) }} @ - {{ info.info.repo }} - {% endif %} -
    + {{ info.active_version_info.repo }} +
    + {% endif %}
  • {% endfor %} diff --git a/src/hydrilla/proxy/web_ui/templates/packages__show_single.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages__show_single.html.jinja index eb526c4..17f958c 100644 --- a/src/hydrilla/proxy/web_ui/templates/packages__show_single.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/packages__show_single.html.jinja @@ -23,15 +23,42 @@ in a proprietary work, I am not going to enforce this in court. {% block title %} {{ _('web_ui.packages.single.title') }} {% endblock %} {% block style %} {{ super() }} + + {% include 'include/item_list_style.css.jinja' %} {% endblock %} {% block main %}

    {{ _('web_ui.packages.single.heading.name_{}') - .format(display_info.info.long_name) + .format(display_info.identifier) }}

    -
    - {{ display_info.info.versioned_identifier }} -
    + TODO: add more info... +

    {{ _('web_ui.packages.single.version_list_heading') }}

    + {% endblock %} 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 new file mode 100644 index 0000000..beedd43 --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/packages__show_single_version.html.jinja @@ -0,0 +1,38 @@ +{# +Spdx-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + + +Proxy web UI package show page. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior + +Dual licensed under +* GNU General Public License v3.0 or later and +* Creative Commons Attribution Share Alike 4.0 International. + +You can choose to use either of these licenses or both. + + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use this code +in a proprietary work, I am not going to enforce this in court. +#} +{% extends "base.html.jinja" %} +{% block title %} {{ _('web_ui.packages.single_version.title') }} {% endblock %} +{% block style %} + {{ super() }} +{% endblock %} +{% block main %} +

    + {{ + _('web_ui.packages.single_version.heading.name_{}') + .format(display_info.info.long_name) + }} +

    +
    + {{ display_info.info.versioned_identifier }} +
    + TODO: add more info... +{% endblock %} -- cgit v1.2.3