From c415b42e76dd8a28f86be7e0ce1e30d0471b6bdd Mon Sep 17 00:00:00 2001 From: Alex Derr Date: Fri, 20 Dec 2024 14:18:28 -0500 Subject: [PATCH] Initial working Custom List Store + typos --- mlte/backend/core/state.py | 10 +- mlte/backend/main.py | 5 + mlte/custom_list/__init__.py | 0 mlte/custom_list/model.py | 29 +++++ mlte/store/catalog/underlying/fs.py | 6 +- mlte/store/custom_list/__init__.py | 0 mlte/store/custom_list/factory.py | 29 +++++ mlte/store/custom_list/store.py | 31 ++++++ mlte/store/custom_list/store_session.py | 63 +++++++++++ mlte/store/custom_list/underlying/__init__.py | 0 mlte/store/custom_list/underlying/fs.py | 103 ++++++++++++++++++ mlte/store/user/store.py | 2 +- mlte/store/user/store_session.py | 2 +- mlte/store/user/underlying/fs.py | 4 +- 14 files changed, 276 insertions(+), 8 deletions(-) create mode 100644 mlte/custom_list/__init__.py create mode 100644 mlte/custom_list/model.py create mode 100644 mlte/store/custom_list/__init__.py create mode 100644 mlte/store/custom_list/factory.py create mode 100644 mlte/store/custom_list/store.py create mode 100644 mlte/store/custom_list/store_session.py create mode 100644 mlte/store/custom_list/underlying/__init__.py create mode 100644 mlte/store/custom_list/underlying/fs.py diff --git a/mlte/backend/core/state.py b/mlte/backend/core/state.py index 41c592cea..fa2fadf1b 100644 --- a/mlte/backend/core/state.py +++ b/mlte/backend/core/state.py @@ -11,6 +11,7 @@ from mlte.store.catalog.catalog_group import CatalogStoreGroup from mlte.store.catalog.store import CatalogStore from mlte.store.user.store import UserStore +from mlte.store.custom_list.store import CustomListStore class State: @@ -30,6 +31,9 @@ def reset(self): self._catalog_stores: CatalogStoreGroup = CatalogStoreGroup() """The list of catalog store instances maintained by the state object.""" + self._custom_list_store: Optional[CustomListStore] = None + """The custom list store instance maintained by the state object.""" + self._jwt_secret_key: str = "" """Secret key used to sign authentication tokens.""" @@ -38,7 +42,7 @@ def set_artifact_store(self, store: ArtifactStore): self._artifact_store = store def set_user_store(self, store: UserStore): - """Set the globally-configured backend artifact store.""" + """Set the globally-configured backend user store.""" self._user_store = store def add_catalog_store( @@ -53,6 +57,10 @@ def add_catalog_store_from_uri( """Adds to the the globally-configured backend list of catalog stores.""" self._catalog_stores.add_catalog_from_uri(id, store_uri, overwite) + def set_custom_list_store(self, store: CustomListStore): + """Set the globally-configured backend custom list store.""" + self._custom_list_store = store + def set_token_key(self, token_key: str): """Sets the globally used token secret key.""" self._jwt_secret_key = token_key diff --git a/mlte/backend/main.py b/mlte/backend/main.py index a3313a7aa..b66147cce 100644 --- a/mlte/backend/main.py +++ b/mlte/backend/main.py @@ -19,6 +19,7 @@ from mlte.store.base import StoreType, StoreURI from mlte.store.catalog.sample_catalog import SampleCatalog from mlte.store.user import factory as user_store_factory +from mlte.store.custom_list import factory as custom_list_store_factory # Application exit codes EXIT_SUCCESS = 0 @@ -95,6 +96,10 @@ def run( f"Adding catalog with id '{id}' and URI of type: {StoreURI.from_string(uri).type}" ) state.add_catalog_store_from_uri(uri, id) + + # Initialize the backing custom list store instance. Assume same store as artifact one for now. + custom_list_store = custom_list_store_factory.create_custom_list_store(store_uri) + state.set_custom_list_store(custom_list_store) # Set the token signing key. state.set_token_key(jwt_secret) diff --git a/mlte/custom_list/__init__.py b/mlte/custom_list/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mlte/custom_list/model.py b/mlte/custom_list/model.py new file mode 100644 index 000000000..d1c4d22e9 --- /dev/null +++ b/mlte/custom_list/model.py @@ -0,0 +1,29 @@ +""" +mlte/custom_list/model.py + +Model implementation for a custom list. +""" +from __future__ import annotations + +from typing import List + +from mlte.model import BaseModel + +class CustomList(BaseModel): + """A model class representing a custom list.""" + + name: str + """An name to uniquely identify the list.""" + + entries: List[CustomListEntry] = [] + """A list of entries in the list.""" + + +class CustomListEntry(BaseModel): + """A model class representing a custom list entry.""" + + namme: str + """A name to uniquely identify the entry.""" + + description: str + """A description of the the entry.""" diff --git a/mlte/store/catalog/underlying/fs.py b/mlte/store/catalog/underlying/fs.py index 811aad5ef..3d733df36 100644 --- a/mlte/store/catalog/underlying/fs.py +++ b/mlte/store/catalog/underlying/fs.py @@ -19,7 +19,7 @@ from mlte.store.common.fs_storage import FileSystemStorage # ----------------------------------------------------------------------------- -# LocalFileSystemStore +# FileSystemCatalogStore # ----------------------------------------------------------------------------- @@ -27,7 +27,7 @@ class FileSystemCatalogStore(CatalogStore): """A local file system implementation of the MLTE catalog store.""" BASE_CATALOGS_FOLDER = "catalogs" - """Base fodler to store catalog entries in.""" + """Base folder to store catalog entries in.""" DEFAULT_CATALOG_FOLDER = "catalog" """A default name for a catalog folder.""" @@ -56,7 +56,7 @@ def session(self) -> FileSystemCatalogStoreSession: # ----------------------------------------------------------------------------- -# LocalFileSystemStoreSession +# FileSystemCatalogStoreSession # ----------------------------------------------------------------------------- diff --git a/mlte/store/custom_list/__init__.py b/mlte/store/custom_list/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mlte/store/custom_list/factory.py b/mlte/store/custom_list/factory.py new file mode 100644 index 000000000..6e89b1dd1 --- /dev/null +++ b/mlte/store/custom_list/factory.py @@ -0,0 +1,29 @@ +""" +mlte/store/custom_list/factory.py + +Top-level functions for custom list store creation. +""" + +from mlte.store.base import StoreType, StoreURI +from mlte.store.custom_list.store import CustomListStore +from mlte.store.custom_list.underlying.fs import FileSystemCustomListStore +# from mlte.store.custom_list.underlying.memory import InMemoryCustomListStore +# from mlte.store.custom_list.underlying.rdbs.store import RelationalDBCustomListStore + +def create_custom_list_store(uri: str) -> CustomListStore: + """ + Create a MLTE custom list store instance. + :param uri: The URI for the store instance + :return: The store instance + """ + parsed_uri = StoreURI.from_string(uri) + if parsed_uri.type == StoreType.LOCAL_MEMORY: + return InMemoryCustomListStore(parsed_uri) + if parsed_uri.type == StoreType.RELATIONAL_DB: + return RelationalDBCustomListStore(parsed_uri) + if parsed_uri.type == StoreType.LOCAL_FILESYSTEM: + return FileSystemCustomListStore(parsed_uri) + else: + raise Exception( + f"Store can't be created, unknown or unsupported URI prefix received for uri {parsed_uri}" + ) \ No newline at end of file diff --git a/mlte/store/custom_list/store.py b/mlte/store/custom_list/store.py new file mode 100644 index 000000000..10a62f542 --- /dev/null +++ b/mlte/store/custom_list/store.py @@ -0,0 +1,31 @@ +""" +mlte/store/custom_list/store.py + +MLTE custom list store implementation +""" + +from __future__ import annotations + +from mlte.store.base import Store, StoreSession + +# ----------------------------------------------------------------------------- +# CustomListStore +# ----------------------------------------------------------------------------- + + +class CustomListStore(Store): + """ + An abstract custom list store + """ + + def __init__(self, uri: StoreURI): + """Base constructor""" + super().__init__(uri=uri) + """Store uri.""" + + def session(self) -> CustomListStoreSession: + """ + Return a session handle for the store instance. + :return: The session handle + """ + raise NotImplementedError("Cannot get handle to abstract Store.") diff --git a/mlte/store/custom_list/store_session.py b/mlte/store/custom_list/store_session.py new file mode 100644 index 000000000..bc17c7b86 --- /dev/null +++ b/mlte/store/custom_list/store_session.py @@ -0,0 +1,63 @@ +""" +mlte/store/custom_list/store_session.py + +MLTE custom list store interface implementation +""" +from __future__ import annotations + +from typing import List + +from mlte.store.base import ResourceMapper, StoreSession +from mlte.custom_list.model import CustomList, CustomListEntry + +# ----------------------------------------------------------------------------- +# CustomListStoreSession +# ----------------------------------------------------------------------------- + + +class CustomListStoreSession(StoreSession): + """The base class for all implementations of the MLTE custom list store session.""" + + custom_list_mapper: CustomListMapper + """Mapper for the custom list resource.""" + + custom_list_entry_mapper: CustomListEntryMapper + """Mapper for the custom list entry resource.""" + + +class CustomListMapper(ResourceMapper): + """An interface for mapping CRUD actions to custom lists.""" + + def create(self, new_custom_list: CustomList) -> CustomList: + raise NotImplementedError(self.NOT_IMPLEMENTED_ERROR_MSG) + + def edit(self, updated_custom_list: CustomList) -> CustomList: + raise NotImplementedError(self.NOT_IMPLEMENTED_ERROR_MSG) + + def read(self, custom_list_name: str) -> CustomList: + raise NotImplementedError(self.NOT_IMPLEMENTED_ERROR_MSG) + + def list(self) -> List[str]: + raise NotImplementedError(self.NOT_IMPLEMENTED_ERROR_MSG) + + def delete(self, custom_list_name: str) -> CustomList: + raise NotImplementedError(self.NOT_IMPLEMENTED_ERROR_MSG) + + +class CustomListEntryMapper(ResourceMapper): + """An interface for mapping CRUD actions to custom list entries.""" + + def create(self, new_custom_list_entry: CustomListEntry) -> CustomListEntry: + raise NotImplementedError(self.NOT_IMPLEMENTED_ERROR_MSG) + + def edit(self, updated_custom_list_entry: CustomListEntry) -> CustomListEntry: + raise NotImplementedError(self.NOT_IMPLEMENTED_ERROR_MSG) + + def read(self, custom_list_entry_name: str) -> CustomListEntry: + raise NotImplementedError(self.NOT_IMPLEMENTED_ERROR_MSG) + + def list(self) -> List[str]: + raise NotImplementedError(self.NOT_IMPLEMENTED_ERROR_MSG) + + def delete(self, custom_list_entry_name: str) -> CustomListEntry: + raise NotImplementedError(self.NOT_IMPLEMENTED_ERROR_MSG) \ No newline at end of file diff --git a/mlte/store/custom_list/underlying/__init__.py b/mlte/store/custom_list/underlying/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mlte/store/custom_list/underlying/fs.py b/mlte/store/custom_list/underlying/fs.py new file mode 100644 index 000000000..50b51afa3 --- /dev/null +++ b/mlte/store/custom_list/underlying/fs.py @@ -0,0 +1,103 @@ +""" +mlte/store/custom_list/underlying/fs.py + +Implementation of local file system custom list store. +""" +from __future__ import annotations + +from pathlib import Path + +from mlte.custom_list.model import CustomList, CustomListEntry +from mlte.store.base import StoreURI +from mlte.store.custom_list.store import CustomListStore +from mlte.store.custom_list.store_session import CustomListStoreSession, CustomListMapper, CustomListEntryMapper +from mlte.store.common.fs_storage import FileSystemStorage + +# ----------------------------------------------------------------------------- +# FileSystemCustomListStore +# ----------------------------------------------------------------------------- + + +class FileSystemCustomListStore(CustomListStore): + """A local file system implementation of the MLTE custom list store.""" + + BASE_CUSTOM_LIST_FOLDER = "custom_lists" + """Base folder to store custom lists in.""" + + def __init__(self, uri: StoreURI) -> None: + self.storage = FileSystemStorage( + uri=uri, sub_folder=self.BASE_CUSTOM_LIST_FOLDER + ) + """Underlying storage.""" + + # Initialize defaults. + super().__init__(uri=uri) + + def session(self) -> FileSystemCustomListStoreSession: + """ + Return a session handle for the store instance. + :return: The session handle + """ + return FileSystemCustomListStoreSession(storage=self.storage) + + +# ----------------------------------------------------------------------------- +# FileSystemCustomLilstStoreSession +# ----------------------------------------------------------------------------- + + +class FileSystemCustomListStoreSession(CustomListStoreSession): + """A local file-system implementation of the MLTE custom list store.""" + + def __init__(self, storage: FileSystemStorage) -> None: + self.custom_list_mapper = FileSystemCustomListMapper(storage) + """The mapper to custom list CRUD.""" + + self.custom_list_entry_mapper = FileSystemCustomListEntryMapper(storage) + """The mapper to custom list entry CRUD.""" + + def close(self) -> None: + """Close the session.""" + # Closing a local FS session is a no-op. + pass + + +# ----------------------------------------------------------------------------- +# FileSystemCustomListMappper +# ----------------------------------------------------------------------------- + + +class FileSystemCustomListMapper(CustomListMapper): + """FS mapper for the custom list resource.""" + + CUSTOM_LIST_FOLDER = "custom_lists" + """Subfolder for custom lists.""" + + def __init__( + self, storage: FileSystemStorage + ) -> None: + self.storage = storage.clone() + """A reference to underlying storage.""" + + self.storage.set_base_path( + Path(FileSystemCustomListStore.BASE_CUSTOM_LIST_FOLDER, self.CUSTOM_LIST_FOLDER) + ) + """Set the subfodler for this resource.""" + + def create(self, custom_list: CustomList) -> CustomList: + self.storage.ensure_resource_does_not_exist(custom_list.name) + return self._write_entry(custom_list) + + def _write_entry(self, custom_list: CustomList) -> CustomList: + """Writes a entry to storage.""" + self.storage.write_resource(custom_list.name, custom_list.model_dump()) + return self._read_entry(custom_list.name) + + +# ----------------------------------------------------------------------------- +# FileSystemCustomListEntryMappper +# ----------------------------------------------------------------------------- + + +class FileSystemCustomListEntryMapper(CustomListEntryMapper): + pass \ No newline at end of file diff --git a/mlte/store/user/store.py b/mlte/store/user/store.py index 3983a2034..9d7eb92ba 100644 --- a/mlte/store/user/store.py +++ b/mlte/store/user/store.py @@ -37,7 +37,7 @@ class UserStore(Store): def __init__(self, uri: StoreURI, add_default_data: bool = True): """Base constructor.""" super().__init__(uri=uri) - "Store uri." + """Store uri.""" # Sets up default user and permissions. if add_default_data: diff --git a/mlte/store/user/store_session.py b/mlte/store/user/store_session.py index 9a0936726..67d84f3d9 100644 --- a/mlte/store/user/store_session.py +++ b/mlte/store/user/store_session.py @@ -37,7 +37,7 @@ def __enter__(self) -> UserStoreSession: class UserMapper(ResourceMapper): - """A interface for mapping CRUD actions to store users.""" + """An interface for mapping CRUD actions to store users.""" def create(self, new_user: UserWithPassword) -> User: raise NotImplementedError(self.NOT_IMPLEMENTED_ERROR_MSG) diff --git a/mlte/store/user/underlying/fs.py b/mlte/store/user/underlying/fs.py index 8e710bf53..68b0bd09e 100644 --- a/mlte/store/user/underlying/fs.py +++ b/mlte/store/user/underlying/fs.py @@ -96,12 +96,12 @@ def __init__( """A reference to underlying storage.""" self.group_mapper = group_mapper - """Refernce to group mapper, to get updated groups when needed.""" + """Reference to group mapper, to get updated groups when needed.""" self.storage.set_base_path( Path(FileSystemUserStore.BASE_USERS_FOLDER, self.USERS_FOLDER) ) - """Set the subfodler for this resrouce.""" + """Set the subfodler for this resource.""" def create(self, user: UserWithPassword) -> User: self.storage.ensure_resource_does_not_exist(user.username)