From d54a95e0f9c689f2bbaaea90a3a16a855a408823 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Wed, 17 Aug 2022 13:50:34 +0200 Subject: allow loading packages from zip files through web UI and listing installed mappings --- src/hydrilla/proxy/state_impl/concrete_state.py | 402 +++--------------------- src/hydrilla/proxy/state_impl/load_packages.py | 344 ++++++++++++++++++++ src/hydrilla/proxy/state_impl/mappings.py | 133 ++++++++ 3 files changed, 513 insertions(+), 366 deletions(-) create mode 100644 src/hydrilla/proxy/state_impl/load_packages.py create mode 100644 src/hydrilla/proxy/state_impl/mappings.py (limited to 'src/hydrilla/proxy/state_impl') diff --git a/src/hydrilla/proxy/state_impl/concrete_state.py b/src/hydrilla/proxy/state_impl/concrete_state.py index bb14734..b2b1033 100644 --- a/src/hydrilla/proxy/state_impl/concrete_state.py +++ b/src/hydrilla/proxy/state_impl/concrete_state.py @@ -46,12 +46,13 @@ from ...exceptions import HaketiloException from ...translations import smart_gettext as _ from ... import pattern_tree from ... import url_patterns -from ... import versions from ... import item_infos from ..simple_dependency_satisfying import compute_payloads, ComputedPayload from .. import state as st from .. import policies from . import base +from . import mappings +from .load_packages import load_packages here = Path(__file__).resolve().parent @@ -79,21 +80,6 @@ class ConcreteRepoIterationRef(st.RepoIterationRef): pass -@dc.dataclass(frozen=True, unsafe_hash=True) -class ConcreteMappingRef(st.MappingRef): - def disable(self, state: st.HaketiloState) -> None: - raise NotImplementedError() - - def forget_enabled(self, state: st.HaketiloState) -> None: - raise NotImplementedError() - - -@dc.dataclass(frozen=True, unsafe_hash=True) -class ConcreteMappingVersionRef(st.MappingVersionRef): - def enable(self, state: st.HaketiloState) -> None: - raise NotImplementedError() - - @dc.dataclass(frozen=True, unsafe_hash=True) class ConcreteResourceRef(st.ResourceRef): pass @@ -106,15 +92,20 @@ class ConcreteResourceVersionRef(st.ResourceVersionRef): @dc.dataclass(frozen=True, unsafe_hash=True) class ConcretePayloadRef(st.PayloadRef): - def get_data(self, state: st.HaketiloState) -> st.PayloadData: - return t.cast(ConcreteHaketiloState, state).payloads_data[self] + state: base.HaketiloStateWithFields = dc.field(hash=False, compare=False) - def get_mapping(self, state: st.HaketiloState) -> st.MappingVersionRef: - return 'to implement' + def get_data(self) -> st.PayloadData: + try: + return self.state.payloads_data[self] + except KeyError: + raise st.MissingItemError() + + def get_mapping(self) -> st.MappingVersionRef: + raise NotImplementedError() - def get_script_paths(self, state: st.HaketiloState) \ + def get_script_paths(self) \ -> t.Iterable[t.Sequence[str]]: - with t.cast(ConcreteHaketiloState, state).cursor() as cursor: + with self.state.cursor() as cursor: cursor.execute( ''' SELECT @@ -153,7 +144,7 @@ class ConcretePayloadRef(st.PayloadRef): return paths - def get_file_data(self, state: st.HaketiloState, path: t.Sequence[str]) \ + def get_file_data(self, path: t.Sequence[str]) \ -> t.Optional[st.FileData]: if len(path) == 0: raise st.MissingItemError() @@ -162,7 +153,7 @@ class ConcretePayloadRef(st.PayloadRef): file_name = '/'.join(file_name_segments) - with t.cast(ConcreteHaketiloState, state).cursor() as cursor: + with self.state.cursor() as cursor: cursor.execute( ''' SELECT @@ -197,61 +188,6 @@ class ConcretePayloadRef(st.PayloadRef): return st.FileData(type=mime_type, name=file_name, contents=data) -# @dc.dataclass(frozen=True, unsafe_hash=True) -# class ConcretePayloadRef(st.PayloadRef): -# computed_payload: ComputedPayload = dc.field(hash=False, compare=False) - -# def get_data(self, state: st.HaketiloState) -> st.PayloadData: -# return t.cast(ConcreteHaketiloState, state).payloads_data[self.id] - -# def get_mapping(self, state: st.HaketiloState) -> st.MappingVersionRef: -# return 'to implement' - -# def get_script_paths(self, state: st.HaketiloState) \ -# -> t.Iterator[t.Sequence[str]]: -# for resource_info in self.computed_payload.resources: -# for file_spec in resource_info.scripts: -# yield (resource_info.identifier, *file_spec.name.split('/')) - -# def get_file_data(self, state: st.HaketiloState, path: t.Sequence[str]) \ -# -> t.Optional[st.FileData]: -# if len(path) == 0: -# raise st.MissingItemError() - -# resource_identifier, *file_name_segments = path - -# file_name = '/'.join(file_name_segments) - -# script_sha256 = '' - -# matched_resource_info = False - -# for resource_info in self.computed_payload.resources: -# if resource_info.identifier == resource_identifier: -# matched_resource_info = True - -# for script_spec in resource_info.scripts: -# if script_spec.name == file_name: -# script_sha256 = script_spec.sha256 - -# break - -# if not matched_resource_info: -# raise st.MissingItemError(resource_identifier) - -# if script_sha256 == '': -# return None - -# store_dir_path = t.cast(ConcreteHaketiloState, state).store_dir -# files_dir_path = store_dir_path / 'temporary_malcontent' / 'file' -# file_path = files_dir_path / 'sha256' / script_sha256 - -# return st.FileData( -# type = 'application/javascript', -# name = file_name, -# contents = file_path.read_bytes() -# ) - def register_payload( policy_tree: base.PolicyTree, pattern: url_patterns.ParsedPattern, @@ -278,205 +214,12 @@ def register_payload( return policy_tree -DataById = t.Mapping[str, st.PayloadData] - AnyInfoVar = t.TypeVar( 'AnyInfoVar', item_infos.ResourceInfo, item_infos.MappingInfo ) -def read_items(malcontent_path: Path, item_class: t.Type[AnyInfoVar]) \ - -> t.Iterator[tuple[AnyInfoVar, str]]: - item_type_path = malcontent_path / item_class.type_name - if not item_type_path.is_dir(): - return - - for item_path in item_type_path.iterdir(): - if not item_path.is_dir(): - continue - - for item_version_path in item_path.iterdir(): - definition = item_version_path.read_text() - item_info = item_class.load(io.StringIO(definition)) - - assert item_info.identifier == item_path.name - assert versions.version_string(item_info.version) == \ - item_version_path.name - - yield item_info, definition - -def get_or_make_repo_iteration(cursor: sqlite3.Cursor, repo_name: str) -> int: - cursor.execute( - ''' - INSERT OR IGNORE INTO repos(name, url, deleted, next_iteration) - VALUES(?, '', TRUE, 2); - ''', - (repo_name,) - ) - - cursor.execute( - ''' - SELECT - repo_id, next_iteration - 1 - FROM - repos - WHERE - name = ?; - ''', - (repo_name,) - ) - - (repo_id, last_iteration), = cursor.fetchall() - - cursor.execute( - ''' - INSERT OR IGNORE INTO repo_iterations(repo_id, iteration) - VALUES(?, ?); - ''', - (repo_id, last_iteration) - ) - - cursor.execute( - ''' - SELECT - repo_iteration_id - FROM - repo_iterations - WHERE - repo_id = ? AND iteration = ?; - ''', - (repo_id, last_iteration) - ) - - (repo_iteration_id,), = cursor.fetchall() - - return repo_iteration_id - -def get_or_make_item(cursor: sqlite3.Cursor, type: str, identifier: str) -> int: - type_letter = {'resource': 'R', 'mapping': 'M'}[type] - - cursor.execute( - ''' - INSERT OR IGNORE INTO items(type, identifier) - VALUES(?, ?); - ''', - (type_letter, identifier) - ) - - cursor.execute( - ''' - SELECT - item_id - FROM - items - WHERE - type = ? AND identifier = ?; - ''', - (type_letter, identifier) - ) - - (item_id,), = cursor.fetchall() - - return item_id - -def get_or_make_item_version( - cursor: sqlite3.Cursor, - item_id: int, - repo_iteration_id: int, - version: versions.VerTuple, - definition: str -) -> int: - ver_str = versions.version_string(version) - - cursor.execute( - ''' - INSERT OR IGNORE INTO item_versions( - item_id, - version, - repo_iteration_id, - definition - ) - VALUES(?, ?, ?, ?); - ''', - (item_id, ver_str, repo_iteration_id, definition) - ) - - cursor.execute( - ''' - SELECT - item_version_id - FROM - item_versions - WHERE - item_id = ? AND version = ? AND repo_iteration_id = ?; - ''', - (item_id, ver_str, repo_iteration_id) - ) - - (item_version_id,), = cursor.fetchall() - - return item_version_id - -def make_mapping_status(cursor: sqlite3.Cursor, item_id: int) -> None: - cursor.execute( - ''' - INSERT OR IGNORE INTO mapping_statuses(item_id, enabled) - VALUES(?, 'N'); - ''', - (item_id,) - ) - -def get_or_make_file(cursor: sqlite3.Cursor, sha256: str, file_bytes: bytes) \ - -> int: - cursor.execute( - ''' - INSERT OR IGNORE INTO files(sha256, data) - VALUES(?, ?) - ''', - (sha256, file_bytes) - ) - - cursor.execute( - ''' - SELECT - file_id - FROM - files - WHERE - sha256 = ?; - ''', - (sha256,) - ) - - (file_id,), = cursor.fetchall() - - return file_id - -def make_file_use( - cursor: sqlite3.Cursor, - item_version_id: int, - file_id: int, - name: str, - type: str, - mime_type: str, - idx: int -) -> None: - cursor.execute( - ''' - INSERT OR IGNORE INTO file_uses( - item_version_id, - file_id, - name, - type, - mime_type, - idx - ) - VALUES(?, ?, ?, ?, ?, ?); - ''', - (item_version_id, file_id, name, type, mime_type, idx) - ) - def get_infos_of_type(cursor: sqlite3.Cursor, info_type: t.Type[AnyInfoVar],) \ -> t.Mapping[AnyInfoVar, int]: cursor.execute( @@ -508,10 +251,7 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): def __post_init__(self) -> None: self._prepare_database() - self._populate_database_with_stuff_from_temporary_malcontent_dir() - - with self.cursor(transaction=True) as cursor: - self.recompute_payloads(cursor) + self._rebuild_structures() def _prepare_database(self) -> None: """....""" @@ -546,7 +286,7 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): (db_haketilo_version,) = cursor.fetchone() if db_haketilo_version != '3.0b1': - raise HaketiloException(_('err.unknown_db_schema')) + raise HaketiloException(_('err.proxy.unknown_db_schema')) cursor.execute('PRAGMA FOREIGN_KEYS;') if cursor.fetchall() == []: @@ -556,88 +296,10 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): finally: cursor.close() - def _populate_database_with_stuff_from_temporary_malcontent_dir(self) \ - -> None: - malcontent_dir_path = self.store_dir / 'temporary_malcontent' - files_by_sha256_path = malcontent_dir_path / 'file' / 'sha256' - + def import_packages(self, malcontent_path: Path) -> None: with self.cursor(transaction=True) as cursor: - for info_type in [item_infos.ResourceInfo, item_infos.MappingInfo]: - info: item_infos.AnyInfo - for info, definition in read_items( - malcontent_dir_path, - info_type # type: ignore - ): - repo_iteration_id = get_or_make_repo_iteration( - cursor, - info.repo - ) - - item_id = get_or_make_item( - cursor, - info.type_name, - info.identifier - ) - - item_version_id = get_or_make_item_version( - cursor, - item_id, - repo_iteration_id, - info.version, - definition - ) - - if info_type is item_infos.MappingInfo: - make_mapping_status(cursor, item_id) - - file_ids_bytes = {} - - file_specifiers = [*info.source_copyright] - if isinstance(info, item_infos.ResourceInfo): - file_specifiers.extend(info.scripts) - - for file_spec in file_specifiers: - file_path = files_by_sha256_path / file_spec.sha256 - file_bytes = file_path.read_bytes() - - sha256 = hashlib.sha256(file_bytes).digest().hex() - assert sha256 == file_spec.sha256 - - file_id = get_or_make_file(cursor, sha256, file_bytes) - - file_ids_bytes[sha256] = (file_id, file_bytes) - - for idx, file_spec in enumerate(info.source_copyright): - file_id, file_bytes = file_ids_bytes[file_spec.sha256] - if file_bytes.isascii(): - mime = 'text/plain' - else: - mime = 'application/octet-stream' - - make_file_use( - cursor, - item_version_id = item_version_id, - file_id = file_id, - name = file_spec.name, - type = 'L', - mime_type = mime, - idx = idx - ) - - if isinstance(info, item_infos.MappingInfo): - continue - - for idx, file_spec in enumerate(info.scripts): - file_id, _ = file_ids_bytes[file_spec.sha256] - make_file_use( - cursor, - item_version_id = item_version_id, - file_id = file_id, - name = file_spec.name, - type = 'W', - mime_type = 'application/javascript', - idx = idx - ) + load_packages(self, cursor, malcontent_path) + self.recompute_payloads(cursor) def recompute_payloads(self, cursor: sqlite3.Cursor) -> None: assert self.connection.in_transaction @@ -700,7 +362,16 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): self._rebuild_structures(cursor) - def _rebuild_structures(self, cursor: sqlite3.Cursor) -> None: + def _rebuild_structures(self, cursor: t.Optional[sqlite3.Cursor] = None) \ + -> None: + """ + Recreation of data structures as done after every recomputation of + dependencies as well as at startup. + """ + if cursor is None: + with self.cursor() as new_cursor: + return self._rebuild_structures(new_cursor) + cursor.execute( ''' SELECT @@ -734,7 +405,7 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): enabled_status, identifier) = row - payload_ref = ConcretePayloadRef(str(payload_id_int)) + payload_ref = ConcretePayloadRef(str(payload_id_int), self) previous_data = self.payloads_data.get(payload_ref) if previous_data is not None: @@ -775,12 +446,11 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): def get_repo_iteration(self, repo_iteration_id: str) -> st.RepoIterationRef: return ConcreteRepoIterationRef(repo_iteration_id) - def get_mapping(self, mapping_id: str) -> st.MappingRef: - return ConcreteMappingRef(mapping_id) + def mapping_store(self) -> st.MappingStore: + raise NotImplementedError() - def get_mapping_version(self, mapping_version_id: str) \ - -> st.MappingVersionRef: - return ConcreteMappingVersionRef(mapping_version_id) + def mapping_version_store(self) -> st.MappingVersionStore: + return mappings.ConcreteMappingVersionStore(self) def get_resource(self, resource_id: str) -> st.ResourceRef: return ConcreteResourceRef(resource_id) @@ -790,7 +460,7 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): return ConcreteResourceVersionRef(resource_version_id) def get_payload(self, payload_id: str) -> st.PayloadRef: - return 'not implemented' + raise NotImplementedError() def add_repo(self, name: t.Optional[str], url: t.Optional[str]) \ -> st.RepoRef: diff --git a/src/hydrilla/proxy/state_impl/load_packages.py b/src/hydrilla/proxy/state_impl/load_packages.py new file mode 100644 index 0000000..6983c3e --- /dev/null +++ b/src/hydrilla/proxy/state_impl/load_packages.py @@ -0,0 +1,344 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +# Haketilo proxy data and configuration (import of packages from disk files). +# +# This file is part of Hydrilla&Haketilo. +# +# Copyright (C) 2022 Wojtek Kosior +# +# This program 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. +# +# This program 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 this program. If not, see . +# +# +# I, Wojtek Kosior, thereby promise not to sue for violation of this +# file's license. Although I request that you do not make use this code +# in a proprietary program, I am not going to enforce this in court. + +""" +.... +""" + +# Enable using with Python 3.7. +from __future__ import annotations + +import io +import hashlib +import dataclasses as dc +import typing as t + +from pathlib import Path + +import sqlite3 + +from ...exceptions import HaketiloException +from ...translations import smart_gettext as _ +from ... import versions +from ... import item_infos +from . import base + + +def get_or_make_repo_iteration(cursor: sqlite3.Cursor, repo_name: str) -> int: + cursor.execute( + ''' + SELECT + repo_id, next_iteration - 1 + FROM + repos + WHERE + name = ?; + ''', + (repo_name,) + ) + + (repo_id, last_iteration), = cursor.fetchall() + + cursor.execute( + ''' + INSERT OR IGNORE INTO repo_iterations(repo_id, iteration) + VALUES(?, ?); + ''', + (repo_id, last_iteration) + ) + + cursor.execute( + ''' + SELECT + repo_iteration_id + FROM + repo_iterations + WHERE + repo_id = ? AND iteration = ?; + ''', + (repo_id, last_iteration) + ) + + (repo_iteration_id,), = cursor.fetchall() + + return repo_iteration_id + +def get_or_make_item(cursor: sqlite3.Cursor, type: str, identifier: str) -> int: + type_letter = {'resource': 'R', 'mapping': 'M'}[type] + + cursor.execute( + ''' + INSERT OR IGNORE INTO items(type, identifier) + VALUES(?, ?); + ''', + (type_letter, identifier) + ) + + cursor.execute( + ''' + SELECT + item_id + FROM + items + WHERE + type = ? AND identifier = ?; + ''', + (type_letter, identifier) + ) + + (item_id,), = cursor.fetchall() + + return item_id + +def get_or_make_item_version( + cursor: sqlite3.Cursor, + item_id: int, + repo_iteration_id: int, + version: versions.VerTuple, + definition: str +) -> int: + ver_str = versions.version_string(version) + + cursor.execute( + ''' + INSERT OR IGNORE INTO item_versions( + item_id, + version, + repo_iteration_id, + definition + ) + VALUES(?, ?, ?, ?); + ''', + (item_id, ver_str, repo_iteration_id, definition) + ) + + cursor.execute( + ''' + SELECT + item_version_id + FROM + item_versions + WHERE + item_id = ? AND version = ? AND repo_iteration_id = ?; + ''', + (item_id, ver_str, repo_iteration_id) + ) + + (item_version_id,), = cursor.fetchall() + + return item_version_id + +def make_mapping_status(cursor: sqlite3.Cursor, item_id: int) -> None: + cursor.execute( + ''' + INSERT OR IGNORE INTO mapping_statuses(item_id, enabled, frozen) + VALUES(?, 'E', 'R'); + ''', + (item_id,) + ) + +def get_or_make_file(cursor: sqlite3.Cursor, sha256: str, file_bytes: bytes) \ + -> int: + cursor.execute( + ''' + INSERT OR IGNORE INTO files(sha256, data) + VALUES(?, ?) + ''', + (sha256, file_bytes) + ) + + cursor.execute( + ''' + SELECT + file_id + FROM + files + WHERE + sha256 = ?; + ''', + (sha256,) + ) + + (file_id,), = cursor.fetchall() + + return file_id + +def make_file_use( + cursor: sqlite3.Cursor, + item_version_id: int, + file_id: int, + name: str, + type: str, + mime_type: str, + idx: int +) -> None: + cursor.execute( + ''' + INSERT OR IGNORE INTO file_uses( + item_version_id, + file_id, + name, + type, + mime_type, + idx + ) + VALUES(?, ?, ?, ?, ?, ?); + ''', + (item_version_id, file_id, name, type, mime_type, idx) + ) + +@dc.dataclass(frozen=True) +class _FileInfo: + id: int + is_ascii: bool + +def _add_item( + cursor: sqlite3.Cursor, + files_by_sha256_path: Path, + info: item_infos.AnyInfo, + definition: str +) -> None: + repo_iteration_id = get_or_make_repo_iteration(cursor, '') + + item_id = get_or_make_item(cursor, info.type_name, info.identifier) + + item_version_id = get_or_make_item_version( + cursor, + item_id, + repo_iteration_id, + info.version, + definition + ) + + if isinstance(info, item_infos.MappingInfo): + make_mapping_status(cursor, item_id) + + file_infos = {} + + file_specifiers = [*info.source_copyright] + if isinstance(info, item_infos.ResourceInfo): + file_specifiers.extend(info.scripts) + + for file_spec in file_specifiers: + file_path = files_by_sha256_path / file_spec.sha256 + if not file_path.is_file(): + fmt = _('err.proxy.file_missing_{item_identifier}_{file_name}_{sha256}') + msg = fmt.format( + item_identifier = info.identifier, + file_name = file_spec.name, + sha256 = file_spec.sha256 + ) + raise HaketiloException(msg) + + file_bytes = file_path.read_bytes() + + sha256 = hashlib.sha256(file_bytes).digest().hex() + if sha256 != file_spec.sha256: + fmt = _('err.proxy.file_hash_mismatched_{item_identifier}_{file_name}_{expected_sha256}_{actual_sha256}') + msg = fmt.format( + item_identifier = info.identifier, + file_name = file_spec.name, + expected_sha256 = file_spec.sha256, + actual_sha256 = sha256 + ) + raise HaketiloException(msg) + + file_id = get_or_make_file(cursor, sha256, file_bytes) + + file_infos[sha256] = _FileInfo(file_id, file_bytes.isascii()) + + for idx, file_spec in enumerate(info.source_copyright): + file_info = file_infos[file_spec.sha256] + if file_info.is_ascii: + mime = 'text/plain' + else: + mime = 'application/octet-stream' + + make_file_use( + cursor, + item_version_id = item_version_id, + file_id = file_info.id, + name = file_spec.name, + type = 'L', + mime_type = mime, + idx = idx + ) + + if isinstance(info, item_infos.MappingInfo): + return + + for idx, file_spec in enumerate(info.scripts): + file_info = file_infos[file_spec.sha256] + make_file_use( + cursor, + item_version_id = item_version_id, + file_id = file_info.id, + name = file_spec.name, + type = 'W', + mime_type = 'application/javascript', + idx = idx + ) + +AnyInfoVar = t.TypeVar( + 'AnyInfoVar', + item_infos.ResourceInfo, + item_infos.MappingInfo +) + +def _read_items(malcontent_path: Path, item_class: t.Type[AnyInfoVar]) \ + -> t.Iterator[tuple[AnyInfoVar, str]]: + item_type_path = malcontent_path / item_class.type_name + if not item_type_path.is_dir(): + return + + for item_path in item_type_path.iterdir(): + if not item_path.is_dir(): + continue + + for item_version_path in item_path.iterdir(): + definition = item_version_path.read_text() + item_info = item_class.load(io.StringIO(definition)) + + assert item_info.identifier == item_path.name + assert versions.version_string(item_info.version) == \ + item_version_path.name + + yield item_info, definition + +def load_packages( + state: base.HaketiloStateWithFields, + cursor: sqlite3.Cursor, + malcontent_path: Path +) -> None: + files_by_sha256_path = malcontent_path / 'file' / 'sha256' + + for info_type in [item_infos.ResourceInfo, item_infos.MappingInfo]: + info: item_infos.AnyInfo + for info, definition in _read_items( + malcontent_path, + info_type # type: ignore + ): + _add_item(cursor, files_by_sha256_path, info, definition) diff --git a/src/hydrilla/proxy/state_impl/mappings.py b/src/hydrilla/proxy/state_impl/mappings.py new file mode 100644 index 0000000..5e31814 --- /dev/null +++ b/src/hydrilla/proxy/state_impl/mappings.py @@ -0,0 +1,133 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +# Haketilo proxy data and configuration (MappingRef and MappingStore subtypes). +# +# This file is part of Hydrilla&Haketilo. +# +# Copyright (C) 2022 Wojtek Kosior +# +# This program 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. +# +# This program 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 this program. If not, see . +# +# +# I, Wojtek Kosior, thereby promise not to sue for violation of this +# file's license. Although I request that you do not make use this code +# in a proprietary program, I am not going to enforce this in court. + +""" +This module provides an interface to interact with mappings inside Haketilo. +""" + +# Enable using with Python 3.7. +from __future__ import annotations + +import io +import typing as t +import dataclasses as dc + +from ... import item_infos +from .. import state as st +from . import base + + +@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.MappingDisplayInfo: + with self.state.cursor() as cursor: + cursor.execute( + ''' + SELECT + enabled, + definition, + repo, + repo_iteration, + is_orphan + FROM + mapping_display_infos + WHERE + item_version_id = ?; + ''', + (self.id,) + ) + + rows = cursor.fetchall() + + if rows == []: + raise st.MissingItemError() + + (status_letter, definition, repo, repo_iteration, is_orphan), = rows + + item_info = item_infos.MappingInfo.load( + io.StringIO(definition), + repo, + repo_iteration + ) + + status = st.EnabledStatus(status_letter) + + return st.MappingDisplayInfo(self, item_info, status, is_orphan) + + +@dc.dataclass(frozen=True) +class ConcreteMappingVersionStore(st.MappingVersionStore): + state: base.HaketiloStateWithFields + + def get(self, id: str) -> st.MappingVersionRef: + return ConcreteMappingVersionRef(id, self.state) + + def get_display_infos(self) -> t.Iterable[st.MappingDisplayInfo]: + with self.state.cursor() as cursor: + cursor.execute( + ''' + SELECT + enabled, + item_version_id, + definition, + repo, + repo_iteration, + is_orphan + FROM + mapping_display_infos; + ''' + ) + + all_rows = cursor.fetchall() + + result = [] + + for row in all_rows: + (status_letter, item_version_id, definition, repo, repo_iteration, + is_orphan) = row + + ref = ConcreteMappingVersionRef(str(item_version_id), self.state) + + item_info = item_infos.MappingInfo.load( + io.StringIO(definition), + repo, + repo_iteration + ) + + status = st.EnabledStatus(status_letter) + + info = st.MappingDisplayInfo(ref, item_info, status, is_orphan) + result.append(info) + + return result -- cgit v1.2.3