From 6b021bedcb36b1fcc49935660d7be1126db31856 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Fri, 25 Oct 2024 10:22:54 +0200 Subject: [PATCH 01/52] README update --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c0d66284..9021e961 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,17 @@ [![PyPI-Server](https://img.shields.io/pypi/v/osw.svg)](https://pypi.org/project/osw/) [![DOI](https://zenodo.org/badge/458130867.svg)](https://zenodo.org/badge/latestdoi/458130867) -[![Coveralls](https://img.shields.io/coveralls/github/OpenSemanticLab/osw-python/main.svg)](https://coveralls.io/r//osw) +[![Coveralls](https://img.shields.io/coveralls/github/OpenSemanticLab/osw-python/main.svg)](https://coveralls.io/r/OpenSemanticLab/osw) +[![docs](xx.xx)](https://opensemanticlab.github.io/osw-python/) +![license](https://img.shields.io/github/license/OpenSemanticLab/osw-python.svg) +[![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://pydantic.dev) [![Project generated with PyScaffold](https://img.shields.io/badge/-PyScaffold-005CA0?logo=pyscaffold)](https://pyscaffold.org/) # osw Python toolset for data processing, queries, wikicode generation and page manipulation within OpenSemanticLab. -General features for object oriented interaction with knowledge graphs are planned to be moved to a standalone package: [oold-python](https://github.com/OpenSemanticWorld/oold-python) + +General features for object-oriented interaction with knowledge graphs are planned to be moved to a standalone package: +[oold-python](https://github.com/OpenSemanticWorld/oold-python) ## Installation ``` From c7d7c2ae0106d8e104723a1bd5f51f841cdd581f Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Fri, 25 Oct 2024 10:24:46 +0200 Subject: [PATCH 02/52] import_with_fallback implented and some reformatting --- src/osw/core.py | 17 +++++++++-------- src/osw/express.py | 25 +++++++++++++++++++++---- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/osw/core.py b/src/osw/core.py index 21b5cc1e..82593e47 100644 --- a/src/osw/core.py +++ b/src/osw/core.py @@ -840,8 +840,10 @@ def set_content(content_to_set: dict) -> None: page.set_slot_content(slot_, content_to_set[slot_]) # Create a variable to hold the new content - new_content = { # required for json parsing and header rendering - "header": "{{#invoke:Entity|header}}", # required for footer rendering + new_content = { + # required for json parsing and header rendering + "header": "{{#invoke:Entity|header}}", + # required for footer rendering "footer": "{{#invoke:Entity|footer}}", } # Take the shortcut if @@ -880,9 +882,8 @@ def set_content(content_to_set: dict) -> None: remote_content = {} # Get the remote content for slot in ["jsondata", "header", "footer"]: # SLOTS: - remote_content[slot] = page.get_slot_content( - slot - ) # Todo: remote content does not contain properties that are not set + remote_content[slot] = page.get_slot_content(slot) + # Todo: remote content does not contain properties that are not set if remote_content["header"]: # not None or {} or "" new_content["header"] = remote_content["header"] if remote_content["footer"]: @@ -1076,13 +1077,13 @@ def store_entity_( meta_category_template, page.get_slot_content("jsondata"), { - "_page_title": entity_title, # legacy + "_page_title": entity_title, # Legacy "_current_subject_": entity_title, }, ) schema = json.loads(schema_str) - # put generated schema in definitions section - # currently only enabled for Characteristics + # Put generated schema in definitions section, + # currently only enabled for Characteristics if hasattr(model, "CharacteristicType") and isinstance( entity_, model.CharacteristicType ): diff --git a/src/osw/express.py b/src/osw/express.py index d3e90349..ed0aaab4 100644 --- a/src/osw/express.py +++ b/src/osw/express.py @@ -350,7 +350,10 @@ class DataModel(OswBaseModel): def import_with_fallback( - to_import: List[DataModel], dependencies: Dict[str, str] = None, domain: str = None + to_import: Union[List[DataModel], Dict[str, str]], + module: str = None, + dependencies: Dict[str, str] = None, + domain: str = None, ): """Imports data models with a fallback to fetch the dependencies from an OSL instance if the data models are not available in the local osw.model.entity module. @@ -359,6 +362,9 @@ def import_with_fallback( ---------- to_import List of DataModel objects to import. + module + (Optional) The module to import the data models from. Used only if to_import + is of type List[Dict]. Defaults to 'osw.model.entity' if not specified. dependencies A dictionary with the keys being the names of the dependencies and the values being the full page name of the dependencies. @@ -370,6 +376,18 @@ def import_with_fallback( ------- """ + if isinstance(to_import, dict): + # Assume all DataModels are part of osw.model.entity + if module is None: + module = "osw.model.entity" + to_import = [ + DataModel( + module=module, + class_name=key, + osw_fpt=value, + ) + for key, value in to_import.items() + ] try: for ti in to_import: # Raises AttributeError if the target could not be found @@ -803,9 +821,8 @@ def __init__( # Upload to the target OSW instance wfc.put_from(data.get("source_file_controller"), **data) data["url_or_title"] = wfc.url - super().__init__( - **{**wfc.dict(), **data} - ) # Don't open the local (uploaded) file + super().__init__(**{**wfc.dict(), **data}) + # Don't open the local (uploaded) file def osw_upload_file( From 81b3d660024747687c6999ccd5728a5bcd05854c Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Wed, 6 Nov 2024 17:15:16 +0100 Subject: [PATCH 03/52] Doc string reformatting --- src/osw/utils/wiki.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/osw/utils/wiki.py b/src/osw/utils/wiki.py index 7d137d8b..4c69241d 100644 --- a/src/osw/utils/wiki.py +++ b/src/osw/utils/wiki.py @@ -12,11 +12,11 @@ def get_osw_id(uuid: UUID) -> str: Parameters ---------- uuid - uuid object, e. g. UUID("2ea5b605-c91f-4e5a-9559-3dff79fdd4a5") + An UUID object, e.g., UUID("2ea5b605-c91f-4e5a-9559-3dff79fdd4a5") Returns ------- - OSW-ID string, e. g. OSW2ea5b605c91f4e5a95593dff79fdd4a5 + OSW-ID string, e.g., OSW2ea5b605c91f4e5a95593dff79fdd4a5 """ return "OSW" + str(uuid).replace("-", "") @@ -27,11 +27,11 @@ def get_uuid(osw_id) -> UUID: Parameters ---------- osw_id - OSW-ID string, e. g. OSW2ea5b605c91f4e5a95593dff79fdd4a5 + OSW-ID string, e.g., OSW2ea5b605c91f4e5a95593dff79fdd4a5 Returns ------- - uuid object, e. g. UUID("2ea5b605-c91f-4e5a-9559-3dff79fdd4a5") + uuid object, e.g., UUID("2ea5b605-c91f-4e5a-9559-3dff79fdd4a5") """ return UUID(osw_id.replace("OSW", "")) From d7824fc82e06378208cd9b4b137846fe8617cdea Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Wed, 6 Nov 2024 17:17:43 +0100 Subject: [PATCH 04/52] Reformatting --- src/osw/controller/file/remote.py | 3 ++- src/osw/model/static.py | 3 ++- src/osw/utils/strings.py | 12 ++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/osw/controller/file/remote.py b/src/osw/controller/file/remote.py index 5497569a..11a592ea 100644 --- a/src/osw/controller/file/remote.py +++ b/src/osw/controller/file/remote.py @@ -4,7 +4,8 @@ from osw.controller.file.base import FileController from osw.core import model -# TODO: add addional remove file with https://docs.prefect.io/2.11.4/concepts/filesystems/ +# TODO: add additional remove file with +# https://docs.prefect.io/2.11.4/concepts/filesystems/ # Note: the order of the base classes is important diff --git a/src/osw/model/static.py b/src/osw/model/static.py index fbe73d18..bb4ea65f 100644 --- a/src/osw/model/static.py +++ b/src/osw/model/static.py @@ -68,7 +68,8 @@ def test_if_empty_list_or_none(obj) -> bool: k: v for k, v in self.dict().items() if not test_if_empty_list_or_none(v) } combined_args = {**self_args, **kwargs} - del combined_args["type"] + if "type" in combined_args: + del combined_args["type"] return cls(**combined_args) diff --git a/src/osw/utils/strings.py b/src/osw/utils/strings.py index 7b49de92..639aa228 100644 --- a/src/osw/utils/strings.py +++ b/src/osw/utils/strings.py @@ -52,6 +52,9 @@ class RegExPatternExtended(OswBaseModel): used to test the pattern by asserting list(self.example_match.groups.values()) == self.expected_groups""" + class Config: + arbitrary_types_allowed = True + def __init__(self, **data): super().__init__(**data) if isinstance(self.pattern, str): @@ -113,14 +116,14 @@ def test_pattern(self) -> bool: else: return False - class Config: - arbitrary_types_allowed = True - class MatchResult(OswBaseModel): match: Union[re.Match, None] pattern: Union[RegExPatternExtended, None] + class Config: + arbitrary_types_allowed = True + @property def groups(self): """Return a dictionary representation of the object, enabling accessing the @@ -133,9 +136,6 @@ def groups(self): for key in keys } - class Config: - arbitrary_types_allowed = True - class SearchResult(MatchResult): pass From 2cbac2107e7e8f96369b0d3f52eda18eea241f1d Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Wed, 6 Nov 2024 17:24:36 +0100 Subject: [PATCH 05/52] Bug fixes and doc improvements to express.py --- examples/use_express_functions.py | 1 - src/osw/express.py | 79 ++++++++++++++++++++++++------- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/examples/use_express_functions.py b/examples/use_express_functions.py index 9024b03f..92298cdb 100644 --- a/examples/use_express_functions.py +++ b/examples/use_express_functions.py @@ -37,7 +37,6 @@ overwrite=True, # Required if file already exists ) local_file_path = local_file.path -local_file.close() # required to release the file lock # Open a file with context manager directly from an OSW instance with osw_download_file( diff --git a/src/osw/express.py b/src/osw/express.py index ed0aaab4..b7c9e9f4 100644 --- a/src/osw/express.py +++ b/src/osw/express.py @@ -12,6 +12,8 @@ IO, TYPE_CHECKING, Any, + AnyStr, + Buffer, Dict, List, Optional, @@ -178,19 +180,22 @@ def __init__( self.cred_filepath = cred_filepath def __enter__(self): + """Return self when entering the context manager.""" return self def __exit__(self): + """Close the connection to the OSL instance when exiting the context manager.""" self.close_connection() def close_connection(self): + """Close the connection to the OSL instance.""" self.site._site.connection.close() def shut_down(self): + """Makes sure this OSL instance can't be reused after it was shut down, + as the connection can't be reopened except when initializing a new instance.""" self.close_connection() del self - # Make sure this osw instance can't be reused after it was shut down (the - # connection can't be reopened except when initializing a new instance) def install_dependencies( self, @@ -334,8 +339,10 @@ def upload_file( data = {**locals(), **properties} # Clean data dict to avoid passing None values data = {key: value for key, value in data.items() if value is not None} + # Make sure self is passed as osw_express + data["osw_express"] = self # Initialize the UploadFileResult object - return UploadFileResult(source=source, osw_express=self, **data) + return UploadFileResult(source=source, **data) class DataModel(OswBaseModel): @@ -467,7 +474,7 @@ def import_with_fallback( class FileResult(OswBaseModel): url_or_title: Optional[str] = None """The URL or full page title of the WikiFile page.""" - file: Optional[TextIO] = None + file_io: Optional[TextIO] = None """The file object. They type depends on the file type.""" mode: str = "r" """The mode to open the file in. Default is 'r'. Implements the built-in open.""" @@ -494,20 +501,45 @@ class FileResult(OswBaseModel): class Config: arbitrary_types_allowed = True - def open(self, mode: str = "r", **kwargs): + def open(self, mode: str = None, **kwargs) -> TextIO: + """Open the file, if not already opened using the 'mode' argument (priority) or + the 'mode' attribute.""" + if mode is None: + mode = self.mode kwargs["mode"] = mode - return open(self.path, **kwargs) + if self.file_io is None or self.file_io.closed: + return open(self.path, **kwargs) + return self.file_io + + def close(self) -> None: + """Close the file, if not already closed.""" + if self.file_io is None or self.file_io.closed: + warn("File already closed or not opened.") + else: + self.file_io.close() - def close(self): - self.file.close() + def read(self, n: int = -1) -> AnyStr: + """Read the file. If n is not specified, the entire file will be read. + If the file is not already opened, it will be opened.""" + if self.file_io is None or self.file_io.closed: + self.file_io = self.open(mode="r") + return self.file_io.read(n) - def read(self, *args, **kwargs): - return self.file.read(*args, **kwargs) + def write(self, s: Union[Buffer, AnyStr]): + """Write to the file. If the file is not already opened, it will be opened.""" + if self.file_io is None or self.file_io.closed: + self.file_io = self.open(mode="w") + return self.file_io.write(s) def __enter__(self): + """Open the file when entering the context manager.""" + if self.file_io is None or self.file_io.closed: + self.file_io = self.open() return self def __exit__(self, exc_type, exc_value, traceback): + """Close the file when exiting the context manager, and deletes the file if + 'delete_after_use' was set.""" self.close() if self.delete_after_use and self.path.exists(): self.path.unlink() @@ -523,6 +555,14 @@ def process_init_data(self, data: Dict[str, Any]) -> Dict[str, Any]: if data.get(key) is None: data[key] = value # Do replacements + if ( + data.get("label") == InMemoryController.__fields__["label"].default + or data.get("label") == LocalFileController.__fields__["label"].default + or data.get("label") == WikiFileController.__fields__["label"].default + ): + # Make sure that the label is not set to the default value, it will be + # set by the source file controller + del data["label"] if data.get("cred_filepath") is None: data["cred_filepath"] = cred_filepath_default.get_default() if not data.get("cred_filepath").parent.exists(): @@ -621,8 +661,7 @@ def __init__(self, url_or_title, **data): data = {key: value for key, value in data.items() if value is not None} super().__init__(**{**lf.dict(), **data}) self.put_from(wf) - # Do open - self.file = self.open(mode=data.get("mode")) + # File is only opened at request to avoid locking the file def osw_download_file( @@ -795,10 +834,18 @@ def __init__( ) # Create an osw_express object if not given if data.get("osw_express") is None: - data["osw_express"] = OswExpress( - domain=data.get("domain"), - cred_mngr=data.get("cred_mngr"), - ) + create_new = True + # Try to get the osw_express object from the source_file_controller + if data.get("source_file_controller") is not None: + if hasattr(data["source_file_controller"], "osw_express"): + create_new = False + data["osw_express"] = data["source_file_controller"].osw_express + # Otherwise create a new osw_express object + if create_new: + data["osw_express"] = OswExpress( + domain=data.get("domain"), + cred_mngr=data.get("cred_mngr"), + ) # If given set titel and namespace if data.get("target_fpt") is not None: namespace = data.get("target_fpt").split(":")[0] From df5cecdf0af0c7eca20fbbf66f3a71dd7a1a83df Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Wed, 6 Nov 2024 18:42:35 +0100 Subject: [PATCH 06/52] Fix import_with_fallback() --- src/osw/express.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/osw/express.py b/src/osw/express.py index b7c9e9f4..57c7991a 100644 --- a/src/osw/express.py +++ b/src/osw/express.py @@ -368,7 +368,8 @@ def import_with_fallback( Parameters ---------- to_import - List of DataModel objects to import. + List of DataModel objects or a dictionary, with (key: value) pairs (class_name: + osw_fpt) to import. module (Optional) The module to import the data models from. Used only if to_import is of type List[Dict]. Defaults to 'osw.model.entity' if not specified. @@ -384,9 +385,11 @@ def import_with_fallback( """ if isinstance(to_import, dict): + # The arg 'to_import' should have the right structure to act as 'dependencies' # Assume all DataModels are part of osw.model.entity if module is None: module = "osw.model.entity" + # A list of DataModels will be derived from the dict to_import = [ DataModel( module=module, @@ -396,6 +399,7 @@ def import_with_fallback( for key, value in to_import.items() ] try: + # Try to import the listed data models from the (listed) module(s) for ti in to_import: # Raises AttributeError if the target could not be found globals()[ti.class_name] = getattr( @@ -406,21 +410,21 @@ def import_with_fallback( dependencies = {} warn( "No 'dependencies' were passed to the function " - "import_with_fallback()!" + "import_with_fallback()! Trying to derive them from 'to_import'." ) new_dependencies = { - f"{module.class_name}": module.osw_fpt + module.class_name: module.osw_fpt for module in to_import if module.osw_fpt is not None } + dependencies.update(new_dependencies) if not dependencies: - # If dependencies is an empty dict, + # If dependencies is still an empty dict, raise AttributeError( f"An exception occurred while loading the module dependencies: \n'{e}'" "No 'dependencies' were passed to the function import_with_fallback() " "and could not be derived from 'to_import'!" ) - dependencies.update(new_dependencies) warn( f"An exception occurred while loading the module dependencies: \n'{e}'" "You will be now have to connect to an OSW instance to fetch the " @@ -434,7 +438,7 @@ def import_with_fallback( osw_express.install_dependencies(dependencies, mode="append") osw_express.shut_down() # Avoiding connection error - # Try again + # Try again to import the data models for ti in to_import: # Raises AttributeError if the target could not be found globals()[ti.class_name] = getattr( From 22bbbaf322d358a30dd4c68d4c62e81b1c09c425 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Wed, 6 Nov 2024 17:19:58 +0100 Subject: [PATCH 07/52] Improvements to usability and readability --- src/osw/data/import_utility.py | 61 ++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/src/osw/data/import_utility.py b/src/osw/data/import_utility.py index 37f936fa..b7d137a6 100644 --- a/src/osw/data/import_utility.py +++ b/src/osw/data/import_utility.py @@ -9,6 +9,7 @@ import numpy as np from geopy import Nominatim from jsonpath_ng import ext as jp +from pydantic.v1 import create_model import osw.utils.strings as strutil from osw import wiki_tools as wt @@ -28,13 +29,58 @@ # Classes class HelperModel(model.OswBaseModel): + """Helper class for model transformations. The first base of the inheriting class + should always be the target class and the second base should be this class. + + Example + ------- + >>> class Person(model.OswBaseModel): + >>> first_name: str + >>> surname: str + >>> email: Set[str] + >>> + >>> john_dict = {"FirstName": "John", "LastName": "Doe", "Email": { + "john.doe@example.com"}} + >>> + >>> class PersonHelper(Person, HelperModel): + >>> FirstName: Any + >>> LastName: Any + >>> Email: Any + >>> + >>> def transform_attributes(self, dd: dict) -> bool: + >>> super().transform_attributes(dd) + >>> self.first_name = self.FirstName + >>> self.surname = self.LastName + >>> self.email = {self.Email} + >>> return True + """ + # Custom attributes attributes_transformed: bool = False references_transformed: bool = False casted_instance: Any = None full_page_title: Optional[str] - def transform_attributes(self, dd: dict) -> bool: + class Config: + arbitrary_types_allowed = True + + def __init_subclass__(cls, **kwargs): + """Will overwrite the annotations and fields of the inheriting class, + defined in the first base class with Optional[Any] annotations. This is + necessary to prevent errors when casting to the inheriting class.""" + super().__init_subclass__(**kwargs) + first_base = cls.__bases__[0] + if not issubclass(first_base, model.OswBaseModel): + return None + fields = {name: (Optional[Any], None) for name in first_base.__annotations__} + new_first_base = create_model(first_base.__name__, **fields) + for field_name in new_first_base.__fields__: + if field_name in cls.__fields__: # Replace existing fields + cls.__fields__[field_name] = new_first_base.__fields__[field_name] + if field_name in cls.__annotations__: # Replace existing annotations + cls.__annotations__[field_name] = Optional[Any] + + def transform_attributes(self, dd: dict = None) -> bool: if not self.attributes_transformed: uuid = uuid_module.uuid4() if hasattr(self, "uuid"): @@ -45,7 +91,7 @@ def transform_attributes(self, dd: dict) -> bool: self.attributes_transformed = True return True - def transform_references(self, dd: dict) -> bool: + def transform_references(self, dd: dict = None) -> bool: if not self.attributes_transformed: self.transform_attributes(dd) if not self.references_transformed: @@ -56,14 +102,22 @@ def transform_references(self, dd: dict) -> bool: self.references_transformed = True return True - def cast_to_superclass(self, dd): + def cast_to_superclass(self, dd: dict = None, return_casted: bool = False) -> bool: + """Casts the instance to the superclass of the inheriting class. Assumes that + the first base of the inheriting class is the target class.""" if not self.references_transformed: self.transform_references(dd) else: superclass = self.__class__.__bases__[0] self.casted_instance = self.cast_none_to_default(cls=superclass) + if return_casted: + return self.casted_instance return True + @property + def casted(self): + return self.cast_to_superclass(return_casted=True) + # Functions def transform_attributes_and_merge( @@ -89,6 +143,7 @@ def transform_attributes_and_merge( if not inplace: ent = copy.deepcopy(ent) ent_as_dict = copy.deepcopy(ent_as_dict) + # Transform attributes ent, ent_as_dict = loop_and_call_method( entities=ent, method_name="transform_attributes", From 01f0b1d8ad8b05d255527b8bb3efe05cd28a3ea6 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Thu, 7 Nov 2024 10:21:26 +0100 Subject: [PATCH 08/52] Fix cases where the default_factory would not be present in the inherited model --- src/osw/data/import_utility.py | 43 +++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/osw/data/import_utility.py b/src/osw/data/import_utility.py index b7d137a6..a16df9c1 100644 --- a/src/osw/data/import_utility.py +++ b/src/osw/data/import_utility.py @@ -1,15 +1,16 @@ import copy +import inspect import re import uuid as uuid_module import warnings from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Type, Union import deepl import numpy as np from geopy import Nominatim from jsonpath_ng import ext as jp -from pydantic.v1 import create_model +from pydantic.v1.fields import ModelField import osw.utils.strings as strutil from osw import wiki_tools as wt @@ -47,7 +48,7 @@ class HelperModel(model.OswBaseModel): >>> LastName: Any >>> Email: Any >>> - >>> def transform_attributes(self, dd: dict) -> bool: + >>> def transform_attributes(self, dd: dict = None) -> bool: >>> super().transform_attributes(dd) >>> self.first_name = self.FirstName >>> self.surname = self.LastName @@ -69,14 +70,33 @@ def __init_subclass__(cls, **kwargs): defined in the first base class with Optional[Any] annotations. This is necessary to prevent errors when casting to the inheriting class.""" super().__init_subclass__(**kwargs) - first_base = cls.__bases__[0] + first_base: Type[model.OswBaseModel] = cls.__bases__[0] if not issubclass(first_base, model.OswBaseModel): return None - fields = {name: (Optional[Any], None) for name in first_base.__annotations__} - new_first_base = create_model(first_base.__name__, **fields) - for field_name in new_first_base.__fields__: + + model_fields = {} + constructor_kwargs = [ + ele + for ele in list(inspect.signature(ModelField.__init__).parameters.keys()) + if ele != "self" + ] + for field_name in first_base.__fields__: + model_field_kwargs = {} + for arg in constructor_kwargs: + arg_val = getattr(first_base.__fields__[field_name], arg) + if arg_val is None: + # Keeps the default values of the ModelField class constructor + continue + # Reuses values originally passed to construct the ModelField instance + model_field_kwargs[arg] = arg_val + # Overwrite the type and required model_field_kwargs + model_field_kwargs["type_"] = Optional[Any] + model_field_kwargs["required"] = False + # Create a new ModelField instance + model_fields[field_name] = ModelField(**model_field_kwargs) + for field_name in model_fields: if field_name in cls.__fields__: # Replace existing fields - cls.__fields__[field_name] = new_first_base.__fields__[field_name] + cls.__fields__[field_name] = model_fields[field_name] if field_name in cls.__annotations__: # Replace existing annotations cls.__annotations__[field_name] = Optional[Any] @@ -217,6 +237,7 @@ def get_uuid_from_object_via_type(obj: Any) -> Union[uuid_module.UUID, None]: def get_lang_specific_label(label: list, lang: str) -> Union[str, None]: """Get the label in a specific language from a list of labels""" + # todo: rework to not break on missing LangCode for ele in label: if ele["lang"] == model.LangCode(lang): return ele["text"] @@ -365,15 +386,15 @@ def jsonpath_search_and_return_list( ------- Searching through entites of type HelperWikiFile within all entities ( entities_as_dict) and returning the full_page_title of entities matching filename - by the attribute name. Afterwards the attribute image is set to the result if it any - >>> res = diu.jsonpath_search_and_return_list( + by the attribute name. Afterward the attribute image is set to the result if it any + >>> res = jsonpath_search_and_return_list( >>> jp_str=f'*[?name = "{filename}"]', >>> val_key="full_page_title", >>> search_tar=entities_as_dict, >>> class_to_match=HelperWikiFile, >>> ) >>> if len(res) > 0: - >>> self.image = res[0] + >>> image = res[0] """ jp_parse = jp.parse(path=jp_str) From c104e3966e817375d68ef4cf3c0c7d535e1ec205 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Fri, 15 Nov 2024 17:16:08 +0100 Subject: [PATCH 09/52] Fix: import_with_fallback() & Context Manager for osw_download_file() --- src/osw/express.py | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/osw/express.py b/src/osw/express.py index 57c7991a..ede0ee6b 100644 --- a/src/osw/express.py +++ b/src/osw/express.py @@ -5,6 +5,7 @@ import importlib.util import re +from io import TextIOWrapper from pathlib import Path from warnings import warn @@ -33,8 +34,11 @@ DOWNLOAD_DIR_DEFAULT = BASE_PATH / "osw_files" / "downloads" DEPENDENCIES = { - # "Entity": "Category:Entity", # depends on nothing + # "Entity": "Category:Entity", # depends on nothing# + "Category": "Category:Category", # depends on Entity + "Property": "Category:Property", # depends on Entity # "Item": "Category:Item", # depends on Entity + "Characteristics": "Category:OSW93ccae36243542ceac6c951450a81d47", # depends on Item # "Data": "Category:OSW2ac4493f8635481eaf1db961b63c8325", # depends on Item # "File": "Category:OSWff333fd349af4f65a69100405a9e60c7", # depends on Data "LocalFile": "Category:OSW3e3f5dd4f71842fbb8f270e511af8031", # depends on File @@ -358,6 +362,7 @@ class DataModel(OswBaseModel): def import_with_fallback( to_import: Union[List[DataModel], Dict[str, str]], + caller_globals: dict, module: str = None, dependencies: Dict[str, str] = None, domain: str = None, @@ -370,6 +375,9 @@ def import_with_fallback( to_import List of DataModel objects or a dictionary, with (key: value) pairs (class_name: osw_fpt) to import. + caller_globals + (Mandatory!) The globals dictionary as returned by globals() in the calling + script. module (Optional) The module to import the data models from. Used only if to_import is of type List[Dict]. Defaults to 'osw.model.entity' if not specified. @@ -380,9 +388,16 @@ def import_with_fallback( The domain of the OSL instance to connect to, if the dependencies are not available in the local osw.model.entity module. + Examples + -------- + >>> import_with_fallback( + >>> [DataModel(module="osw.controller.file.base",class_name="FileController")], + >>> globals(), + >>> ) + Returns ------- - + None """ if isinstance(to_import, dict): # The arg 'to_import' should have the right structure to act as 'dependencies' @@ -398,11 +413,16 @@ def import_with_fallback( ) for key, value in to_import.items() ] + if to_import is None: + raise ValueError( + "Either the argument 'to_import' or 'dependencies' must be passed to the " + "function import_with_fallback()." + ) try: # Try to import the listed data models from the (listed) module(s) for ti in to_import: # Raises AttributeError if the target could not be found - globals()[ti.class_name] = getattr( + caller_globals[ti.class_name] = getattr( importlib.import_module(ti.module), ti.class_name ) except Exception as e: @@ -419,7 +439,6 @@ def import_with_fallback( } dependencies.update(new_dependencies) if not dependencies: - # If dependencies is still an empty dict, raise AttributeError( f"An exception occurred while loading the module dependencies: \n'{e}'" "No 'dependencies' were passed to the function import_with_fallback() " @@ -427,7 +446,7 @@ def import_with_fallback( ) warn( f"An exception occurred while loading the module dependencies: \n'{e}'" - "You will be now have to connect to an OSW instance to fetch the " + "You will now have to connect to an OSW instance to fetch the " "dependencies from!" ) if domain is None: @@ -441,7 +460,7 @@ def import_with_fallback( # Try again to import the data models for ti in to_import: # Raises AttributeError if the target could not be found - globals()[ti.class_name] = getattr( + caller_globals[ti.class_name] = getattr( importlib.import_module(ti.module), ti.class_name ) @@ -465,6 +484,7 @@ def import_with_fallback( class_name="WikiFileController", ), ], + caller_globals=globals(), dependencies=DEPENDENCIES, ) @@ -478,7 +498,7 @@ def import_with_fallback( class FileResult(OswBaseModel): url_or_title: Optional[str] = None """The URL or full page title of the WikiFile page.""" - file_io: Optional[TextIO] = None + file_io: Optional[Union[TextIO, TextIOWrapper, None]] = None """The file object. They type depends on the file type.""" mode: str = "r" """The mode to open the file in. Default is 'r'. Implements the built-in open.""" @@ -512,7 +532,7 @@ def open(self, mode: str = None, **kwargs) -> TextIO: mode = self.mode kwargs["mode"] = mode if self.file_io is None or self.file_io.closed: - return open(self.path, **kwargs) + self.file_io = open(self.path, **kwargs) return self.file_io def close(self) -> None: @@ -538,7 +558,7 @@ def write(self, s: Union[Buffer, AnyStr]): def __enter__(self): """Open the file when entering the context manager.""" if self.file_io is None or self.file_io.closed: - self.file_io = self.open() + self.open() return self def __exit__(self, exc_type, exc_value, traceback): From 91bd5a2aa3768a1da7e6060d66eba5f4ed05e1aa Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Fri, 15 Nov 2024 17:19:29 +0100 Subject: [PATCH 10/52] Moving some functionality from functions in osw.utils.wiki to methods of osw.model.static.OswBaseModel --- src/osw/model/static.py | 131 ++++++++++++++++++++++++++++++++++++++++ src/osw/utils/wiki.py | 79 +++--------------------- 2 files changed, 140 insertions(+), 70 deletions(-) diff --git a/src/osw/model/static.py b/src/osw/model/static.py index bb4ea65f..e240f289 100644 --- a/src/osw/model/static.py +++ b/src/osw/model/static.py @@ -20,8 +20,70 @@ _basemodel_decorator = lambda x: x # noqa: E731 +def custom_issubclass(obj: Union[type, T], class_name: str) -> bool: + """ + Custom issubclass function that checks if the object is a subclass of a class + with the given name. + + Parameters + ---------- + obj : object + The object to check. + class_name : str + The name of the class to check against. + + Returns + ------- + bool + True if the object is a subclass of the class with the given name, + False otherwise. + """ + + def check_bases(cls, name): + if hasattr(cls, "__name__") and cls.__name__ == name: + return True + if not hasattr(cls, "__bases__"): + return False + for base in cls.__bases__: + if check_bases(base, name): + return True + return False + + return check_bases(obj, class_name) + + +def custom_isinstance(obj: Union[type, T], class_name: str) -> bool: + """ + Custom isinstance function that checks if the object is an instance of a class with + the given name. + + Parameters + ---------- + obj : object + The object to check. + class_name : str + The name of the class to check against. + + Returns + ------- + bool + True if the object is an instance of the class with the given name, + False otherwise. + """ + if not hasattr(obj, "__class__"): + return False + + return custom_issubclass(obj.__class__, class_name) + + @_basemodel_decorator class OswBaseModel(BaseModel): + + class Config: + strict = False # Additional fields are allowed + validate_assignment = True # Ensures that the assignment of a value to a + # field is validated + def full_dict(self, **kwargs): # extent BaseClass export function d = super().dict(**kwargs) for key in ("_osl_template", "_osl_footer"): @@ -72,6 +134,75 @@ def test_if_empty_list_or_none(obj) -> bool: del combined_args["type"] return cls(**combined_args) + def get_uuid(self) -> Union[str, None]: + return getattr(self, "uuid", None) + + def get_osw_id(self) -> Union[str, None]: + osw_id = getattr(self, "osw_id", None) + uuid = self.get_uuid() + from_uuid = None if uuid is None else f"OSW{str(uuid).replace('-', '')}" + if osw_id is None: + return from_uuid + if osw_id != from_uuid: + raise ValueError(f"OSW-ID does not match UUID: {osw_id} != {from_uuid}") + return osw_id + + def get_namespace(self) -> Union[str, None]: + """Determines the wiki namespace based on the entity's type/class + + Returns + ------- + The namespace as a string or None if the namespace could not be determined + """ + namespace = None + + if hasattr(self, "meta") and self.meta and self.meta.wiki_page: + if self.meta.wiki_page.namespace: + namespace = self.meta.wiki_page.namespace + + if namespace is None: + if custom_issubclass(self, "Entity"): + namespace = "Category" + elif custom_isinstance(self, "Category"): + namespace = "Category" + elif custom_issubclass(self, "Characteristic"): + namespace = "Category" + elif custom_isinstance(self, "Item"): + namespace = "Item" + elif custom_isinstance(self, "Property"): + namespace = "Property" + elif custom_isinstance(self, "WikiFile"): + namespace = "File" + + return namespace + + def get_title(self) -> Union[str, None]: + title = None + + if hasattr(self, "meta") and self.meta and self.meta.wiki_page: + if self.meta.wiki_page.title: + title = self.meta.wiki_page.title + + if title is None: + title = self.get_osw_id() + + return title + + def get_iri(self) -> Union[str, None]: + """Determines the IRI / wiki full title (namespace:title) based on the entity's + data + + Returns + ------- + The full title as a string or None if the title could not be determined. + """ + namespace = self.get_namespace() + title = self.get_title() + if namespace is not None and title is not None: + return namespace + ":" + title + elif title is not None: + return title + class Ontology(OswBaseModel): iri: str diff --git a/src/osw/utils/wiki.py b/src/osw/utils/wiki.py index 4c69241d..9a15e96b 100644 --- a/src/osw/utils/wiki.py +++ b/src/osw/utils/wiki.py @@ -37,70 +37,22 @@ def get_uuid(osw_id) -> UUID: def get_namespace(entity: Union[OswBaseModel, Type[OswBaseModel]]) -> Union[str, None]: - """determines the wiki namespace based on the entity's type/class + """Determines the wiki namespace based on the entity's type/class Parameters ---------- entity - the entity to determine the namespace for + The entity to determine the namespace for Returns ------- - the namespace as a string or None if the namespace could not be determined + The namespace as a string or None if the namespace could not be determined """ - namespace = None - - if hasattr(entity, "meta") and entity.meta and entity.meta.wiki_page: - if entity.meta.wiki_page.namespace: - namespace = entity.meta.wiki_page.namespace - - # (model classes may not exist => try except) - # note: this may not work properly with dynamic reloaded model module - # note: some of these lines lead to AssertationError in coverage.py - # and are therefore excluded from coverage - # (see also https://github.com/OpenSemanticLab/osw-python/issues/74) - if namespace is None: - try: - if issubclass(entity, model.Entity): - namespace = "Category" - except (TypeError, AttributeError): - pass - if namespace is None: - try: - if isinstance(entity, model.Category): # pragma: no cover - namespace = "Category" - except AttributeError: - pass - if namespace is None: - try: - if issubclass(entity, model.Characteristic): # pragma: no cover - namespace = "Category" - except (TypeError, AttributeError): - pass - if namespace is None: - try: - if isinstance(entity, model.Item): - namespace = "Item" - except AttributeError: - pass - if namespace is None: - try: - if isinstance(entity, model.Property): # pragma: no cover - namespace = "Property" - except AttributeError: - pass - if namespace is None: - try: - if isinstance(entity, model.WikiFile): # pragma: no cover - namespace = "File" - except AttributeError: - pass - - return namespace + return entity.get_namespace() def get_title(entity: model.Entity) -> Union[str, None]: - """determines the wiki page title based on the entity's data + """Determines the wiki page title based on the entity's data Parameters ---------- @@ -111,16 +63,7 @@ def get_title(entity: model.Entity) -> Union[str, None]: ------- the title as a string or None if the title could not be determined """ - title = None - - if hasattr(entity, "meta") and entity.meta and entity.meta.wiki_page: - if entity.meta.wiki_page.title: - title = entity.meta.wiki_page.title - - if title is None: - title = get_osw_id(entity.uuid) - - return title + return entity.get_title() def get_full_title(entity: model.Entity) -> Union[str, None]: @@ -135,12 +78,7 @@ def get_full_title(entity: model.Entity) -> Union[str, None]: ------- the full title as a string or None if the title could not be determined """ - namespace = get_namespace(entity) - title = get_title(entity) - if namespace is not None and title is not None: - return namespace + ":" + title - else: - return title + return entity.get_full_title() def namespace_from_full_title(full_title: str) -> str: @@ -170,7 +108,8 @@ def title_from_full_title(full_title: str) -> str: ------- the title as a string """ - return full_title.split(":")[-1] + namespace = full_title.split(":")[0] + return full_title.split(f"{namespace}:")[-1] def is_empty(val): From 8539b9593de0854a6b6a07f8f0ab049bf323e7fb Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Fri, 15 Nov 2024 17:20:53 +0100 Subject: [PATCH 11/52] Making SearchParam available at WtSite.SearchParam --- src/osw/wtsite.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/osw/wtsite.py b/src/osw/wtsite.py index 92edd1f9..e3de2ce5 100644 --- a/src/osw/wtsite.py +++ b/src/osw/wtsite.py @@ -385,7 +385,10 @@ def _clear_cookies(self): cookie.domain, cookie.path, cookie.name ) - def prefix_search(self, text: Union[str, wt.SearchParam]): + class SearchParam(wt.SearchParam): + pass + + def prefix_search(self, text: Union[str, SearchParam]): """Send a prefix search request to the site. Parameters @@ -399,7 +402,7 @@ def prefix_search(self, text: Union[str, wt.SearchParam]): """ return wt.prefix_search(self._site, text) - def semantic_search(self, query: Union[str, wt.SearchParam]): + def semantic_search(self, query: Union[str, SearchParam]): """Send a swm ask query to the site. Parameters @@ -922,7 +925,7 @@ def get_file_pages(self, limit: int = 1000000) -> List[str]: def get_file_info_and_usage( self, - page_titles: Union[str, List[str], wt.SearchParam], + page_titles: Union[str, List[str], SearchParam], ) -> list: """Get the file info and usage for one or more file pages. From b7222f39615b7fc572b85f354851f90ebd41a236 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Fri, 15 Nov 2024 17:21:44 +0100 Subject: [PATCH 12/52] Making openpyxl a requirement for dataimport --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index a9a70693..3f5c7ef5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -88,6 +88,7 @@ S3 = dataimport = geopy deepl + openpyxl UI = pysimplegui all = From 5b5060c2c437e3436e1eb39cff75d8140313a1a0 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Fri, 15 Nov 2024 17:28:30 +0100 Subject: [PATCH 13/52] Improvements to import_utility.py - potentially breaking some functions, but most likely avoiding unwanted behavior --- src/osw/data/import_utility.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/osw/data/import_utility.py b/src/osw/data/import_utility.py index a16df9c1..00c54181 100644 --- a/src/osw/data/import_utility.py +++ b/src/osw/data/import_utility.py @@ -102,34 +102,37 @@ def __init_subclass__(cls, **kwargs): def transform_attributes(self, dd: dict = None) -> bool: if not self.attributes_transformed: - uuid = uuid_module.uuid4() - if hasattr(self, "uuid"): - if self.uuid is not None: - uuid = self.uuid - self.full_page_title = uuid_to_full_page_title(uuid=uuid) + if getattr(self, "uuid", None) is None: + if hasattr(self, "uuid"): + self.uuid = uuid_module.uuid4() + self.full_page_title = self.get_iri() # All set successfully self.attributes_transformed = True return True def transform_references(self, dd: dict = None) -> bool: + # Ensure that the attributes are transformed before transforming the references if not self.attributes_transformed: self.transform_attributes(dd) if not self.references_transformed: # Test casting - superclass = self.__class__.__bases__[0] - self.casted_instance = self.cast_none_to_default(cls=superclass) + # superclass = self.__class__.__bases__[0] + # Todo: this might cast with attributes not yet set + # self.casted_instance = self.cast_none_to_default(cls=superclass) # All set successfully self.references_transformed = True return True - def cast_to_superclass(self, dd: dict = None, return_casted: bool = False) -> bool: + def cast_to_superclass( + self, dd: dict = None, return_casted: bool = False + ) -> Union[bool, Type[model.OswBaseModel]]: """Casts the instance to the superclass of the inheriting class. Assumes that the first base of the inheriting class is the target class.""" if not self.references_transformed: self.transform_references(dd) - else: - superclass = self.__class__.__bases__[0] - self.casted_instance = self.cast_none_to_default(cls=superclass) + + superclass = self.__class__.__bases__[0] + self.casted_instance = self.cast_none_to_default(cls=superclass) if return_casted: return self.casted_instance return True From 13acb67ba3a7cec9510cf4467ff496944d6379ef Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Sat, 16 Nov 2024 10:36:15 +0100 Subject: [PATCH 14/52] Renaming of casted -> transformed --- src/osw/data/import_utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/osw/data/import_utility.py b/src/osw/data/import_utility.py index 00c54181..2d52cf7a 100644 --- a/src/osw/data/import_utility.py +++ b/src/osw/data/import_utility.py @@ -138,7 +138,7 @@ def cast_to_superclass( return True @property - def casted(self): + def transformed(self): return self.cast_to_superclass(return_casted=True) From 76bec5eb27e0cd0638e2eb47c40a8f6c7bae9469 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Sun, 17 Nov 2024 18:45:12 +0100 Subject: [PATCH 15/52] Fixing failing tests due to missing methods (on classes) --- src/osw/model/static.py | 200 +++++++++++++++++++++++++++++----------- src/osw/utils/wiki.py | 50 +--------- 2 files changed, 146 insertions(+), 104 deletions(-) diff --git a/src/osw/model/static.py b/src/osw/model/static.py index e240f289..39fae071 100644 --- a/src/osw/model/static.py +++ b/src/osw/model/static.py @@ -2,7 +2,9 @@ This module is to be imported in the dynamically created and updated entity.py module. """ +from types import NoneType from typing import TYPE_CHECKING, Type, TypeVar, Union +from uuid import UUID from pydantic.v1 import BaseModel @@ -79,10 +81,10 @@ def custom_isinstance(obj: Union[type, T], class_name: str) -> bool: @_basemodel_decorator class OswBaseModel(BaseModel): - class Config: - strict = False # Additional fields are allowed - validate_assignment = True # Ensures that the assignment of a value to a - # field is validated + # class Config: + # strict = False # Additional fields are allowed + # validate_assignment = True # Ensures that the assignment of a value to a + # field is validated def full_dict(self, **kwargs): # extent BaseClass export function d = super().dict(**kwargs) @@ -116,7 +118,12 @@ def cast_none_to_default(self, cls: Union[Type[T], type], **kwargs) -> T: includes None values, the attribute is not passed to the instance of the target class, which will then fall back to the default.""" - def test_if_empty_list_or_none(obj) -> bool: + def test_if_empty_list_or_none( + obj: Union[ + NoneType, + list, + ] + ) -> bool: if obj is None: return True elif isinstance(obj, list): @@ -134,61 +141,43 @@ def test_if_empty_list_or_none(obj) -> bool: del combined_args["type"] return cls(**combined_args) - def get_uuid(self) -> Union[str, None]: + def get_uuid(self) -> Union[str, UUID, NoneType]: + """Getter for the attribute 'uuid' of the entity + + Returns + ------- + The uuid as a string or None if the uuid could not be determined + """ return getattr(self, "uuid", None) - def get_osw_id(self) -> Union[str, None]: - osw_id = getattr(self, "osw_id", None) - uuid = self.get_uuid() - from_uuid = None if uuid is None else f"OSW{str(uuid).replace('-', '')}" - if osw_id is None: - return from_uuid - if osw_id != from_uuid: - raise ValueError(f"OSW-ID does not match UUID: {osw_id} != {from_uuid}") - return osw_id - - def get_namespace(self) -> Union[str, None]: + def get_osw_id(self) -> Union[str, NoneType]: + """Determines the OSW-ID based on the entity's uuid. + + Returns + ------- + The OSW-ID as a string or None if the OSW-ID could not be determined + """ + return get_osw_id(self) + + def get_namespace(self) -> Union[str, NoneType]: """Determines the wiki namespace based on the entity's type/class Returns ------- The namespace as a string or None if the namespace could not be determined """ - namespace = None - - if hasattr(self, "meta") and self.meta and self.meta.wiki_page: - if self.meta.wiki_page.namespace: - namespace = self.meta.wiki_page.namespace - - if namespace is None: - if custom_issubclass(self, "Entity"): - namespace = "Category" - elif custom_isinstance(self, "Category"): - namespace = "Category" - elif custom_issubclass(self, "Characteristic"): - namespace = "Category" - elif custom_isinstance(self, "Item"): - namespace = "Item" - elif custom_isinstance(self, "Property"): - namespace = "Property" - elif custom_isinstance(self, "WikiFile"): - namespace = "File" - - return namespace - - def get_title(self) -> Union[str, None]: - title = None - - if hasattr(self, "meta") and self.meta and self.meta.wiki_page: - if self.meta.wiki_page.title: - title = self.meta.wiki_page.title - - if title is None: - title = self.get_osw_id() + return get_namespace(self) - return title + def get_title(self) -> Union[str, NoneType]: + """Determines the wiki page title based on the entity's data - def get_iri(self) -> Union[str, None]: + Returns + ------- + The title as a string or None if the title could not be determined + """ + return get_title(self) + + def get_iri(self) -> Union[str, NoneType]: """Determines the IRI / wiki full title (namespace:title) based on the entity's data @@ -196,12 +185,111 @@ def get_iri(self) -> Union[str, None]: ------- The full title as a string or None if the title could not be determined. """ - namespace = self.get_namespace() - title = self.get_title() - if namespace is not None and title is not None: - return namespace + ":" + title - elif title is not None: - return title + return get_full_title(self) + + +def get_osw_id(entity: Union[OswBaseModel, Type[OswBaseModel]]) -> Union[str, NoneType]: + """Determines the OSW-ID based on the entity's data - either from the entity's + attribute 'osw_id' or 'uuid'. + + Parameters + ---------- + entity + The entity to determine the OSW-ID for + + Returns + ------- + The OSW-ID as a string or None if the OSW-ID could not be determined + """ + osw_id = getattr(entity, "osw_id", None) + uuid = entity.get_uuid() + from_uuid = None if uuid is None else f"OSW{str(uuid).replace('-', '')}" + if osw_id is None: + return from_uuid + if osw_id != from_uuid: + raise ValueError(f"OSW-ID does not match UUID: {osw_id} != {from_uuid}") + return osw_id + + +def get_namespace( + entity: Union[OswBaseModel, Type[OswBaseModel]] +) -> Union[str, NoneType]: + """Determines the wiki namespace based on the entity's type/class + + Parameters + ---------- + entity + The entity to determine the namespace for + + Returns + ------- + The namespace as a string or None if the namespace could not be determined + """ + namespace = None + + if hasattr(entity, "meta") and entity.meta and entity.meta.wiki_page: + if entity.meta.wiki_page.namespace: + namespace = entity.meta.wiki_page.namespace + + if namespace is None: + if custom_issubclass(entity, "Entity"): + namespace = "Category" + elif custom_isinstance(entity, "Category"): + namespace = "Category" + elif custom_issubclass(entity, "Characteristic"): + namespace = "Category" + elif custom_isinstance(entity, "Item"): + namespace = "Item" + elif custom_isinstance(entity, "Property"): + namespace = "Property" + elif custom_isinstance(entity, "WikiFile"): + namespace = "File" + + return namespace + + +def get_title(entity: OswBaseModel) -> Union[str, NoneType]: + """Determines the wiki page title based on the entity's data + + Parameters + ---------- + entity + the entity to determine the title for + + Returns + ------- + the title as a string or None if the title could not be determined + """ + title = None + + if hasattr(entity, "meta") and entity.meta and entity.meta.wiki_page: + if entity.meta.wiki_page.title: + title = entity.meta.wiki_page.title + + if title is None: + title = get_osw_id(entity) + + return title + + +def get_full_title(entity: OswBaseModel) -> Union[str, NoneType]: + """determines the wiki full title (namespace:title) based on the entity's data + + Parameters + ---------- + entity + the entity to determine the full title for + + Returns + ------- + the full title as a string or None if the title could not be determined + """ + namespace = get_namespace(entity) + title = get_title(entity) + if namespace is not None and title is not None: + return namespace + ":" + title + elif title is not None: + return title class Ontology(OswBaseModel): diff --git a/src/osw/utils/wiki.py b/src/osw/utils/wiki.py index 9a15e96b..36410049 100644 --- a/src/osw/utils/wiki.py +++ b/src/osw/utils/wiki.py @@ -1,8 +1,7 @@ -from typing import Type, Union from uuid import UUID -import osw.model.entity as model -from osw.model.static import OswBaseModel +# Legacy imports: +from osw.model.static import get_full_title, get_namespace, get_title # noqa: F401 def get_osw_id(uuid: UUID) -> str: @@ -36,51 +35,6 @@ def get_uuid(osw_id) -> UUID: return UUID(osw_id.replace("OSW", "")) -def get_namespace(entity: Union[OswBaseModel, Type[OswBaseModel]]) -> Union[str, None]: - """Determines the wiki namespace based on the entity's type/class - - Parameters - ---------- - entity - The entity to determine the namespace for - - Returns - ------- - The namespace as a string or None if the namespace could not be determined - """ - return entity.get_namespace() - - -def get_title(entity: model.Entity) -> Union[str, None]: - """Determines the wiki page title based on the entity's data - - Parameters - ---------- - entity - the entity to determine the title for - - Returns - ------- - the title as a string or None if the title could not be determined - """ - return entity.get_title() - - -def get_full_title(entity: model.Entity) -> Union[str, None]: - """determines the wiki full title (namespace:title) based on the entity's data - - Parameters - ---------- - entity - the entity to determine the full title for - - Returns - ------- - the full title as a string or None if the title could not be determined - """ - return entity.get_full_title() - - def namespace_from_full_title(full_title: str) -> str: """extracts the namespace from a full title (namespace:title) From f63df228b8e35192d4583f864cc7845568b40ef7 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 18 Nov 2024 09:43:08 +0100 Subject: [PATCH 16/52] Moving NoneType to a separate module --- src/osw/model/static.py | 3 ++- src/osw/types.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 src/osw/types.py diff --git a/src/osw/model/static.py b/src/osw/model/static.py index 39fae071..c45993f2 100644 --- a/src/osw/model/static.py +++ b/src/osw/model/static.py @@ -2,12 +2,13 @@ This module is to be imported in the dynamically created and updated entity.py module. """ -from types import NoneType from typing import TYPE_CHECKING, Type, TypeVar, Union from uuid import UUID from pydantic.v1 import BaseModel +from osw.types import NoneType + T = TypeVar("T", bound=BaseModel) # This is dirty, but required for autocompletion: diff --git a/src/osw/types.py b/src/osw/types.py new file mode 100644 index 00000000..54a37853 --- /dev/null +++ b/src/osw/types.py @@ -0,0 +1,6 @@ +import sys + +if sys.version_info < (3, 10): + NoneType = type(None) +else: + from types import NoneType # noqa: F401 From df680e0ecd9e32f94bae95dfda9eb10b7977ab59 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 18 Nov 2024 09:43:50 +0100 Subject: [PATCH 17/52] Validate attributes of OswBaseModel on assignment --- src/osw/model/static.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/osw/model/static.py b/src/osw/model/static.py index c45993f2..4c6f59dc 100644 --- a/src/osw/model/static.py +++ b/src/osw/model/static.py @@ -82,10 +82,11 @@ def custom_isinstance(obj: Union[type, T], class_name: str) -> bool: @_basemodel_decorator class OswBaseModel(BaseModel): - # class Config: - # strict = False # Additional fields are allowed - # validate_assignment = True # Ensures that the assignment of a value to a - # field is validated + class Config: + # strict = False + # Additional fields are allowed + validate_assignment = True + # Ensures that the assignment of a value to a field is validated def full_dict(self, **kwargs): # extent BaseClass export function d = super().dict(**kwargs) From 57957a1363418ef32bdca042a1ab7829c7a2eeef Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 18 Nov 2024 10:30:51 +0100 Subject: [PATCH 18/52] Fixing validation errors, causing tests to fail --- src/osw/auth.py | 4 +++- src/osw/ontology.py | 2 +- src/osw/types.py | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/osw/auth.py b/src/osw/auth.py index a232fd5b..cb0bcf33 100644 --- a/src/osw/auth.py +++ b/src/osw/auth.py @@ -7,9 +7,10 @@ from warnings import warn import yaml -from pydantic.v1 import FilePath, PrivateAttr +from pydantic.v1 import PrivateAttr from osw.model.static import OswBaseModel +from osw.types import FilePath CREDENTIALS_FN_DEFAULT = "credentials.pwd.yaml" @@ -254,6 +255,7 @@ def save_credentials_to_file( if self.cred_filepath is None: filepath_ = [Path.cwd() / CREDENTIALS_FN_DEFAULT] if set_cred_filepath: + # Creates error if file does not exist -> Using custom FilePath self.cred_filepath = filepath_ for fp in filepath_: file = Path(fp) diff --git a/src/osw/ontology.py b/src/osw/ontology.py index 34b55535..699601d6 100644 --- a/src/osw/ontology.py +++ b/src/osw/ontology.py @@ -738,7 +738,7 @@ def _store_ontology(self, param: StoreOntologyParam): if not hasattr(e, "subclass_of"): e.subclass_of = [] if len(e.subclass_of) == 0: - e.subclass_of = self.import_config.meta_class_title + e.subclass_of.append(self.import_config.meta_class_title) elif namespace == "Property": smw_import_type = "Type:" + e.cast(model.Property).property_type else: diff --git a/src/osw/types.py b/src/osw/types.py index 54a37853..8a2aa53b 100644 --- a/src/osw/types.py +++ b/src/osw/types.py @@ -1,6 +1,23 @@ import sys +from pathlib import Path + +from pydantic.v1.types import FilePath as PydanticFilePath +from pydantic.v1.validators import path_validator # , path_exists_validator if sys.version_info < (3, 10): NoneType = type(None) else: from types import NoneType # noqa: F401 + + +class FilePath(PydanticFilePath): + # Overwrite the Pydantic FilePath class to allow non-existing paths + @classmethod + def __get_validators__(cls): + yield path_validator + # yield path_exists_validator # Remove this line to allow non-existing paths + yield cls.validate + + @classmethod + def validate(cls, value: Path) -> Path: + return value From 2967d431f18e89ee483e6c5d5efffd4bf616bac1 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 18 Nov 2024 10:32:21 +0100 Subject: [PATCH 19/52] A collection of the param objects defined in the package --- src/osw/params.py | 123 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/osw/params.py diff --git a/src/osw/params.py b/src/osw/params.py new file mode 100644 index 00000000..fef768f4 --- /dev/null +++ b/src/osw/params.py @@ -0,0 +1,123 @@ +from typing_extensions import deprecated + +from osw.auth import CredentialManager # noqa: F401 +from osw.core import ( # noqa: F401 + OSW, + OVERWRITE_CLASS_OPTIONS, + AddOverwriteClassOptions, + OverwriteOptions, +) +from osw.express import ( # noqa: F401 + CredentialsFpDefault, + DataModel, + DownloadDirDefault, + FilePathDefault, + OswExpress, +) +from osw.wiki_tools import SearchParam # noqa: F401 +from osw.wtsite import WtPage, WtSite # noqa: F401 + + +# From osw.auth.CredentialManager: +class UserPwdCredential(CredentialManager.UserPwdCredential): + pass + + +class CredentialFallback(CredentialManager.CredentialFallback): + pass + + +class CredentialConfig(CredentialManager.CredentialConfig): + pass + + +# From osw.core.OSW: +class SchemaUnregistration(OSW.SchemaUnregistration): + pass + + +class SchemaRegistration(OSW.SchemaRegistration): + pass + + +class FetchSchemaMode(OSW.FetchSchemaMode): + pass + + +class FetchSchemaParam(OSW.FetchSchemaParam): + pass + + +class LoadEntityParam(OSW.LoadEntityParam): + pass + + +class OverwriteClassParam(OSW.OverwriteClassParam): + pass + + +class StoreEntityParam(OSW.StoreEntityParam): + pass + + +class DeleteEntityParam(OSW.DeleteEntityParam): + pass + + +class QueryInstancesParam(OSW.QueryInstancesParam): + pass + + +# From osw.wtsite.WtSite: +class WtSiteConfig(WtSite.WtSiteConfig): + pass + + +@deprecated("Use WtSiteConfig instead") +class WtSiteLegacyConfig(WtSite.WtSiteLegacyConfig): + pass + + +class GetPageParam(WtSite.GetPageParam): + pass + + +class ModifySearchResultsParam(WtSite.ModifySearchResultsParam): + pass + + +class UploadPageParam(WtSite.UploadPageParam): + pass + + +class CopyPagesParam(WtSite.CopyPagesParam): + pass + + +class CreatePagePackageParam(WtSite.CreatePagePackageParam): + pass + + +class ReadPagePackageParam(WtSite.ReadPagePackageParam): + pass + + +class UploadPagePackageParam(WtSite.UploadPagePackageParam): + pass + + +# From osw.wtsite.WtPage: +class CopyPageConfig(WtPage.CopyPageConfig): + pass + + +class PageDumpConfig(WtPage.PageDumpConfig): + pass + + +class ExportConfig(WtPage.ExportConfig): + pass + + +class ImportConfig(WtPage.ImportConfig): + pass From 632a0a1b7068d4ad4ed953d434b08c6d12204f04 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 18 Nov 2024 17:33:43 +0100 Subject: [PATCH 20/52] Introducing change id and StoreEntityResult --- src/osw/core.py | 144 +++++++-------------------------------------- src/osw/express.py | 24 +++++++- 2 files changed, 43 insertions(+), 125 deletions(-) diff --git a/src/osw/core.py b/src/osw/core.py index 82593e47..22d79fb7 100644 --- a/src/osw/core.py +++ b/src/osw/core.py @@ -9,7 +9,7 @@ from copy import deepcopy from enum import Enum from typing import Any, Dict, List, Optional, Type, Union -from uuid import UUID +from uuid import UUID, uuid4 from warnings import warn import rdflib @@ -969,6 +969,10 @@ class StoreEntityParam(OswBaseModel): """A list of OverwriteClassParam objects. If a class specific overwrite setting is set, this setting is used. """ + change_id: Optional[str] = None + """ID to document the change. Entities within the same store_entity() call will + share the same change_id. This parameter can also be used to link multiple + store_entity() calls.""" meta_category_title: Optional[str] = "Category:Category" debug: Optional[bool] = False _overwrite_per_class: Dict[str, Dict[str, OSW.OverwriteClassParam]] = ( @@ -981,6 +985,15 @@ def __init__(self, **data): super().__init__(**data) if not isinstance(self.entities, list): self.entities = [self.entities] + if self.change_id is None: + self.change_id = str(uuid4()) + for entity in self.entities: + if not hasattr(entity, "meta"): + entity.meta = model.Meta() + if entity.meta.change_id is None: + entity.meta.change_id = [] + if self.change_id not in entity.meta.change_id: + entity.meta.change_id.append(self.change_id) if len(self.entities) > 5 and self.parallel is None: self.parallel = True if self.parallel is None: @@ -1006,9 +1019,15 @@ def __init__(self, **data): self._overwrite_per_class["by name"][model_name] = param self._overwrite_per_class["by type"][model_type] = param + class StoreEntityResult(OswBaseModel): + """Result of store_entity()""" + + change_id: str + """The ID of the change""" + def store_entity( self, param: Union[StoreEntityParam, OswBaseModel, List[OswBaseModel]] - ) -> None: + ) -> StoreEntityResult: """stores the given dataclass instance as OSW page by calling BaseModel.json() Parameters @@ -1171,6 +1190,7 @@ def handle_upload_object_(upload_object: UploadObject) -> None: handle_upload_object_(upload_object) for upload_object in upload_object_list ] + return OSW.StoreEntityResult(change_id=param.change_id) class DeleteEntityParam(OswBaseModel): entities: Union[OswBaseModel, List[OswBaseModel]] @@ -1304,126 +1324,6 @@ def query_instances( full_page_titles = self.site.semantic_search(search_param) return full_page_titles - class JsonLdMode(str, Enum): - """enum for jsonld processing mode""" - - expand = "expand" - flatten = "flatten" - compact = "compact" - frame = "frame" - - class ExportJsonLdParams(OswBaseModel): - context_loader_config: Optional[WtSite.JsonLdContextLoaderParams] = None - """The configuration for the JSON-LD context loader.""" - entities: Union[OswBaseModel, List[OswBaseModel]] - """The entities to convert to JSON-LD. Can be a single entity or a list of - entities.""" - resolve_context: Optional[bool] = True - """If True, remote context URLs are resolved.""" - mode: Optional[OSW.JsonLdMode] = "expand" - """The JSON-LD processing mode to apply if resolve_context is True.""" - context: Optional[Union[str, list, Dict[str, Any]]] = None - """The JSON-LD context to apply. Replaces any existing context.""" - additional_context: Optional[Union[str, list, Dict[str, Any]]] = None - """The JSON-LD context to apply on top of the existing context.""" - frame: Optional[Dict[str, Any]] = None - """The JSON-LD frame to use for framed mode. If not set, the existing context is used""" - build_rdf_graph: Optional[bool] = False - """If True, the output is a graph.""" - debug: Optional[bool] = False - - def __init__(self, **data): - super().__init__(**data) - if not isinstance(self.entities, list): - self.entities = [self.entities] - - class ExportJsonLdResult(OswBaseModel): - documents: List[Union[Dict[str, Any]]] - """A single JSON-LD document per entity""" - graph_document: Dict[str, Any] = None - """A single JSON-LD document with a @graph element containing all entities""" - graph: rdflib.Graph = None - """RDF graph containing all entities. Build only if build_rdf_graph is True""" - - class Config: - arbitrary_types_allowed = True - - def export_jsonld(self, params: ExportJsonLdParams) -> ExportJsonLdResult: - """Exports the given entity/entities as JSON-LD.""" - - if params.resolve_context: - jsonld.set_document_loader( - self.site.get_jsonld_context_loader(params.context_loader_config) - ) - - documents = [] - graph_document = {"@graph": []} - graph = None - if params.build_rdf_graph: - graph = rdflib.Graph() - prefixes = self.site.get_prefix_dict() - for prefix in prefixes: - graph.bind(prefix, prefixes[prefix]) - - for e in params.entities: - data = json.loads(e.json(exclude_none=True, indent=4, ensure_ascii=False)) - - data["@context"] = [] - if params.context is None: - for t in e.type: - data["@context"].append("/wiki/" + t) - if params.context is not None: - data["@context"].append(params.context) - else: - data["@context"] = { - **self.site.get_jsonld_context_prefixes(), - **params.context, - } - if params.additional_context is not None: - if data["@context"] is None: - data["@context"] = [] - elif not isinstance(data["@context"], list): - data["@context"] = [data["@context"]] - data["@context"].append(params.additional_context) - - data["@id"] = get_full_title(e) - - if params.resolve_context: - graph_document["@graph"].append(jsonld.expand(data)) - if params.mode == "expand": - data = jsonld.expand(data) - if isinstance(data, list): - data = data[0] - elif params.mode == "flatten": - data = jsonld.flatten(data) - elif params.mode == "compact": - # data = jsonld.expand(data) - # if isinstance(data, list): data = data[0] - data = jsonld.compact( - data, - data["@context"] if params.context is None else params.context, - ) - elif params.mode == "frame": - data = jsonld.frame( - data, - ( - {"@context": data["@context"]} - if params.frame is None - else params.frame - ), - ) - - if params.build_rdf_graph: - graph.parse(data=json.dumps(data), format="json-ld") - - documents.append(data) - - result = OSW.ExportJsonLdResult( - documents=documents, graph_document=graph_document, graph=graph - ) - return result - OSW._ApplyOverwriteParam.update_forward_refs() OSW.StoreEntityParam.update_forward_refs() -OSW.ExportJsonLdParams.update_forward_refs() diff --git a/src/osw/express.py b/src/osw/express.py index ede0ee6b..de100057 100644 --- a/src/osw/express.py +++ b/src/osw/express.py @@ -7,6 +7,7 @@ import re from io import TextIOWrapper from pathlib import Path +from uuid import uuid4 from warnings import warn from typing_extensions import ( @@ -300,6 +301,7 @@ def upload_file( label: Optional[List[model.Label]] = None, name: Optional[str] = None, description: Optional[List[model.Description]] = None, + change_id: Optional[str] = None, **properties: Dict[str, Any], ) -> "UploadFileResult": """Upload a file to an OSL page. @@ -327,6 +329,9 @@ def upload_file( description The description to set on the WikiFile data model prior to uploading it to the OSL instance. + change_id + The id of the change to use for the upload. If None, a new change id will be + generated. properties The properties to set on the WikiFile data model prior to uploading it to the OSL instance. Properties listed here, won't overwrite properties handed @@ -781,7 +786,7 @@ class UploadFileResult(FileResult, WikiFileController): overwrite: OVERWRITE_CLASS_OPTIONS = OverwriteOptions.true """If True, the file will be overwritten if it already exists. If False, the file will not be uploaded if it already exists. See osw.core for more information.""" - change_id: Optional[List[str]] = None + change_id: Optional[str] = None """The change ID of the WikiFile page to upload the file to, stored in the meta property.""" @@ -870,15 +875,24 @@ def __init__( domain=data.get("domain"), cred_mngr=data.get("cred_mngr"), ) + # If no change_id is given, generate a new one + if data.get("change_id") is None: + data["change_id"] = str(uuid4()) + # Change_id will be returned in the UploadFileResult.change_id attribute # If given set titel and namespace if data.get("target_fpt") is not None: namespace = data.get("target_fpt").split(":")[0] title = data.get("target_fpt").split(":")[-1] wiki_page = model.WikiPage(namespace=namespace, title=title) data["meta"] = model.Meta(wiki_page=wiki_page) - if data.get("change_id") is not None: - data["meta"].change_id = data.get("change_id") data["title"] = title + # Set change_id to existing meta + for meta in [data.get("meta"), getattr(data["source_file_controller"], "meta")]: + if meta is not None: + if getattr(meta, "change_id", None) is None: + meta.change_id = [data["change_id"]] + else: + meta.change_id.append(data.get("change_id")) # Clean data dict data = {key: value for key, value in data.items() if value is not None} # Create the WikiFileController from the source_file_controller @@ -908,6 +922,7 @@ def osw_upload_file( label: Optional[List[model.Label]] = None, name: Optional[str] = None, description: Optional[List[model.Description]] = None, + change_id: Optional[str] = None, **properties: Dict[str, Any], ) -> UploadFileResult: """Upload a file to an OSL page. @@ -949,6 +964,9 @@ def osw_upload_file( description The description to set on the WikiFile data model prior to uploading it to the OSL instance. + change_id + The change ID of the WikiFile page to upload the file to, stored in the meta + property. properties The properties to set on the WikiFile data model prior to uploading it to the OSL instance. Properties listed here, won't overwrite properties handed From a42fddded38da8bc61202fcea263482901f86902 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 18 Nov 2024 17:35:17 +0100 Subject: [PATCH 21/52] Resolving circular import due to identical naming of osw.types --- src/osw/auth.py | 2 +- src/osw/{types.py => custom_types.py} | 0 src/osw/model/static.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/osw/{types.py => custom_types.py} (100%) diff --git a/src/osw/auth.py b/src/osw/auth.py index cb0bcf33..420b6a13 100644 --- a/src/osw/auth.py +++ b/src/osw/auth.py @@ -9,8 +9,8 @@ import yaml from pydantic.v1 import PrivateAttr +from osw.custom_types import FilePath from osw.model.static import OswBaseModel -from osw.types import FilePath CREDENTIALS_FN_DEFAULT = "credentials.pwd.yaml" diff --git a/src/osw/types.py b/src/osw/custom_types.py similarity index 100% rename from src/osw/types.py rename to src/osw/custom_types.py diff --git a/src/osw/model/static.py b/src/osw/model/static.py index 4c6f59dc..02634ff8 100644 --- a/src/osw/model/static.py +++ b/src/osw/model/static.py @@ -7,7 +7,7 @@ from pydantic.v1 import BaseModel -from osw.types import NoneType +from osw.custom_types import NoneType T = TypeVar("T", bound=BaseModel) From ee3def9e6e9de0ff5236c9f88c18b96bcceee385 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 18 Nov 2024 17:36:06 +0100 Subject: [PATCH 22/52] Minor improvements to express functions --- src/osw/express.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/osw/express.py b/src/osw/express.py index de100057..67308eb8 100644 --- a/src/osw/express.py +++ b/src/osw/express.py @@ -27,6 +27,7 @@ from osw.auth import CREDENTIALS_FN_DEFAULT, CredentialManager from osw.core import OSW, OVERWRITE_CLASS_OPTIONS, OverwriteOptions from osw.model.static import OswBaseModel +from osw.utils.wiki import namespace_from_full_title, title_from_full_title from osw.wtsite import WtSite # Definition of constants @@ -667,13 +668,14 @@ def __init__(self, url_or_title, **data): data = {key: value for key, value in data.items() if value is not None} super().__init__(**data) # data includes "path" else: - if data.get("osw_express") is None: - data["osw_express"] = OswExpress( + osw_obj: OswExpress = data.get("osw_express") + if osw_obj is None: + osw_obj = OswExpress( domain=data.get("domain"), cred_mngr=data.get("cred_mngr"), ) title: str = "File:" + url_or_title.split("File:")[-1] - file = data.get("osw_express").load_entity(title) + file = osw_obj.load_entity(title) wf: WikiFileController = file.cast( WikiFileController, osw=data.get("osw_express") ) @@ -818,7 +820,11 @@ def __init__( data["source_file_controller"] = LocalFileController(path=data.get("path")) elif isinstance(source, IO): data["source_file_controller"] = InMemoryController(stream=source) - + else: + raise ValueError( + "The 'source' argument must be a LocalFileController, WikiFileController," + " str, Path or IO object." + ) # If url_or_title is given, it either # * contains a valid domain, which can be used in osw_express # * contains a full page title, which is the target page @@ -881,8 +887,8 @@ def __init__( # Change_id will be returned in the UploadFileResult.change_id attribute # If given set titel and namespace if data.get("target_fpt") is not None: - namespace = data.get("target_fpt").split(":")[0] - title = data.get("target_fpt").split(":")[-1] + namespace = namespace_from_full_title(data.get("target_fpt")) + title = title_from_full_title(data.get("target_fpt")) wiki_page = model.WikiPage(namespace=namespace, title=title) data["meta"] = model.Meta(wiki_page=wiki_page) data["title"] = title @@ -900,8 +906,8 @@ def __init__( other=data.get("source_file_controller"), osw=data.get("osw_express"), **data, - # Passes arguments to the cast() method, e.g. overwrite the label - # cast method will call init + # Passes arguments to the cast() method, e.g., overwrite the label + # cast method will call init ) # Upload to the target OSW instance wfc.put_from(data.get("source_file_controller"), **data) @@ -1011,4 +1017,3 @@ def osw_upload_file( # * Save a pandas.DataFrame to a WikiFile (as table, e.g. as csv, xlsx, # json) # * Save a wiki page as pdf -# * make upload function work with IO objects From b279ebf5c9d30be8965a0fd32c83d90519be76d2 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 18 Nov 2024 18:01:43 +0100 Subject: [PATCH 23/52] Small fix to osw.core.OSW.StoreEntityParam --- src/osw/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/osw/core.py b/src/osw/core.py index 22d79fb7..ec23ccd6 100644 --- a/src/osw/core.py +++ b/src/osw/core.py @@ -988,7 +988,7 @@ def __init__(self, **data): if self.change_id is None: self.change_id = str(uuid4()) for entity in self.entities: - if not hasattr(entity, "meta"): + if getattr(entity, "meta", None) is None: entity.meta = model.Meta() if entity.meta.change_id is None: entity.meta.change_id = [] From beffddfd5b0e471d844e0769da0747f2ee52109e Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 18 Nov 2024 18:23:28 +0100 Subject: [PATCH 24/52] Still using pydantic.v1 --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9021e961..a1ac1e0a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ [![Coveralls](https://img.shields.io/coveralls/github/OpenSemanticLab/osw-python/main.svg)](https://coveralls.io/r/OpenSemanticLab/osw) [![docs](xx.xx)](https://opensemanticlab.github.io/osw-python/) ![license](https://img.shields.io/github/license/OpenSemanticLab/osw-python.svg) -[![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://pydantic.dev) + [![Project generated with PyScaffold](https://img.shields.io/badge/-PyScaffold-005CA0?logo=pyscaffold)](https://pyscaffold.org/) # osw From 3f1d047d0c307167be9e2946a5eb4b33a7f6405d Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Tue, 19 Nov 2024 18:26:22 +0100 Subject: [PATCH 25/52] Fixes to QueryInstancesParam & some readability improvements --- src/osw/core.py | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/osw/core.py b/src/osw/core.py index ec23ccd6..f4d1c4d1 100644 --- a/src/osw/core.py +++ b/src/osw/core.py @@ -668,19 +668,19 @@ def load_entity( ) schemas.append(schema) # generate model if not already exists - cls = schema["title"] - if not hasattr(model, cls): + cls_name: str = schema["title"] + if not hasattr(model, cls_name): if param.autofetch_schema: self.fetch_schema( OSW.FetchSchemaParam( schema_title=category, mode="append" ) ) - if not hasattr(model, cls): + if not hasattr(model, cls_name): schemas_fetched = False print( - f"Error: Model {cls} not found. Schema {category} needs to " - f"be fetched first." + f"Error: Model {cls_name} not found. Schema {category} " + f"needs to be fetched first." ) if not schemas_fetched: continue @@ -689,7 +689,7 @@ def load_entity( print("Error: no schema defined") elif len(schemas) == 1: - cls = getattr(model, schemas[0]["title"]) + cls: Type[model.Entity] = getattr(model, schemas[0]["title"]) entity: model.Entity = cls(**jsondata) else: @@ -1216,28 +1216,28 @@ def delete_entity( if comment is not None: entity.comment = comment - def delete_entity_(entity, comment_: str = None): + def delete_entity_(entity_, comment_: str = None): """Deletes the given entity from the OSW instance. Parameters ---------- - entity: + entity_: The dataclass instance to delete comment_: Command for the change log, by default None """ title_ = None namespace_ = None - if hasattr(entity, "meta"): - if entity.meta and entity.meta.wiki_page: - if entity.meta.wiki_page.title: - title_ = entity.meta.wiki_page.title - if entity.meta.wiki_page.namespace: - namespace_ = entity.meta.wiki_page.namespace + if hasattr(entity_, "meta"): + if entity_.meta and entity_.meta.wiki_page: + if entity_.meta.wiki_page.title: + title_ = entity_.meta.wiki_page.title + if entity_.meta.wiki_page.namespace: + namespace_ = entity_.meta.wiki_page.namespace if namespace_ is None: - namespace_ = get_namespace(entity) + namespace_ = get_namespace(entity_) if title_ is None: - title_ = OSW.get_osw_id(entity.uuid) + title_ = OSW.get_osw_id(entity_.uuid) if namespace_ is None or title_ is None: print("Error: Unsupported entity type") return @@ -1263,7 +1263,9 @@ def delete_entity_(entity, comment_: str = None): _ = [delete_entity_(e, entity.comment) for e in entity.entities] class QueryInstancesParam(OswBaseModel): - categories: Union[Union[str, OswBaseModel], List[Union[str, OswBaseModel]]] + categories: Union[ + Union[str, Type[OswBaseModel]], List[Union[str, Type[OswBaseModel]]] + ] parallel: Optional[bool] = None debug: Optional[bool] = False limit: Optional[int] = 1000 @@ -1272,20 +1274,20 @@ class QueryInstancesParam(OswBaseModel): @staticmethod def get_full_page_name_parts( - category_: Union[str, OswBaseModel] + category_: Union[str, Type[OswBaseModel]] ) -> Dict[str, str]: error_msg = ( f"Category must be a string like 'Category:' or a " - f"dataclass instance with a 'type' attribute. This error occurred on " + f"dataclass subclass with a 'type' attribute. This error occurred on " f"'{str(category_)}'" ) if isinstance(category_, str): string_to_split = category_ - elif isinstance(category_, OswBaseModel): - type_ = getattr(category_, "type", None) - if type_ is None: + elif issubclass(category_, OswBaseModel): + type_ = category_.__fields__.get("type") + if getattr(type_, "default", None) is None: raise TypeError(error_msg) - string_to_split = type_[0] + string_to_split = type_.default[0] else: raise TypeError(error_msg) if "Category:" not in string_to_split: From d908a414d3d6b7d60bc9a4b82e8aadd440ce4d68 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Tue, 19 Nov 2024 18:27:05 +0100 Subject: [PATCH 26/52] Fixing validation error (introduced earlier) --- src/osw/express.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/osw/express.py b/src/osw/express.py index 67308eb8..a3b9b389 100644 --- a/src/osw/express.py +++ b/src/osw/express.py @@ -668,14 +668,13 @@ def __init__(self, url_or_title, **data): data = {key: value for key, value in data.items() if value is not None} super().__init__(**data) # data includes "path" else: - osw_obj: OswExpress = data.get("osw_express") - if osw_obj is None: - osw_obj = OswExpress( + if data.get("osw_express") is None: + data["osw_express"] = OswExpress( domain=data.get("domain"), cred_mngr=data.get("cred_mngr"), ) title: str = "File:" + url_or_title.split("File:")[-1] - file = osw_obj.load_entity(title) + file = data.get("osw_express").load_entity(title) wf: WikiFileController = file.cast( WikiFileController, osw=data.get("osw_express") ) From 81c51d29ee6b9873135e03bb75f1e87e050554fe Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Wed, 20 Nov 2024 11:14:28 +0100 Subject: [PATCH 27/52] Accompanying changes in the Category:Entity schema --- src/osw/model/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/osw/model/entity.py b/src/osw/model/entity.py index 68cb8f7c..0c73c09d 100644 --- a/src/osw/model/entity.py +++ b/src/osw/model/entity.py @@ -47,6 +47,7 @@ class WikiPage(OswBaseModel): class Meta(OswBaseModel): + uuid: UUID = Field(default_factory=uuid4, title="UUID") # todo: code generator? wiki_page: Optional[WikiPage] = Field(None, title="Wiki page") """ The wiki page containing this entity From 8d61b9ca9c3411ada01bc30f0e5eba1f6dff01f4 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Thu, 21 Nov 2024 17:43:41 +0100 Subject: [PATCH 28/52] Deleting empty strings from jsondata to avoid validation errors --- src/osw/core.py | 22 +++++++++++++++++++--- src/osw/utils/wiki.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/osw/core.py b/src/osw/core.py index f4d1c4d1..54a57c89 100644 --- a/src/osw/core.py +++ b/src/osw/core.py @@ -31,6 +31,7 @@ get_uuid, is_empty, namespace_from_full_title, + remove_empty_strings, title_from_full_title, ) from osw.wiki_tools import SearchParam @@ -604,6 +605,8 @@ class LoadEntityParam(BaseModel): autofetch_schema: Optional[bool] = True """If true, load the corresponding schemas / categories ad-hoc if not already present""" + remove_empty_strings: Optional[bool] = True + """If true, remove key with an empty string value from the jsondata.""" disable_cache: bool = False """If true, disable the cache for the loading process""" @@ -659,6 +662,8 @@ def load_entity( schemas = [] schemas_fetched = True jsondata = page.get_slot_content("jsondata") + if param.remove_empty_strings: + remove_empty_strings(jsondata) if jsondata: for category in jsondata["type"]: schema = ( @@ -700,7 +705,7 @@ def load_entity( entity: model.Entity = cls(**jsondata) if entity is not None: - # make sure we do not override existing meta data + # make sure we do not override existing metadata if not hasattr(entity, "meta") or entity.meta is None: entity.meta = model.Meta() if ( @@ -775,6 +780,7 @@ class _ApplyOverwriteParam(OswBaseModel): namespace: Optional[str] meta_category_title: Optional[str] meta_category_template_str: Optional[str] + remove_empty_strings: Optional[bool] = False inplace: Optional[bool] = False debug: Optional[bool] = False @@ -855,6 +861,8 @@ def set_content(content_to_set: dict) -> None: ): # Use pydantic serialization, skip none values: new_content["jsondata"] = json.loads(param.entity.json(exclude_none=True)) + if param.remove_empty_strings: + remove_empty_strings(new_content["jsondata"]) set_content(new_content) page.changed = True return page # Guard clause --> exit function @@ -969,6 +977,8 @@ class StoreEntityParam(OswBaseModel): """A list of OverwriteClassParam objects. If a class specific overwrite setting is set, this setting is used. """ + remove_empty_strings: Optional[bool] = True + """If true, remove key with an empty string value from the jsondata.""" change_id: Optional[str] = None """ID to document the change. Entities within the same store_entity() call will share the same change_id. This parameter can also be used to link multiple @@ -1087,14 +1097,18 @@ def store_entity_( namespace=namespace_, policy=overwrite_class_param, meta_category_template_str=meta_category_template_str, + remove_empty_strings=param.remove_empty_strings, debug=param.debug, ) ) if meta_category_template: try: + jsondata = page.get_slot_content("jsondata") + if param.remove_empty_strings: + remove_empty_strings(jsondata) schema_str = eval_compiled_handlebars_template( meta_category_template, - page.get_slot_content("jsondata"), + jsondata, { "_page_title": entity_title, # Legacy "_current_subject_": entity_title, @@ -1208,7 +1222,9 @@ def __init__(self, **data): self.parallel = False def delete_entity( - self, entity: Union[OswBaseModel, DeleteEntityParam], comment: str = None + self, + entity: Union[OswBaseModel, List[OswBaseModel], DeleteEntityParam], + comment: str = None, ): """Deletes the given entity/entities from the OSW instance.""" if not isinstance(entity, OSW.DeleteEntityParam): diff --git a/src/osw/utils/wiki.py b/src/osw/utils/wiki.py index 36410049..75287366 100644 --- a/src/osw/utils/wiki.py +++ b/src/osw/utils/wiki.py @@ -1,3 +1,4 @@ +from copy import deepcopy from uuid import UUID # Legacy imports: @@ -73,3 +74,33 @@ def is_empty(val): elif isinstance(val, list) or isinstance(val, str) or isinstance(val, dict): return len(val) == 0 return False + + +def remove_empty_strings(d: dict, inplace: bool = True) -> dict: + """Iterates through the dictionary structure and removes key-value pairs + where the value is an empty string + + Parameters + ---------- + d: + The dictionary to perform the operation on + inplace: + Whether to perform the operation in place or return a new dictionary + + Returns + ------- + result: + The modified dictionary + """ + if not inplace: + d = deepcopy(d) + for key, value in d.items(): + if isinstance(value, dict): + remove_empty_strings(value) + elif isinstance(value, list): + for item in value: + if isinstance(item, dict): + remove_empty_strings(item) + if value == "": + del d[key] + return d From 2af7054cdab25599a6090d3057d5be291d6d8ec3 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Thu, 21 Nov 2024 17:46:16 +0100 Subject: [PATCH 29/52] Added method to delete a list of pages --- src/osw/wtsite.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/osw/wtsite.py b/src/osw/wtsite.py index e3de2ce5..b62ecc6f 100644 --- a/src/osw/wtsite.py +++ b/src/osw/wtsite.py @@ -600,6 +600,65 @@ def copy_single_page(content_dict: dict): else: return [copy_single_page(content) for content in content_list] + class DeletePageParam(OswBaseModel): + page: Union["WtPage", List["WtPage"], str, List[str]] + comment: Optional[str] = None + debug: Optional[bool] = True + """If True, debug messages will be printed.""" + parallel: Optional[bool] = None + """If true, processes the pages in parallel.""" + + class Config: + arbitrary_types_allowed = True + + def __init__(self, **data): + super().__init__(**data) + if not isinstance(self.page, list): + self.page = [self.page] + if not self.comment: + self.comment = "Deleted via osw-python" + if len(self.page) > 5 and self.parallel is None: + self.parallel = True + + def delete_page( + self, + param: Union["WtPage", List["WtPage"], str, List[str], DeletePageParam], + comment: str = None, + ): + """Deletes a page or a list of pages from the site. + + Parameters + ---------- + param: + DeletePageParam object + comment: + The comment to leave when deleting the page + """ + if not isinstance(param, WtSite.DeletePageParam): + param = WtSite.DeletePageParam(page=param) + if comment: + param.comment = comment + pages = [] + for page in param.page: + if isinstance(page, str): + pages.append(WtPage(self, title=page, do_init=True)) + else: + pages.append(page) + param.page = pages + + def delete_single_page(page_: "WtPage", comment: str): + return page_.delete(comment=comment) + + if param.parallel: + return ut.parallelize( + delete_single_page, + param.page, + comment=param.comment, + flush_at_end=param.debug, + ) + else: + return [delete_single_page(page, param.comment) for page in param.page] + class CreatePagePackageParam(OswBaseModel): """Parameter object for create_page_package method.""" @@ -1536,7 +1595,7 @@ def delete(self, comment: str = None): """ self._page.delete(comment) - def move(self, new_title: str, comment: str = None, redirect=True): + def move(self, new_title: str, comment: str = None, redirect: bool = True): """Moves (=renames) the page to a new title Parameters @@ -1998,3 +2057,4 @@ def import_xml(self, config: ImportConfig) -> ImportResult: WtSite.CreatePagePackageParam.update_forward_refs() WtSite.UploadPagePackageParam.update_forward_refs() WtSite.ReadPagePackageResult.update_forward_refs() +WtSite.DeletePageParam.update_forward_refs() From 1b2bf1feeac432e1e49c3e0f5f21910df053a41f Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Thu, 21 Nov 2024 17:46:49 +0100 Subject: [PATCH 30/52] Fixing params.py: Enums can't be inherited, added new param object --- src/osw/params.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/osw/params.py b/src/osw/params.py index fef768f4..4130734b 100644 --- a/src/osw/params.py +++ b/src/osw/params.py @@ -1,5 +1,3 @@ -from typing_extensions import deprecated - from osw.auth import CredentialManager # noqa: F401 from osw.core import ( # noqa: F401 OSW, @@ -23,8 +21,9 @@ class UserPwdCredential(CredentialManager.UserPwdCredential): pass -class CredentialFallback(CredentialManager.CredentialFallback): - pass +# Enum +# class CredentialFallback(CredentialManager.CredentialFallback): +# pass class CredentialConfig(CredentialManager.CredentialConfig): @@ -40,8 +39,9 @@ class SchemaRegistration(OSW.SchemaRegistration): pass -class FetchSchemaMode(OSW.FetchSchemaMode): - pass +# Enum +# class FetchSchemaMode(OSW.FetchSchemaMode): +# pass class FetchSchemaParam(OSW.FetchSchemaParam): @@ -73,11 +73,6 @@ class WtSiteConfig(WtSite.WtSiteConfig): pass -@deprecated("Use WtSiteConfig instead") -class WtSiteLegacyConfig(WtSite.WtSiteLegacyConfig): - pass - - class GetPageParam(WtSite.GetPageParam): pass @@ -106,6 +101,10 @@ class UploadPagePackageParam(WtSite.UploadPagePackageParam): pass +class DeletePageParam(WtSite.DeletePageParam): + pass + + # From osw.wtsite.WtPage: class CopyPageConfig(WtPage.CopyPageConfig): pass From e22b0e4fbc1c8d645dec5a0b134aba8ac7109955 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Thu, 21 Nov 2024 17:48:16 +0100 Subject: [PATCH 31/52] Applying template for Entity.name & Introducing Meta.uuid --- src/osw/model/entity.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/osw/model/entity.py b/src/osw/model/entity.py index 0c73c09d..1b3a1275 100644 --- a/src/osw/model/entity.py +++ b/src/osw/model/entity.py @@ -10,6 +10,7 @@ from pydantic.v1 import Field, constr from osw.model.static import OswBaseModel +from osw.utils.strings import pascal_case class ReadAccess(OswBaseModel): @@ -47,7 +48,7 @@ class WikiPage(OswBaseModel): class Meta(OswBaseModel): - uuid: UUID = Field(default_factory=uuid4, title="UUID") # todo: code generator? + uuid: UUID = Field(default_factory=uuid4, title="UUID") wiki_page: Optional[WikiPage] = Field(None, title="Wiki page") """ The wiki page containing this entity @@ -101,6 +102,19 @@ class Entity(OswBaseModel): attachments: Optional[List[str]] = Field(None, title="File attachments") meta: Optional[Meta] = None + def __init__(self, **data): + if data.get("label"): + labels = [] + for label in data["label"]: + if isinstance(label, dict): + data["label"] = labels.append(Label(**label)) + else: + labels.append(Label(text=label)) + data["label"] = labels + if data.get("name") is None and "label" in data: + data["name"] = pascal_case(data["label"][0].text) + super().__init__(**data) + class ObjectStatement(OswBaseModel): rdf_type: Optional[Any] = "rdf:Statement" From 665a189f129337d2d5e48761e2ef61c1249aa038 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Thu, 21 Nov 2024 21:36:18 +0100 Subject: [PATCH 32/52] Merge pull request #93 from OpenSemanticLab/feat-add-rdf-generation Feat: add rdf generation # Conflicts: # src/osw/core.py --- src/osw/core.py | 120 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/src/osw/core.py b/src/osw/core.py index 54a57c89..eccbaaf5 100644 --- a/src/osw/core.py +++ b/src/osw/core.py @@ -1342,6 +1342,126 @@ def query_instances( full_page_titles = self.site.semantic_search(search_param) return full_page_titles + class JsonLdMode(str, Enum): + """enum for jsonld processing mode""" + + expand = "expand" + flatten = "flatten" + compact = "compact" + frame = "frame" + + class ExportJsonLdParams(OswBaseModel): + context_loader_config: Optional[WtSite.JsonLdContextLoaderParams] = None + """The configuration for the JSON-LD context loader.""" + entities: Union[OswBaseModel, List[OswBaseModel]] + """The entities to convert to JSON-LD. Can be a single entity or a list of + entities.""" + resolve_context: Optional[bool] = True + """If True, remote context URLs are resolved.""" + mode: Optional[OSW.JsonLdMode] = "expand" + """The JSON-LD processing mode to apply if resolve_context is True.""" + context: Optional[Union[str, list, Dict[str, Any]]] = None + """The JSON-LD context to apply. Replaces any existing context.""" + additional_context: Optional[Union[str, list, Dict[str, Any]]] = None + """The JSON-LD context to apply on top of the existing context.""" + frame: Optional[Dict[str, Any]] = None + """The JSON-LD frame to use for framed mode. If not set, the existing context is used""" + build_rdf_graph: Optional[bool] = False + """If True, the output is a graph.""" + debug: Optional[bool] = False + + def __init__(self, **data): + super().__init__(**data) + if not isinstance(self.entities, list): + self.entities = [self.entities] + + class ExportJsonLdResult(OswBaseModel): + documents: List[Union[Dict[str, Any]]] + """A single JSON-LD document per entity""" + graph_document: Dict[str, Any] = None + """A single JSON-LD document with a @graph element containing all entities""" + graph: rdflib.Graph = None + """RDF graph containing all entities. Build only if build_rdf_graph is True""" + + class Config: + arbitrary_types_allowed = True + + def export_jsonld(self, params: ExportJsonLdParams) -> ExportJsonLdResult: + """Exports the given entity/entities as JSON-LD.""" + + if params.resolve_context: + jsonld.set_document_loader( + self.site.get_jsonld_context_loader(params.context_loader_config) + ) + + documents = [] + graph_document = {"@graph": []} + graph = None + if params.build_rdf_graph: + graph = rdflib.Graph() + prefixes = self.site.get_prefix_dict() + for prefix in prefixes: + graph.bind(prefix, prefixes[prefix]) + + for e in params.entities: + data = json.loads(e.json(exclude_none=True, indent=4, ensure_ascii=False)) + + data["@context"] = [] + if params.context is None: + for t in e.type: + data["@context"].append("/wiki/" + t) + if params.context is not None: + data["@context"].append(params.context) + else: + data["@context"] = { + **self.site.get_jsonld_context_prefixes(), + **params.context, + } + if params.additional_context is not None: + if data["@context"] is None: + data["@context"] = [] + elif not isinstance(data["@context"], list): + data["@context"] = [data["@context"]] + data["@context"].append(params.additional_context) + + data["@id"] = get_full_title(e) + + if params.resolve_context: + graph_document["@graph"].append(jsonld.expand(data)) + if params.mode == "expand": + data = jsonld.expand(data) + if isinstance(data, list): + data = data[0] + elif params.mode == "flatten": + data = jsonld.flatten(data) + elif params.mode == "compact": + # data = jsonld.expand(data) + # if isinstance(data, list): data = data[0] + data = jsonld.compact( + data, + data["@context"] if params.context is None else params.context, + ) + elif params.mode == "frame": + data = jsonld.frame( + data, + ( + {"@context": data["@context"]} + if params.frame is None + else params.frame + ), + ) + + if params.build_rdf_graph: + graph.parse(data=json.dumps(data), format="json-ld") + + documents.append(data) + + result = OSW.ExportJsonLdResult( + documents=documents, graph_document=graph_document, graph=graph + ) + return result + OSW._ApplyOverwriteParam.update_forward_refs() OSW.StoreEntityParam.update_forward_refs() +OSW.ExportJsonLdParams.update_forward_refs() From c43477717da032e9e822d26a224ffbfa46b8a951 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Fri, 22 Nov 2024 10:15:38 +0100 Subject: [PATCH 33/52] Fixing: length of dictionary changed during iteration --- src/osw/utils/wiki.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/osw/utils/wiki.py b/src/osw/utils/wiki.py index 75287366..3590171a 100644 --- a/src/osw/utils/wiki.py +++ b/src/osw/utils/wiki.py @@ -94,7 +94,9 @@ def remove_empty_strings(d: dict, inplace: bool = True) -> dict: """ if not inplace: d = deepcopy(d) - for key, value in d.items(): + keys = list(d.keys()) + for key in keys: + value = d[key] if isinstance(value, dict): remove_empty_strings(value) elif isinstance(value, list): From f8959d2fa452ee51e0459fb40b06c258373973eb Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Fri, 22 Nov 2024 10:18:51 +0100 Subject: [PATCH 34/52] apply_overwrite_policy() should apply the removal of empty strings --- src/osw/core.py | 6 +++++- tests/test_osl.py | 27 ++++++++++++++++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/osw/core.py b/src/osw/core.py index eccbaaf5..db6d992f 100644 --- a/src/osw/core.py +++ b/src/osw/core.py @@ -780,7 +780,7 @@ class _ApplyOverwriteParam(OswBaseModel): namespace: Optional[str] meta_category_title: Optional[str] meta_category_template_str: Optional[str] - remove_empty_strings: Optional[bool] = False + remove_empty_strings: Optional[bool] = True inplace: Optional[bool] = False debug: Optional[bool] = False @@ -892,6 +892,8 @@ def set_content(content_to_set: dict) -> None: for slot in ["jsondata", "header", "footer"]: # SLOTS: remote_content[slot] = page.get_slot_content(slot) # Todo: remote content does not contain properties that are not set + if param.remove_empty_strings: + remove_empty_strings(remote_content["jsondata"]) if remote_content["header"]: # not None or {} or "" new_content["header"] = remote_content["header"] if remote_content["footer"]: @@ -902,6 +904,8 @@ def set_content(content_to_set: dict) -> None: # Properties that are not set in the local content will be set to None # We want those not to be listed as keys local_content["jsondata"] = json.loads(param.entity.json(exclude_none=True)) + if param.remove_empty_strings: + remove_empty_strings(local_content["jsondata"]) if param.debug: print(f"'local_content': {str(remote_content)}") # Apply the overwrite logic diff --git a/tests/test_osl.py b/tests/test_osl.py index 8aa6bed3..b5f826da 100644 --- a/tests/test_osl.py +++ b/tests/test_osl.py @@ -4,6 +4,7 @@ import osw.model.entity as model from osw.core import OSW, AddOverwriteClassOptions, OverwriteOptions +from osw.utils.wiki import remove_empty_strings from osw.wtsite import WtPage @@ -55,7 +56,8 @@ def check_false(original: model.Entity, altered: model.Entity, stored: model.Ent but add those additionally present in the altered entity.""" assert stored.label[0].text == original.label[0].text assert stored.name == original.name - assert stored.iri == original.iri + # empty string property is removed on store and load: + assert stored.iri == altered.iri if len(original.description) == 0: assert stored.description == original.description else: @@ -74,6 +76,7 @@ def check_only_empty( entity in the OSW that are empty, but are not empty in the altered entity.""" assert stored.label[0].text == original.label[0].text assert stored.name == original.name + # empty string property is removed on store and load: assert stored.iri == altered.iri assert stored.description[0].text == altered.description[0].text assert stored.query_label == altered.query_label @@ -102,7 +105,9 @@ def check_keep_existing( 'keep existing', which is supposed to keep the existing entity in the OSW.""" assert stored.label[0].text == original.label[0].text assert stored.name == original.name - assert stored.iri == original.iri + # assert stored.iri == original.iri + # empty string property is removed on store and load: + assert getattr(stored, "iri", None) is None assert stored.description == original.description # empty list assert stored.query_label == original.query_label assert stored.image == original.image @@ -135,7 +140,7 @@ def test_apply_overwrite_policy(): for check in checks: # Create a new item with some properties - original_item = model.Item( + original_item_local = model.Item( label=[model.Label(text="My Item")], name="MyItem", iri="", # Empty string property @@ -146,11 +151,19 @@ def test_apply_overwrite_policy(): original_page = OfflineWtPage( # wtSite=OSW.wt_site, # todo: missing title="Item:" - + OSW.get_osw_id(original_item.uuid) - ) - original_page.set_slot_content( - "jsondata", json.loads(original_item.json(exclude_none=True)) + + OSW.get_osw_id(original_item_local.uuid) ) + jsondata = json.loads(original_item_local.json(exclude_none=True)) + # Emulate the default setting for StoreEntityParam.remove_empty_strings, + # which is True and applied on store_entity + remove_empty_strings(jsondata) + + original_page.set_slot_content("jsondata", jsondata) + # To reproduce what happens in the OSL instance, we need to get the content + # from the page again + original_content = original_page.get_slot_content("jsondata") + # And create the original item from that content + original_item = model.Item(**original_content) # Alter some of the property values altered_props = { "label": [model.Label(text="My Item Duplicate")], From 0d9b87b90580192b545340bac1844377033e0898 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Fri, 22 Nov 2024 10:20:27 +0100 Subject: [PATCH 35/52] Fix: Validation error for Entity.label list elements of type Label were being passed to Label(), causing validation errors --- src/osw/model/entity.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/osw/model/entity.py b/src/osw/model/entity.py index 1b3a1275..28174478 100644 --- a/src/osw/model/entity.py +++ b/src/osw/model/entity.py @@ -104,13 +104,22 @@ class Entity(OswBaseModel): def __init__(self, **data): if data.get("label"): + if not isinstance(data["label"], list): + raise ValueError( + "label must be a list of Label objects", + ) labels = [] for label in data["label"]: if isinstance(label, dict): - data["label"] = labels.append(Label(**label)) + labels.append(Label(**label)) else: - labels.append(Label(text=label)) + # The list element should be a Label object + labels.append(label) data["label"] = labels + if not all(isinstance(label, Label) for label in data["label"]): + raise ValueError( + "label must be a list of Label objects", + ) if data.get("name") is None and "label" in data: data["name"] = pascal_case(data["label"][0].text) super().__init__(**data) From d316236459893ab65e2ce5a8202f8f61438abe19 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Wed, 16 Oct 2024 16:18:23 +0200 Subject: [PATCH 36/52] First tutorial draft --- docs/tutorials/basics.ipynb | 538 ++++++++++++++++++ docs/tutorials/img/osw_intro_data_model.png | Bin 0 -> 63641 bytes .../img/osw_intro_technology_stack.png | Bin 0 -> 202566 bytes 3 files changed, 538 insertions(+) create mode 100644 docs/tutorials/basics.ipynb create mode 100644 docs/tutorials/img/osw_intro_data_model.png create mode 100644 docs/tutorials/img/osw_intro_technology_stack.png diff --git a/docs/tutorials/basics.ipynb b/docs/tutorials/basics.ipynb new file mode 100644 index 00000000..4368bc42 --- /dev/null +++ b/docs/tutorials/basics.ipynb @@ -0,0 +1,538 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "47f7c6ba83e22f59", + "metadata": {}, + "source": [ + "# Tutorial: Basics\n", + "\n", + "This tutorial will cover the basics and prerequisites, required to use the osw-python library in Python and to \n", + "interact with an [Open Semantic Lab (OSL)](https://github.com/OpenSemanticLab) instance, like the [OpenSemanticWorld \n", + "Registry](https://opensemantic.world/). To do this tutorial interactively, jump to [Downloading the library](#Downloading-the-library-optional) and open this notebook in a Jupyter environment.\n", + "\n", + "- [OSL data model](#OSL-data-model)\n", + "- [Downloading the library (optional)](#Downloading-the-library-optional)\n", + "- [Installation](#Installation)\n", + "- [Connecting to an OSL instance](#Connecting-to-an-OSL-instance)\n", + "- [Downloading data model dependencies](#Downloading-data-model-dependencies)\n", + "- [Interact with an entity](#Interact-with-an-entity) \n", + " - Download an entity\n", + " - Modify the entity\n", + " - Upload an entity\n", + "- [Interact with files](#Interact-with-files)\n", + " - Download a file\n", + " - Read contents of a file\n", + " - Upload a file\n", + "- [Interface data sources](#Interface-data-sources)\n", + " - Tabular data: Excel, CSV --> pandas --> dict\n", + " - Database: SQL --> pyodbc --> dict\n", + " - Two options:\n", + " - Build a Helper class, inheriting from target dataclass and osw.data.import_utility.HelperModel\n", + " - Write a transformation function\n", + "- User workflows --> prefect" + ] + }, + { + "cell_type": "markdown", + "id": "64e0121de7a40c80", + "metadata": {}, + "source": [ + "## OSL data model\n", + "\n", + "Open Semantic Lab provides an [extension](https://github.com/OpenSemanticLab/mediawiki-extensions-OpenSemanticLab) \n", + "for Semantic Mediawiki, delivering a machine-readable data structure based on industry standards, like JSON, JSON-LD,\n", + " JSON-Schema. It allows to import, reference and interface existing (OWL, RDF) ontologies and aims to facilitate the \n", + " implementation of [FAIR Data principles](https://www.go-fair.org/fair-principles/) out-of-the-box.\n", + "\n", + "
\n", + " \n", + " \"Components\n", + "
\n", + "\n", + "JSON serves as the central data storage element for structured data, including the definition of classes and forms\n", + " via JSON-Schema, linking JSON-Data to ontologies and building property graphs.\n", + "\n", + "### Namespaces\n", + "\n", + "As we are using Semantic Mediawiki, the data is stored in pages, which are organized in namespaces. Full page titles \n", + "follow this structure: `:`. While the `` can contain `:`, it is rarely found. The \n", + "most important namespaces in OSL and stored entries are:\n", + "\n", + "- Category - Classes (instances of MetaClasses) and MetaClasses\n", + "- Item - Instances of classes\n", + "- Property - Semantic properties and reusable property schemas\n", + "- JsonSchema - Reusable JSON-Schema definitions\n", + "- Template - Templates for rendering pages or performing queries\n", + "\n", + "### Slots\n", + "\n", + "The data stored on a page in Semantic Mediawiki can be stored as plain text (main slot, content model: wikitext) or in\n", + " an arbitrary format in dedicated slots. In OSL, we go with nine slots, tailored to the needs of a data scientist, \n", + " around the JSON format. The most important slots are `jsondata` and `jsonschema`, which store the data and the schema:\n", + "\n", + "| Slot name | Content model | Description |\n", + "|-----------------|---------------|---------------------------------------------------------------------------------------------------------------------|\n", + "| main | wikitext | Default content slot, rendered between the header and footer of the page |\n", + "| jsondata | JSON | Structured data, (partially) used to render the infobox on the page |\n", + "| jsonschema | JSON | stored within a category (=class) page, defining the schema for the jsondata slot of any category member (instance) |\n", + "| header | wikitext | Content to be placed at the top of the page, below the heading |\n", + "| footer | wikitext | Content to be placed at the bottom of the page, above the (Semantic Mediawiki) built-in elements |\n", + "| header_template | wikitext | Stored within a category (=class) page, renders the page header of any category member (instance) |\n", + "| footer_template | wikitext | stored within a category (=class) page, renders the page footer of any category member (instance) |\n", + " \n", + "This data structure can be used to generate Python data classes, which can be used to interact with the data in a type-safe manner. The osw-python library includes a [code generator](https://github.com/koxudaxi/datamodel-code-generator/) to generate Python data classes from the JSON schema. \n", + "\n", + "At the same time, this data structure can be used to auto-generate form editors, create property graphs, and provide \n", + "data and interfaces for applications, such as Machine Learning and data processing.\n", + "\n", + "### Data Classes / Class Hierarchy\n", + "\n", + "Everything is considered an 'Entity', which is analogous to the 'object' in Python. 'Classes' are subclasses and \n", + "instances of 'Entity' or specific 'MetaClasses'. 'MetaClasses' define a JSON schema used to validate the structured \n", + "data stored in the jsondata slot of 'Classes', just as 'Classes' do for individual 'Instances' or 'Items'.\n", + "\n", + "
\n", + " \n", + " \"OSL\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "6bef0569b5824ea0", + "metadata": {}, + "source": [ + "### JSON / JSON-Schema\n", + "\n", + "The JSON schema stored in the `jsonschema` slot of a Category (=class) defines the structure of the data stored in \n", + "the `jsondata` slot of members of this category (=items). The JSON schema is a JSON object that defines the \n", + "properties and their types, constraints, and relationships. The JSON schema can be generated from the data stored \n", + "in the `jsondata` slot of the category (=class) or can be created manually. We are using the \n", + "[JSON-Schema](https://json-schema.org/) standard to define the schema. \n", + "\n", + "Through their ensured consistency, JSON can be used to generate Python data classes and instances, which can be used \n", + "as parameter objects for functions and methods. The generated classes are based on Pydantic models, which provide validation and serialization capabilities.\n", + "\n", + "#### JSON-Schema to Python Data Classes\n", + "\n", + "**Category:MyCategory `jsonschema` slot:**\n", + "```json\n", + "{\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"text\": { \"type\": \"string\" },\n", + " \"number\": { \"type\": \"number\" },\n", + " \"array\": { \"type\": \"array\" }\n", + " }\n", + "}\n", + "```\n", + "**Category:MySubCategory `jsonschema` slot:**\n", + "```json\n", + "{\n", + " \"type\": \"object\",\n", + " \"allOf\": \"/wiki/Category:MyCategory?action=raw&slot=jsonschema\",\n", + " \"properties\": {\n", + " \"additional_property\": { \"type\": \"string\" }\n", + " }\n", + "}\n", + "```\n", + "**Generated Python data classes:**\n", + "```python\n", + "from osw.model.entity import Entity\n", + "\n", + "class MyClass(Entity):\n", + " text: str\n", + " number: float\n", + " array: List[Any]\n", + " \n", + "class MySubClass(MyClass):\n", + " additional_property: str\n", + "```\n", + "\n", + "#### Python instance to JSON data\n", + "\n", + "```python\n", + "from osw.express import OswExpress\n", + "\n", + "osw_obj = OswExpress(domain=\"wiki-dev.open-semantic-lab.org\")\n", + "\n", + "my_instance = MySubClass(\n", + " text=\"some text\",\n", + " number=1.1,\n", + " array=[1, \"two\", 3.0],\n", + " additional_property = \"test2\",\n", + ")\n", + "my_instance.json()\n", + "my_instance = osw_obj.store_entity(my_instance) # wiki upload\n", + "```\n", + "\n", + "### Object Oriented Linked Data (OO-LD)\n", + "\n", + "The example above [JSON / JSON Schema](#JSON-/-JSON-Schema) already showed the integration of Object Oriented \n", + "Programming (OOP) into JSON and JSON Schema. Adding the linked data component of [JSON-LD](https://json-ld.org/) \n", + "enables the reusable annotation of datasets with well established vocabularies (ontologies), such as [schema.org] \n", + "(https://schema.org/). Annotation have to be made at Category (=class) level only, and are available on export of \n", + "instances. This makes the datasets machine-readable, allows for the integration of the data into the \n", + "[Semantic Web](https://en.wikipedia.org/wiki/Semantic_Web) and the creation of property graphs. \n", + "\n", + "#### A minimal example:\n", + "```json\n", + "{\n", + " \"@context\": {\n", + " \"schema\": \"https://schema.org/\",\n", + " \"name\": \"schema:name\"\n", + " },\n", + " \"title\": \"Person\",\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"name\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"First and Last name\"\n", + " }\n", + " }\n", + "}\n", + "```\n", + "\n", + "### Further reading\n", + "\n", + "- [OSW Introduction](https://opensemantic.world/wiki/Item:OSWdb485a954a88465287b341d2897a84d6)\n", + "- [OSW Python Package](https://opensemantic.world/wiki/Item:OSW659a81662ff44af1b2b6febeee7c3a25)\n", + "- [JSON Tutorial](https://opensemantic.world/wiki/Item:OSWf1df064239044b8fa3c968339fb93344)\n", + "- [JSON-Schema Tutorial](https://opensemantic.world/wiki/Item:OSWf4a9514baed04859a4c6c374a7312f10)\n", + "- [JSON-LD Tutorial](https://opensemantic.world/wiki/Item:OSW911488771ea449a6a34051f8213d7f2f)\n", + "- [OO-LD Tutorial](https://opensemantic.world/wiki/Item:OSWee501c0fa6a9407d99c058b5ff9d55b4)" + ] + }, + { + "cell_type": "markdown", + "id": "6e259e4f34e709ea", + "metadata": {}, + "source": [ + "## Downloading the library (optional)\n", + "\n", + "The osw-python library is available as GitHub repository and can be downloaded as a ZIP file or via git:\n", + "\n", + "```bash\n", + "git clone https://github.com/OpenSemanticLab/osw-python.git \n", + "```\n", + "\n", + "## Installation\n", + "\n", + "### From PyPI\n", + "\n", + "Preferably, you can install the library from the Python Package Index (PyPI) via pip, which is recommended for most users:\n", + "\n", + "```bash\n", + "conda activate # optional\n", + "pip install osw-python\n", + "```\n", + "\n", + "### From source\n", + "\n", + "If you want to install the library from source, you can clone the repository and install it via pip. The option `-e`\n", + " installs the package in editable mode, which means that the source code is linked to the installed package. This is \n", + " useful for development and testing.\n", + "\n", + "```bash\n", + "git clone https://github.com/OpenSemanticLab/osw-python.git \n", + "cd \n", + "conda activate # optional\n", + "pip install [-e] .\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "c089a8c6224ff1be", + "metadata": {}, + "source": [ + "## Connecting to an OSL instance\n", + "\n", + "To connect to an OSL instance, you need to provide your login credentials. You can either provide your username and \n", + "password directly or create a bot password. The bot is preferred because its edit rights can be restricted and at the \n", + "same time, edits made programmatically are traceable, being marked as bot edits.\n", + "\n", + "### Creating a bot password\n", + "\n", + "- Log in to your OSL instance\n", + "- Navigate to **Special:BotPasswords**, via **Toggle menu → Special pages → Bot passwords**,\n", + " e.g., `https:///wiki/Special:BotPasswords`, \n", + "- You must log in again to verify your identity\n", + "- Create a new bot password by providing a `Bot name`, e.g., 'PythoBot' and click **Create**\n", + "- Save the `Username` and `Bot password` in a safe place, as the password will not be displayed again\n", + "\n", + "### (Optional) Creating a credentials file\n", + "\n", + "You can create a YAML file, e.g., 'credentials.pwd.yaml', with your login credentials, which can be used to connect to the OSL instance. The file must follow the structure below:\n", + "\n", + "```yaml\n", + " :\n", + " username: \n", + " password: \n", + "```\n", + "\n", + "### Connecting via osw-python\n", + "\n", + "It is recommended to use the `osw.express.OswExpress` class to connect to an OSL instance. The class provides a \n", + "number of convenience functions ontop of the underlying `osw.core.OSW`. \n", + "\n", + "On the first execution of the following cell you will be prompted to enter domain, username and password. The \n", + "credentials will be stored in a file named **credentials.pwd.yaml** in a subfolder **osw_files** of the current working \n", + "directory. In the current working directory, a **.gitignore** file will be created or updated to include the \n", + "credentials file. \n", + "\n", + "This step is required to download all dependencies (data models) of OswExpress from the OSL instance." + ] + }, + { + "cell_type": "code", + "id": "162b6208a105bbde", + "metadata": {}, + "source": [ + "from osw.express import OswExpress" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "98a021ab9cc1124f", + "metadata": {}, + "source": [ + "#### Option 1: Reuse the credentials file created in the previous step\n", + "\n", + "If you are still running in the same CWD, OswExpress will automatically find the credentials file.\n", + "\n", + "Else you will be prompted to enter your username and password." + ] + }, + { + "cell_type": "code", + "id": "54f082d780c3dd79", + "metadata": {}, + "source": [ + "osw_obj = OswExpress(domain=\"wiki-dev.open-semantic-lab.org\") # Replace with your OSL instance" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "c735e2f5c4f55c05", + "metadata": {}, + "source": [ + "#### Option 2: Provide a credentials file (path)\n", + "\n", + "If the file does not exist or the domain is not in the file, you will be prompted to enter your username and password.\n", + "Unknown domains will be appended to the file." + ] + }, + { + "cell_type": "code", + "id": "24810957b39bfd3a", + "metadata": {}, + "source": [ + "osw_obj = OswExpress(domain=\"wiki-dev.open-semantic-lab.org\", cred_filepath=\"credentials.pwd.yaml\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "9022d39338ed267f", + "metadata": {}, + "source": [ + "## Downloading data model dependencies\n", + "\n", + "Before we can upload entities or files, we need to download the required data models. The data models are stored in the \n", + "`jsonschema` slot of the respective categories (=classes) and are used to generate Python data classes. OswExpress \n", + "offers a convenience function to download all dependencies of a given category, that an item is an instance of. " + ] + }, + { + "cell_type": "markdown", + "id": "537cdcc821e4803e", + "metadata": {}, + "source": [ + "### Identify required data models\n", + "\n", + "All categories (=classes) are subcategories of the **Entity** category. The classes **Entity**, **Item** and concepts \n", + "required to provide typing for those classes are provided out-of-the-box within `osw.model.entity`, which imports \n", + "**OswBasemodel(pydantic.BaseModel)** from `osw.model.static`.\n", + "\n", + "To store structured information in an OSL instance, you need to find a fitting **Category**, to create pages \n", + "(in the **Item** or **Category** namespace) in. To explore the data model hierarchy, you can use the graph tool \n", + "provided under **Graph** on every page in the `Category` or `Item` namespace, following the `SubClassOf` property. \n", + "\n", + "A good alternativ is to consult the **Category tree** page and navigate through the collapsible tree. The page can \n", + "be found under \n", + "`https:///wiki/Special:CategoryTree?target=Category%3AEntity&mode=categories&namespaces=`. \n", + "\n", + "Save the `Machine compatible name` and `Full page title` of the category you want to work with in a dictionary. Note \n", + "that only the category, the farthest down a branch, with respect to the root category **Entity**, is required. All \n", + "other categories will be downloaded automatically.\n", + "\n", + "**Example category tree**:\n", + "```\n", + "Entity\n", + "├── Property\n", + "├── Statement\n", + "└── Item\n", + " ├── Person\n", + " | └── User\n", + " ├── Location\n", + " | ├── Site\n", + " | ├── Building\n", + " | ├── Floor\n", + " | └── Room\n", + " ├── CreativeWork\n", + " | ├── Article\n", + " | └── Tutorial\n", + " └── OrganizationalUnit\n", + " └── Organization\n", + "```\n", + "\n", + "> [!Note]\n", + "> \n", + "> If you find no category, ask your administrator to install page packages via the special page 'Special:Packages'. \n", + "> Page packages are maintained via [GitHub](https://github.com/OpenSemanticWorld-Packages/osw-package-maintenance)" + ] + }, + { + "cell_type": "code", + "id": "7381372e09b55c7a", + "metadata": {}, + "source": [ + "dependencies = {\n", + " \"Organization\": \"Category:OSW1969007d5acf40539642877659a02c23\", # Will fetch: Organization, OrganizationalUnit\n", + " \"Person\": \"Category:OSWd9aa0bca9b0040d8af6f5c091bf9eec7\", # Will fetch: Person\n", + " \"Room\": \"Category:OSWc5ed0ed1e33c4b31887c67af25a610c1\", # Will fetch: Room, Location, but not: Site, Building, Floor\n", + " \"Tutorial\": \"Category:OSW494f660e6a714a1a9681c517bbb975da\", # Will fetch: Tutorial, Article, CreativeWork\n", + "}" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "da879e15b57f0c3e", + "metadata": {}, + "source": [ + "> [!Note]\n", + "> \n", + "> Keys in this dictionary will eventually be used in the import statements, should therefore fit the auto generated \n", + "> class names, which are the same as the category's `Machine compatible name`!" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Install data models\n", + "\n", + "Data models (data classes generated in osw.model.entity) can not be imported in Python scripts and modules prior to \n", + "installation. Therefore, it is recommended to do this step either in a separate script, which is run before the main\n", + "script, or in the main script itself, before the import statements.\n", + "\n", + "#### Option 1: Install dependencies before import from osw.model.entity \n", + "\n", + "This option is recommended to put in a separate script, which is run before the main script." + ], + "id": "7a8684bb1b8779fd" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from typing import TYPE_CHECKING\n", + "\n", + "\n", + "# Will run everytime the script is executed:\n", + "osw_obj.install_dependencies(dependencies)\n", + "\n", + "\n", + "# Static code checker will note 'Module not found' before the installation:\n", + "if TYPE_CHECKING:\n", + " from osw.model.entity import Organization, Person, Room, Tutorial" + ], + "id": "39260a2e792deb47", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "eb01d61d5c5586ee", + "metadata": {}, + "source": [ + "#### Option 2: Use OswExpress comfort function for imports\n", + "\n", + "This option is recommended to put in the main script, before the first `from osw.model.entity import` statement." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from typing import TYPE_CHECKING\n", + "from osw.express import import_with_fallback\n", + "\n", + "\n", + "# Will fetch and install dependencies only if not already installed:\n", + "import_with_fallback(dependencies)\n", + "\n", + "\n", + "# Otherwise static code checker will note 'Module not found' before the installation:\n", + "if TYPE_CHECKING:\n", + " from osw.model.entity import Organization, Person, Room, Tutorial" + ], + "id": "a7c8b1233c5817b3", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Interact with an entity\n", + "\n" + ], + "id": "40dc1839eeb1e503" + }, + { + "metadata": {}, + "cell_type": "code", + "source": "from osw.model.entity import Organization, Person, Room, Tutorial", + "id": "fb10f35fd3927b6a", + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/tutorials/img/osw_intro_data_model.png b/docs/tutorials/img/osw_intro_data_model.png new file mode 100644 index 0000000000000000000000000000000000000000..81d52923fbdf5fd0ea5295cf9a24dc303be7ec8f GIT binary patch literal 63641 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

_m}C5%iX)?OZky&`jd-xNj3iMMG#kR9%~S@nHL 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

>kC%lZ=bqm2l$$ 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 L?6c-tddL0;W(Nd& literal 0 HcmV?d00001 diff --git a/docs/tutorials/img/osw_intro_technology_stack.png b/docs/tutorials/img/osw_intro_technology_stack.png new file mode 100644 index 0000000000000000000000000000000000000000..d63fc178b8c9b773245763f87b20e7d6f5141152 GIT binary patch literal 202566 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%0ATD{>sU7(QIht-deANfQlBXa27uI?SX09 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 literal 0 HcmV?d00001 From db0d59e1746ed93ae28772ae936714059a9af3bb Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Fri, 25 Oct 2024 10:22:13 +0200 Subject: [PATCH 37/52] Continued content creation --- docs/tutorials/basics.ipynb | 261 ++++++++++++++++++++++++++++++++---- 1 file changed, 234 insertions(+), 27 deletions(-) diff --git a/docs/tutorials/basics.ipynb b/docs/tutorials/basics.ipynb index 4368bc42..a93279d7 100644 --- a/docs/tutorials/basics.ipynb +++ b/docs/tutorials/basics.ipynb @@ -98,8 +98,8 @@ "
\n", " \n", " \"OSL\n", "
" ] @@ -175,7 +175,7 @@ "\n", "### Object Oriented Linked Data (OO-LD)\n", "\n", - "The example above [JSON / JSON Schema](#JSON-/-JSON-Schema) already showed the integration of Object Oriented \n", + "The example above [JSON / JSON Schema](#JSON-/-JSON-Schema) already showed the integration of Object-Oriented \n", "Programming (OOP) into JSON and JSON Schema. Adding the linked data component of [JSON-LD](https://json-ld.org/) \n", "enables the reusable annotation of datasets with well established vocabularies (ontologies), such as [schema.org] \n", "(https://schema.org/). Annotation have to be made at Category (=class) level only, and are available on export of \n", @@ -351,9 +351,15 @@ "source": [ "## Downloading data model dependencies\n", "\n", + "Loading entities from OSL fetches required data models by default. So if you just want to load, modify and upload one\n", + " (type of) entity, you can scip to [Downloading an entity](#Downloading-an-entity).\n", + "\n", "Before we can upload entities or files, we need to download the required data models. The data models are stored in the \n", "`jsonschema` slot of the respective categories (=classes) and are used to generate Python data classes. OswExpress \n", - "offers a convenience function to download all dependencies of a given category, that an item is an instance of. " + "offers a convenience function to download all dependencies of a given category, that an item is an instance of. \n", + "\n", + "> [!NOTE]\n", + "> It is important to execute this notebook with the same environment, where the data models are installed to!" ] }, { @@ -393,14 +399,13 @@ " | ├── Floor\n", " | └── Room\n", " ├── CreativeWork\n", - " | ├── Article\n", + " | └── Article\n", " | └── Tutorial\n", " └── OrganizationalUnit\n", " └── Organization\n", "```\n", "\n", - "> [!Note]\n", - "> \n", + "> [!NOTE]\n", "> If you find no category, ask your administrator to install page packages via the special page 'Special:Packages'. \n", "> Page packages are maintained via [GitHub](https://github.com/OpenSemanticWorld-Packages/osw-package-maintenance)" ] @@ -425,15 +430,15 @@ "id": "da879e15b57f0c3e", "metadata": {}, "source": [ - "> [!Note]\n", - "> \n", + "> [!NOTE]\n", "> Keys in this dictionary will eventually be used in the import statements, should therefore fit the auto generated \n", "> class names, which are the same as the category's `Machine compatible name`!" ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "7a8684bb1b8779fd", + "metadata": {}, "source": [ "### Install data models\n", "\n", @@ -444,25 +449,20 @@ "#### Option 1: Install dependencies before import from osw.model.entity \n", "\n", "This option is recommended to put in a separate script, which is run before the main script." - ], - "id": "7a8684bb1b8779fd" + ] }, { - "metadata": {}, "cell_type": "code", + "id": "39260a2e792deb47", + "metadata": {}, "source": [ - "from typing import TYPE_CHECKING\n", - "\n", - "\n", "# Will run everytime the script is executed:\n", "osw_obj.install_dependencies(dependencies)\n", "\n", "\n", "# Static code checker will note 'Module not found' before the installation:\n", - "if TYPE_CHECKING:\n", - " from osw.model.entity import Organization, Person, Room, Tutorial" + "from osw.model.entity import Organization, Person, Room, Tutorial" ], - "id": "39260a2e792deb47", "outputs": [], "execution_count": null }, @@ -477,8 +477,9 @@ ] }, { - "metadata": {}, "cell_type": "code", + "id": "a7c8b1233c5817b3", + "metadata": {}, "source": [ "from typing import TYPE_CHECKING\n", "from osw.express import import_with_fallback\n", @@ -492,26 +493,232 @@ "if TYPE_CHECKING:\n", " from osw.model.entity import Organization, Person, Room, Tutorial" ], - "id": "a7c8b1233c5817b3", "outputs": [], "execution_count": null }, { - "metadata": {}, "cell_type": "markdown", + "id": "40dc1839eeb1e503", + "metadata": {}, "source": [ "### Interact with an entity\n", - "\n" + "\n", + "Data classes created by the code generator are based on Pydantic models, which provide validation and serialization.\n", + "\n", + "#### Creating an entity\n", + "\n", + "To create an entity, you need to create an instance of the respective data class. The `__init__` method of the data \n", + "class expects keyword arguments for all fields. As per usual for Pydantic models, positional arguments are not \n", + "permitted and the input data is validated during initialization." + ] + }, + { + "cell_type": "code", + "id": "fb10f35fd3927b6a", + "metadata": {}, + "source": [ + "# Create a person\n", + "john = Person(\n", + " first_name=\"John\",\n", + " last_name=\"Doe\",\n", + " email=\"john.doe@example.com\"\n", + ")\n", + "# Should return two ValidationErrors:\n", + "# - surname: field required\n", + "# - email: value is not a valid set" ], - "id": "40dc1839eeb1e503" + "outputs": [], + "execution_count": null }, { + "cell_type": "markdown", + "id": "2beecba33c4647ce", "metadata": {}, + "source": [ + "Lets breakdown what happened here\n", + "\n", + "- During initialization, the Pydantic model validates the input data. The validation errors are raised as exceptions.\n", + "- The `surname` field is required, but it was not provided.\n", + "- The extra field `last_name` was provided, but it was not expected. By default, Pydantic models disregard extra \n", + "fields without warning.\n", + "- The `email` field is expected to be a list of strings, but a string was provided. " + ] + }, + { "cell_type": "code", - "source": "from osw.model.entity import Organization, Person, Room, Tutorial", - "id": "fb10f35fd3927b6a", + "id": "eaaf78110e18c493", + "metadata": {}, + "source": [ + "# Should run without validation errors\n", + "john = Person(\n", + " first_name=\"John\",\n", + " surname=\"Doe\",\n", + " email=[\"john.doe@example.com\"],\n", + ")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "9aa534b206d8b486", + "metadata": {}, + "source": [ + "Before storing the entity in the OSL instance, lets check at which full page title it will be stored. \n", + "The full page title is derived from the `uuid` and the `namespace` of the entity." + ] + }, + { + "cell_type": "code", + "id": "d861fde54918bc05", + "metadata": {}, + "source": [ + "from osw.utils.wiki import get_namespace, get_osw_id, get_full_title\n", + "\n", + "print(\"Namespace:\", get_namespace(john))\n", + "print(\"UUID:\", john.uuid)\n", + "print(\"OSW-ID:\", get_osw_id(john.uuid))\n", + "print(\"Full title:\", get_full_title(john))" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "f6dace23bf99c7f3", + "metadata": {}, + "source": [ + "#### Storing an entity\n", + "\n", + "We can now store this entity in the OSL instance. The `store_entity` method uploads the entity to the OSL instance. " + ] + }, + { + "cell_type": "code", + "id": "64e0678b77f00772", + "metadata": {}, + "source": [ + "osw_obj.store_entity(john)\n", + "# In this specific case equivalent to:\n", + "params = OswExpress.StoreEntityParam(\n", + " entities=[john],\n", + " namespace=get_namespace(john),\n", + " parallel=False,\n", + " overwrite=\"keep existing\",\n", + " overwrite_per_class=None,\n", + ") # All default values included\n", + "# osw_obj.store_entity(params)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "78f959815e0fe8f", + "metadata": {}, + "source": [ + "Like most methods and functions in the osw-python library, the `store_entity` takes only a single argument. Usually \n", + "either a specific object type or a dedicated params object is accepted. If an object of type other than the params \n", + "object is passed, it is usually tested for compatibility and put inside a params object, filling the other parameters \n", + "with default values. \n", + "\n", + "This ensures that the method signature is as simple as possible and at the same time allows full typing and validation \n", + "of input parameters that the method is easy to use. " + ] + }, + { + "cell_type": "markdown", + "id": "639db64348bbd905", + "metadata": {}, + "source": [ + "#### Downloading an entity\n", + "\n", + "To download an entity, you need to provide the full page title of the entity. The `download_entity` method downloads the\n", + "entity from the OSL instance and returns an instance of the respective data class. If the respective data class is \n", + "not already part of `osw.model.entity`, the data class is generated on-the-fly by default." + ] + }, + { + "cell_type": "code", + "id": "ea1da2ff6d96808a", + "metadata": {}, + "source": [ + "john2 = osw_obj.load_entity(get_full_title(john))" + ], "outputs": [], "execution_count": null + }, + { + "cell_type": "markdown", + "id": "fe620279252b6886", + "metadata": {}, + "source": [ + "Lets have a look at the attributes of the downloaded entity" + ] + }, + { + "cell_type": "code", + "id": "19d1c6767b2098b", + "metadata": {}, + "source": [ + "from pprint import pprint\n", + "pprint(john2.dict())" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "2ab58db05b3b004b", + "metadata": {}, + "source": [ + "Besides the attributes that we set (first_name, surname, email), the downloaded entity has additional attributes, \n", + "that are generated by default, either when the entity is initialized (uuid, meta.wiki_page.title) or loaded (None \n", + "valued attributes). \n", + "\n", + "Loading an entity from the OSL instance downloads the full `jsondata` slot and pass it to the \n", + "`__init__` method of the respective data class. Thereby, attributes not present in the `jsondata` slot are set to the\n", + " default value of the data class." + ] + }, + { + "cell_type": "markdown", + "id": "d140b974b54155ce", + "metadata": {}, + "source": [ + "#### Modifying an entity\n", + "\n", + "To modify an entity, you can change the attributes of the entity instance. The attributes can be accessed and modified \n", + "like any other attribute of a Python object." + ] + }, + { + "cell_type": "code", + "id": "c0511efb825acf10", + "metadata": {}, + "source": [ + "# Adding a new attribute\n", + "john2.middle_name = \"R.\"\n", + "# Changing an existing attribute (here an item in a list)\n", + "john2.email[0] = \"john.doe@gmx.de\"\n", + "\n", + "# Checking the made changes:\n", + "pprint(john2.dict())" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "f42c1600ed17a059", + "metadata": {}, + "source": [ + "#### Storing an altered entity\n", + "\n", + "Here the same applies as for [Storing an entity](#Storing-an-entity). BUT: overwriting entities is not possible with \n", + "default settings \"keep existing\". Therefore, you need to call the method `store_entity` passing a StoreEntityParam with \n", + "the attribute `overwrite` set to \"overwrite\".\n" + ] } ], "metadata": { @@ -530,7 +737,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.12.4" } }, "nbformat": 4, From 5846338eb477559e92e56c22428a795b48a312b8 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Wed, 6 Nov 2024 17:26:31 +0100 Subject: [PATCH 38/52] Content added and reworked --- docs/tutorials/basics.ipynb | 468 ++++++++++++++++++++++++++++++++++-- 1 file changed, 443 insertions(+), 25 deletions(-) diff --git a/docs/tutorials/basics.ipynb b/docs/tutorials/basics.ipynb index a93279d7..e44e826b 100644 --- a/docs/tutorials/basics.ipynb +++ b/docs/tutorials/basics.ipynb @@ -17,20 +17,8 @@ "- [Connecting to an OSL instance](#Connecting-to-an-OSL-instance)\n", "- [Downloading data model dependencies](#Downloading-data-model-dependencies)\n", "- [Interact with an entity](#Interact-with-an-entity) \n", - " - Download an entity\n", - " - Modify the entity\n", - " - Upload an entity\n", "- [Interact with files](#Interact-with-files)\n", - " - Download a file\n", - " - Read contents of a file\n", - " - Upload a file\n", - "- [Interface data sources](#Interface-data-sources)\n", - " - Tabular data: Excel, CSV --> pandas --> dict\n", - " - Database: SQL --> pyodbc --> dict\n", - " - Two options:\n", - " - Build a Helper class, inheriting from target dataclass and osw.data.import_utility.HelperModel\n", - " - Write a transformation function\n", - "- User workflows --> prefect" + "- [Interface data sources](#Interface-data-sources)" ] }, { @@ -295,31 +283,38 @@ "cell_type": "code", "id": "162b6208a105bbde", "metadata": {}, + "source": "from osw.express import OSW, OswExpress", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", "source": [ - "from osw.express import OswExpress" + "# Define the wiki_domain for later reuse:\n", + "wiki_domain = \"wiki-dev.open-semantic-lab.org\" # Replace with the domain of your OSL instance" ], + "id": "509040ca218924ed", "outputs": [], "execution_count": null }, { - "cell_type": "markdown", - "id": "98a021ab9cc1124f", "metadata": {}, + "cell_type": "markdown", "source": [ "#### Option 1: Reuse the credentials file created in the previous step\n", "\n", "If you are still running in the same CWD, OswExpress will automatically find the credentials file.\n", "\n", "Else you will be prompted to enter your username and password." - ] + ], + "id": "4728ef9d8456dc26" }, { "cell_type": "code", "id": "54f082d780c3dd79", "metadata": {}, - "source": [ - "osw_obj = OswExpress(domain=\"wiki-dev.open-semantic-lab.org\") # Replace with your OSL instance" - ], + "source": "osw_obj = OswExpress(domain=wiki_domain) ", "outputs": [], "execution_count": null }, @@ -338,9 +333,7 @@ "cell_type": "code", "id": "24810957b39bfd3a", "metadata": {}, - "source": [ - "osw_obj = OswExpress(domain=\"wiki-dev.open-semantic-lab.org\", cred_filepath=\"credentials.pwd.yaml\")" - ], + "source": "osw_obj = OswExpress(domain=wiki_domain, cred_filepath=\"credentials.pwd.yaml\")", "outputs": [], "execution_count": null }, @@ -699,8 +692,8 @@ "source": [ "# Adding a new attribute\n", "john2.middle_name = \"R.\"\n", - "# Changing an existing attribute (here an item in a list)\n", - "john2.email[0] = \"john.doe@gmx.de\"\n", + "# Changing an existing attribute \n", + "john2.email = {\"john.doe@gmx.de\"}\n", "\n", "# Checking the made changes:\n", "pprint(john2.dict())" @@ -719,6 +712,431 @@ "default settings \"keep existing\". Therefore, you need to call the method `store_entity` passing a StoreEntityParam with \n", "the attribute `overwrite` set to \"overwrite\".\n" ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Option 1: Overwrite all entities\n", + "osw_obj.store_entity(OSW.StoreEntityParam(entities=[john2], overwrite=True))" + ], + "id": "77b507f13be1235b", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Option 2: Overwrite only entities of type Person\n", + "osw_obj.store_entity(OSW.StoreEntityParam(\n", + " entities=[john2], overwrite_per_class=[OSW.OverwriteClassParam(model=Person, overwrite=True)]))" + ], + "id": "19d4d48e95e0fe82", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Option 3: Overwrite only the email attribute of entities of type Person\n", + "osw_obj.store_entity(OSW.StoreEntityParam(\n", + " entities=[john2], overwrite_per_class=[\n", + " OSW.OverwriteClassParam(model=Person, overwrite=False,per_property={\"email\": True})]))" + ], + "id": "4fc037a10f95356a", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Here all three options will have the same result, but in many cases the result will differ, especially if you have \n", + "entities of different classes in the list of entities to store.\n", + "\n", + "The param `overwrite` is applied to all entities handed to the method regardless of type / class. It is also possible\n", + " to specify the overwrite behavior per class, by providing a list of `OSW.OverwriteClassParam`s. Those can even be \n", + " specific down to the property level. \n", + "- Available options for `OSW.StoreEntitParam.overwrite`, `OSW.OverwriteClassParam.overwrite` (per class) and `OSW\n", + ".OverwriteClassParam.per_property` are: \n", + " - `OSW.OverwriteOptions.true`: True - overwrite the remote entity or property with the local one\n", + " - `OSW.OverwriteOptions.false`: False - do not overwrite the remote entity or property with the local one\n", + " - `OSW.OverwriteOptions.only_empty`: \"only empty\" - overwrite the remote entity or property with the local one, \n", + " if the remote entity or property is empty\n", + "- Only available to `OSW.StoreEntitParam.overwrite` and `OSW.OverwriteClassParam.overwrite` (per class) are:\n", + " - `OSW.AddOverwriteClassOptions.replace_remote`: \"replace remote\" - replace the remote entity with the local one and\n", + " removes all properties not present in the local entity\n", + " - `OSW.AddOverwriteClassOptions.keep_existing`: \"keep existing\" - keep the remote entity, if one exists under this \n", + " OSW-ID " + ], + "id": "133a7c6940f23ae6" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Interact with files \n", + "\n", + "#### Download a file\n", + "\n", + "Let's say you have already uploaded a file to the instance of OSL you are connected to and have the URL to the file \n", + "available. (Execute the Upload a file section to upload a file to the OSL instance.) You can download the file with \n", + "just two lines:\n" + ], + "id": "2dc425de23d5e2ce" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from osw.express import osw_download_file\n", + "local_file = osw_download_file(\n", + " \"https://wiki-dev.open-semantic-lab.org/wiki/File:OSWaa635a571dfb4aa682e43b98937f5dd3.pdf\"\n", + " # , use_cached=True # Can be used to download the file only once, e.g., when developing code\n", + " # , overwrite=True # Can be used to avoid overwriting an existing file\n", + ")" + ], + "id": "9188d8aac00ef04", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "The object `local_file` is an instance of `OswExpress.DownloadFileResult` and contains the path to the downloaded file, \n", + "which is accessible via:" + ], + "id": "59162da197057ee8" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "The class `OswExpress.DownloadFileResult` implements all dunder methods required for a context manager. Therefore, it\n", + " can be used with the `with` statement to ensure the file is closed properly after use:" + ], + "id": "5215e1b89b4ba89f" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "with osw_download_file(\n", + " \"https://wiki-dev.open-semantic-lab.org/wiki/File:OSWac9224e1a280449dba71d945b1581d57.txt\", overwrite=True\n", + ") as file:\n", + " print(file.read())" + ], + "id": "ff674c1413ca6b4b", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "#### Round-Robin\n", + "\n", + "Let's create a file, upload it to the OSL instance, download it and read and alter its content, before uploading it \n", + "again.\n" + ], + "id": "418b47e58febe97d" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from pathlib import Path\n", + "from osw.express import osw_upload_file, osw_download_file\n", + "\n", + "# Create a file\n", + "fp = Path(\"example.txt\")\n", + "with open(fp, \"w\") as file:\n", + " file.write(\"Hello, World!\")\n", + "# Upload a file to an OSW instance\n", + "wiki_file = osw_upload_file(fp, domain=wiki_domain)\n", + "# Delete the local file\n", + "fp.unlink()\n", + "\n", + "# Download the file\n", + "local_file = osw_download_file(wiki_file.url, mode=\"r+\") # mode=\"r+\" to read and write\n", + "\n", + "with local_file as file:\n", + " content = file.read()\n", + " print(\"Original content:\")\n", + " print(content)\n", + " content = content.replace(\"World\", \"OSW\")\n", + " print(\"\\nModified content:\")\n", + " print(content)\n", + " # Write the modified content back to the file\n", + " file.write(content)\n", + "\n", + "# Upload the modified file\n", + "modified_wiki_file = osw_upload_file(local_file)" + ], + "id": "14a0ee12dde2961e", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Delete WikiFile from OSW instance after you are done with it\n", + "wiki_file.delete()" + ], + "id": "9647b490f90d2a0d", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Interface data sources\n", + "\n", + "#### Tabular data: Excel, CSV and others\n", + "\n", + "Let's create a demo table and save it to an Excel file. " + ], + "id": "2095cc63bd256a2c" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import pandas as pd\n", + "\n", + "df = pd.DataFrame(\n", + " {\n", + " \"FirstName\": [\"John\", \"Jane\", \"Alice\"],\n", + " \"LastName\": [\"Doe\", \"Do\", \"Dont\"],\n", + " \"Email\": [\"john.doe@example.com\", \"jane.do@example.com\", \"alice.dont@example.com\"],\n", + " }\n", + ")\n", + "df.to_excel(\"demo.xlsx\", index=False)\n", + "del df" + ], + "id": "64154efbfd92f87", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from pprint import pprint\n", + "# Let's read in our example Excel file\n", + "data_df = pd.read_excel(\"demo.xlsx\")\n", + "# Pandas dict representation is optimal for converting to JSON\n", + "# Let's have a look at the first row\n", + "john_dict = data_df.iloc[0].to_dict()\n", + "pprint(john_dict)" + ], + "id": "ed6cd2984e10baca", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Let's convert the dict to a Person instance\n", + "from osw.model.entity import Person\n", + "john = Person(**john_dict)\n", + "# This will cause ValidationError(s):\n", + "# - first_name: field required\n", + "# - surname: field required" + ], + "id": "840449d1e08ea7f2", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Explanation: This is due to the dictionary unpacking operator `**`, which passes the dictionary keys as keyword\n", + "arguments to the `Person` class. The `first_name` and `surname` fields are required, but they are not present in the\n", + "dictionary. \n", + "\n", + "We have several options to resolve this issue:\n", + "- Replace the dictionary keys with fitting ones\n", + " - By renaming the columns in the DataFrame before converting it to a dictionary\n", + " - By providing a mapping dictionary and replacing the keys in the dictionary, using the following function:\n", + " ```python\n", + " def replace_keys(d, key_map): \n", + " return {key_map.get(k, k): v for k, v in d.items()}\n", + " ```\n", + "- Create a HelperClass, which inherits from the target data class and `osw.data.import_utility.HelperModel`, and \n", + " implements a transformation function to create the target data class instance from the dictionary " + ], + "id": "de2cd483287d86a9" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "##### Option 1: Rename columns in the DataFrame\n", + "\n", + "This option is very simple and will serve you well for cases of low complexity, where the datatypes in the DataFrame \n", + "columns already match the datatypes of the target data class." + ], + "id": "d98830f43fd2a405" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Let's print out the columns first\n", + "print(data_df.columns)" + ], + "id": "88760a60e93fc99e", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Let's create the mapping dictionary and rename the columns\n", + "mapping = {\n", + " \"FirstName\": \"first_name\",\n", + " \"LastName\": \"surname\",\n", + " \"Email\": \"email\"\n", + "}\n", + "data_df.rename(columns=mapping, inplace=True)\n", + "# Let's have a look at the first row\n", + "john_dict = data_df.iloc[0].to_dict()\n", + "print(john_dict)" + ], + "id": "90f7efe362d1fd52", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Let's construct an instance of the Person data model\n", + "john = Person(**john_dict)\n", + "# This will cause a ValidationError:\n", + "# - email: value is not a valid set" + ], + "id": "469e3f2514a16e50", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Let's correct the email field\n", + "john_dict[\"email\"] = [john_dict[\"email\"]]\n", + "john = Person(**john_dict)\n", + "pprint(john.dict())" + ], + "id": "9e5c3bc9ff17b2e4", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "##### Option 2: Create a HelperModel and a transformation function\n", + "\n", + "This approach will be able to treat even cases of high complexity, where the datatypes in the DataFrame columns do not \n", + "match the datatypes of the target data class or where references to other instances in a dataset have to be made. \n", + "\n", + "[!NOTE] Property and variable names in Python must not contain spaces, so the column names in the DataFrame have \n", + "to be transformed accordingly." + ], + "id": "7401bbbb658bc631" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from typing import Any\n", + "from osw.data.import_utility import HelperModel\n", + "\n", + "class PersonHelper(Person, HelperModel):\n", + " FirstName: Any\n", + " LastName: Any\n", + " Email: Any\n", + " \n", + " def transform_attributes(self, dd: dict = None) -> bool:\n", + " super().transform_attributes()\n", + " self.first_name = self.FirstName\n", + " self.surname = self.LastName\n", + " self.email = {self.Email}\n", + " return True\n", + "\n", + "# Let's create a new instance of the PersonHelper class\n", + "data_df = pd.read_excel(\"demo.xlsx\")\n", + "john_dict = data_df.iloc[0].to_dict()\n", + "john_helper = PersonHelper(**john_dict)\n", + "print(\"Before transformation:\")\n", + "pprint(john_helper.dict())" + ], + "id": "ee7efd2374c3f87e", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Let's see the effect of the transformation\n", + "john_helper.transform_attributes()\n", + "print(\"After transformation:\")\n", + "pprint(john_helper.dict())" + ], + "id": "872b9e65d606d544", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Actually we access 'casted' directly\n", + "# If the transformation operations had not been performed already,\n", + "# accessing 'casted' would trigger them\n", + "john = john_helper.casted\n", + "print(\"After casting:\")\n", + "pprint(john.dict())" + ], + "id": "2ff76f2a76c097f6", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# We can do the same for all instances quite easily:\n", + "entities = []\n", + "for ii in data_df.index:\n", + " entities.append(PersonHelper(**data_df.iloc[ii].to_dict()).casted)" + ], + "id": "30e0620c9ef49fdf", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# And store the entities in the OSL instance\n", + "osw_obj.store_entity(entities)" + ], + "id": "15f2596aff7ac2e0", + "outputs": [], + "execution_count": null } ], "metadata": { From 66063165de7434e266d3fada9bbe759d3f82c3f1 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Thu, 7 Nov 2024 10:47:47 +0100 Subject: [PATCH 39/52] Renaming of casted -> transformed --- docs/tutorials/basics.ipynb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/basics.ipynb b/docs/tutorials/basics.ipynb index e44e826b..06351a2a 100644 --- a/docs/tutorials/basics.ipynb +++ b/docs/tutorials/basics.ipynb @@ -1064,6 +1064,7 @@ "from osw.data.import_utility import HelperModel\n", "\n", "class PersonHelper(Person, HelperModel):\n", + " # Attributes of the first base class are set to Optional[Any], default: None\n", " FirstName: Any\n", " LastName: Any\n", " Email: Any\n", @@ -1103,10 +1104,10 @@ "metadata": {}, "cell_type": "code", "source": [ - "# Actually we access 'casted' directly\n", + "# Actually we access 'transformed' directly\n", "# If the transformation operations had not been performed already,\n", - "# accessing 'casted' would trigger them\n", - "john = john_helper.casted\n", + "# accessing 'transformed' would trigger them\n", + "john = john_helper.transformed\n", "print(\"After casting:\")\n", "pprint(john.dict())" ], @@ -1121,7 +1122,7 @@ "# We can do the same for all instances quite easily:\n", "entities = []\n", "for ii in data_df.index:\n", - " entities.append(PersonHelper(**data_df.iloc[ii].to_dict()).casted)" + " entities.append(PersonHelper(**data_df.iloc[ii].to_dict()).transformed)" ], "id": "30e0620c9ef49fdf", "outputs": [], From 557866fe891c80ca78a47506923a6b8c3a1268ef Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Thu, 7 Nov 2024 14:44:46 +0100 Subject: [PATCH 40/52] README.md update to account for tutorial --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index a1ac1e0a..b4acc1af 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,10 @@ os.environ["PATH"] += os.pathsep + "/home/jovyan/.local/bin" https://opensemanticlab.github.io/osw-python/ +### Tutorials + +Tutorials can be found at [tutorials](docs/tutorials/) +- [Basics](docs/tutorials/basics.md) - a description of the data model of OpenSemanticLab and how to interact with it ## Development From 0ae37ca7968f5a2e4b2e8de7ebd9c2b3aa8682fe Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Fri, 15 Nov 2024 17:34:50 +0100 Subject: [PATCH 41/52] Rework tutorial notebook to incorporate tests and feedback, as well as latest fixes and improvements --- docs/tutorials/basics.ipynb | 138 ++++++++++++++++++++++++++---------- setup.cfg | 2 + 2 files changed, 101 insertions(+), 39 deletions(-) diff --git a/docs/tutorials/basics.ipynb b/docs/tutorials/basics.ipynb index 06351a2a..d0a62605 100644 --- a/docs/tutorials/basics.ipynb +++ b/docs/tutorials/basics.ipynb @@ -149,7 +149,7 @@ "```python\n", "from osw.express import OswExpress\n", "\n", - "osw_obj = OswExpress(domain=\"wiki-dev.open-semantic-lab.org\")\n", + "osw_obj = OswExpress(domain=\"demo.open-semantic-lab.org\")\n", "\n", "my_instance = MySubClass(\n", " text=\"some text\",\n", @@ -219,8 +219,10 @@ "\n", "```bash\n", "conda activate # optional\n", - "pip install osw-python\n", + "pip install osw-python[tutorial]\n", "```\n", + "The option `[tutorial]` installs the library with the dependencies required for data import, like `deepl`, `geopy`\n", + "and `openpyxl`.\n", "\n", "### From source\n", "\n", @@ -232,7 +234,7 @@ "git clone https://github.com/OpenSemanticLab/osw-python.git \n", "cd \n", "conda activate # optional\n", - "pip install [-e] .\n", + "pip install [-e] .[tutorial]\n", "```" ] }, @@ -292,7 +294,7 @@ "cell_type": "code", "source": [ "# Define the wiki_domain for later reuse:\n", - "wiki_domain = \"wiki-dev.open-semantic-lab.org\" # Replace with the domain of your OSL instance" + "wiki_domain = \"demo.open-semantic-lab.org\" # Replace with the domain of your OSL instance" ], "id": "509040ca218924ed", "outputs": [], @@ -452,7 +454,6 @@ "# Will run everytime the script is executed:\n", "osw_obj.install_dependencies(dependencies)\n", "\n", - "\n", "# Static code checker will note 'Module not found' before the installation:\n", "from osw.model.entity import Organization, Person, Room, Tutorial" ], @@ -477,14 +478,12 @@ "from typing import TYPE_CHECKING\n", "from osw.express import import_with_fallback\n", "\n", - "\n", "# Will fetch and install dependencies only if not already installed:\n", - "import_with_fallback(dependencies)\n", - "\n", + "import_with_fallback(dependencies, globals(), domain=wiki_domain)\n", "\n", "# Otherwise static code checker will note 'Module not found' before the installation:\n", "if TYPE_CHECKING:\n", - " from osw.model.entity import Organization, Person, Room, Tutorial" + " from osw.model.entity import Description, Label, Organization, Person, Room, Tutorial" ], "outputs": [], "execution_count": null @@ -494,11 +493,11 @@ "id": "40dc1839eeb1e503", "metadata": {}, "source": [ - "### Interact with an entity\n", + "## Interact with an entity\n", "\n", "Data classes created by the code generator are based on Pydantic models, which provide validation and serialization.\n", "\n", - "#### Creating an entity\n", + "### Creating an entity\n", "\n", "To create an entity, you need to create an instance of the respective data class. The `__init__` method of the data \n", "class expects keyword arguments for all fields. As per usual for Pydantic models, positional arguments are not \n", @@ -512,6 +511,7 @@ "source": [ "# Create a person\n", "john = Person(\n", + " name=\"John Doe\",\n", " first_name=\"John\",\n", " last_name=\"Doe\",\n", " email=\"john.doe@example.com\"\n", @@ -544,6 +544,7 @@ "source": [ "# Should run without validation errors\n", "john = Person(\n", + " name=\"John Doe\",\n", " first_name=\"John\",\n", " surname=\"Doe\",\n", " email=[\"john.doe@example.com\"],\n", @@ -557,8 +558,8 @@ "id": "9aa534b206d8b486", "metadata": {}, "source": [ - "Before storing the entity in the OSL instance, lets check at which full page title it will be stored. \n", - "The full page title is derived from the `uuid` and the `namespace` of the entity." + "Before storing the entity in the OSL instance, lets check at which IRI (full page title) it will be stored.\n", + "The IRI is derived from the `uuid` and the `namespace` of the entity." ] }, { @@ -566,12 +567,11 @@ "id": "d861fde54918bc05", "metadata": {}, "source": [ - "from osw.utils.wiki import get_namespace, get_osw_id, get_full_title\n", - "\n", - "print(\"Namespace:\", get_namespace(john))\n", + "print(\"Page name:\", john.name)\n", + "print(\"Namespace:\", john.get_namespace())\n", "print(\"UUID:\", john.uuid)\n", - "print(\"OSW-ID:\", get_osw_id(john.uuid))\n", - "print(\"Full title:\", get_full_title(john))" + "print(\"OSW-ID:\", john.get_osw_id())\n", + "print(\"IRI:\", john.get_iri())" ], "outputs": [], "execution_count": null @@ -581,7 +581,7 @@ "id": "f6dace23bf99c7f3", "metadata": {}, "source": [ - "#### Storing an entity\n", + "### Storing an entity\n", "\n", "We can now store this entity in the OSL instance. The `store_entity` method uploads the entity to the OSL instance. " ] @@ -595,7 +595,7 @@ "# In this specific case equivalent to:\n", "params = OswExpress.StoreEntityParam(\n", " entities=[john],\n", - " namespace=get_namespace(john),\n", + " namespace=john.get_namespace(),\n", " parallel=False,\n", " overwrite=\"keep existing\",\n", " overwrite_per_class=None,\n", @@ -624,9 +624,9 @@ "id": "639db64348bbd905", "metadata": {}, "source": [ - "#### Downloading an entity\n", + "### Downloading an entity\n", "\n", - "To download an entity, you need to provide the full page title of the entity. The `download_entity` method downloads the\n", + "To download an entity, you need to provide the IRI of the entity. The `download_entity` method downloads the\n", "entity from the OSL instance and returns an instance of the respective data class. If the respective data class is \n", "not already part of `osw.model.entity`, the data class is generated on-the-fly by default." ] @@ -635,9 +635,7 @@ "cell_type": "code", "id": "ea1da2ff6d96808a", "metadata": {}, - "source": [ - "john2 = osw_obj.load_entity(get_full_title(john))" - ], + "source": "john2 = osw_obj.load_entity(john.get_iri())", "outputs": [], "execution_count": null }, @@ -679,7 +677,7 @@ "id": "d140b974b54155ce", "metadata": {}, "source": [ - "#### Modifying an entity\n", + "### Modifying an entity\n", "\n", "To modify an entity, you can change the attributes of the entity instance. The attributes can be accessed and modified \n", "like any other attribute of a Python object." @@ -691,8 +689,9 @@ "metadata": {}, "source": [ "# Adding a new attribute\n", - "john2.middle_name = \"R.\"\n", + "john2.middle_name = {\"R.\"}\n", "# Changing an existing attribute \n", + "john2.name = \"John R. Doe\"\n", "john2.email = {\"john.doe@gmx.de\"}\n", "\n", "# Checking the made changes:\n", @@ -701,24 +700,72 @@ "outputs": [], "execution_count": null }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Let's create an organization and link it to the person\n", + "org = Organization(\n", + " name=\"Doe, Inc.\",\n", + " label=[Label(text=\"Doe, Inc.\")],\n", + " description=[Description(text=\"The John Doe Company\")],\n", + " manager=[john2.get_iri()]\n", + ")\n", + "# And another one that is linked to both the person and the first organization\n", + "org2 = Organization(\n", + " name=\"Doe Group, Inc.\",\n", + " label=[Label(text=\"Doe Group, Inc.\")],\n", + " description=[Description(text=\"The Doe Group\")],\n", + " manager=[john2.get_iri()],\n", + " member_ous=[org.get_iri()]\n", + ")" + ], + "id": "b4425d8e6b1cbe62", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Here we have created links between Entities for the first time. The `get_iri` method returns the full title of the to\n", + " be created page(s), which replaces the pointers Python use where ever you reference an object by variable. JSON has\n", + " to use resolvable IRIs to realize this reference.\n", + "\n", + "Note: We are already working on a feature to automatically resolve these references, so you can use the Python\n", + "pointers in the future!" + ], + "id": "df52cc2f7dd2f13" + }, { "cell_type": "markdown", "id": "f42c1600ed17a059", "metadata": {}, "source": [ - "#### Storing an altered entity\n", + "### Storing an altered entity\n", "\n", "Here the same applies as for [Storing an entity](#Storing-an-entity). BUT: overwriting entities is not possible with \n", "default settings \"keep existing\". Therefore, you need to call the method `store_entity` passing a StoreEntityParam with \n", "the attribute `overwrite` set to \"overwrite\".\n" ] }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Define entities to be stored\n", + "entities2store = [john2, org, org2]" + ], + "id": "a7f9540af0850525", + "outputs": [], + "execution_count": null + }, { "metadata": {}, "cell_type": "code", "source": [ "# Option 1: Overwrite all entities\n", - "osw_obj.store_entity(OSW.StoreEntityParam(entities=[john2], overwrite=True))" + "osw_obj.store_entity(OSW.StoreEntityParam(entities=entities2store, overwrite=True))" ], "id": "77b507f13be1235b", "outputs": [], @@ -730,7 +777,7 @@ "source": [ "# Option 2: Overwrite only entities of type Person\n", "osw_obj.store_entity(OSW.StoreEntityParam(\n", - " entities=[john2], overwrite_per_class=[OSW.OverwriteClassParam(model=Person, overwrite=True)]))" + " entities=entities2store, overwrite_per_class=[OSW.OverwriteClassParam(model=Person, overwrite=True)]))" ], "id": "19d4d48e95e0fe82", "outputs": [], @@ -742,7 +789,7 @@ "source": [ "# Option 3: Overwrite only the email attribute of entities of type Person\n", "osw_obj.store_entity(OSW.StoreEntityParam(\n", - " entities=[john2], overwrite_per_class=[\n", + " entities=entities2store, overwrite_per_class=[\n", " OSW.OverwriteClassParam(model=Person, overwrite=False,per_property={\"email\": True})]))" ], "id": "4fc037a10f95356a", @@ -777,9 +824,9 @@ "metadata": {}, "cell_type": "markdown", "source": [ - "### Interact with files \n", + "## Interact with files \n", "\n", - "#### Download a file\n", + "### Download a file\n", "\n", "Let's say you have already uploaded a file to the instance of OSL you are connected to and have the URL to the file \n", "available. (Execute the Upload a file section to upload a file to the OSL instance.) You can download the file with \n", @@ -793,7 +840,7 @@ "source": [ "from osw.express import osw_download_file\n", "local_file = osw_download_file(\n", - " \"https://wiki-dev.open-semantic-lab.org/wiki/File:OSWaa635a571dfb4aa682e43b98937f5dd3.pdf\"\n", + " \"https://demo.open-semantic-lab.org/wiki/File:OSW98a720defe8349cab010ca1fc3528aee.pdf\",\n", " # , use_cached=True # Can be used to download the file only once, e.g., when developing code\n", " # , overwrite=True # Can be used to avoid overwriting an existing file\n", ")" @@ -825,8 +872,9 @@ "cell_type": "code", "source": [ "with osw_download_file(\n", - " \"https://wiki-dev.open-semantic-lab.org/wiki/File:OSWac9224e1a280449dba71d945b1581d57.txt\", overwrite=True\n", + " \"https://demo.open-semantic-lab.org/wiki/File:OSW2c08f45151ee4b518485ed68cf73c5e5.txt\", overwrite=True\n", ") as file:\n", + " file.open(\"r\")\n", " print(file.read())" ], "id": "ff674c1413ca6b4b", @@ -837,7 +885,7 @@ "metadata": {}, "cell_type": "markdown", "source": [ - "#### Round-Robin\n", + "### Round-Robin\n", "\n", "Let's create a file, upload it to the OSL instance, download it and read and alter its content, before uploading it \n", "again.\n" @@ -885,7 +933,8 @@ "cell_type": "code", "source": [ "# Delete WikiFile from OSW instance after you are done with it\n", - "wiki_file.delete()" + "wiki_file.delete()\n", + "# Error: InsufficientPermission, if you do not have the required rights" ], "id": "9647b490f90d2a0d", "outputs": [], @@ -895,9 +944,9 @@ "metadata": {}, "cell_type": "markdown", "source": [ - "### Interface data sources\n", + "## Interface data sources\n", "\n", - "#### Tabular data: Excel, CSV and others\n", + "### Tabular data: Excel, CSV and others\n", "\n", "Let's create a demo table and save it to an Excel file. " ], @@ -1008,6 +1057,8 @@ " \"Email\": \"email\"\n", "}\n", "data_df.rename(columns=mapping, inplace=True)\n", + "# Auto create the name\n", + "data_df[\"name\"] = data_df[\"first_name\"] + \" \" + data_df[\"surname\"]\n", "# Let's have a look at the first row\n", "john_dict = data_df.iloc[0].to_dict()\n", "print(john_dict)" @@ -1074,6 +1125,15 @@ " self.first_name = self.FirstName\n", " self.surname = self.LastName\n", " self.email = {self.Email}\n", + " self.name = f\"{self.first_name} {self.surname}\"\n", + " if self.middle_name:\n", + " self.name = f\"{self.first_name} {self.middle_name} {self.surname}\"\n", + " return True\n", + "\n", + " def transform_references(self, dd: dict = None) -> bool:\n", + " super().transform_references()\n", + " if self.organization:\n", + " self.organization = set([org.get_iri() for org in self.organization])\n", " return True\n", "\n", "# Let's create a new instance of the PersonHelper class\n", diff --git a/setup.cfg b/setup.cfg index 3f5c7ef5..267a3b9f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -91,6 +91,8 @@ dataimport = openpyxl UI = pysimplegui +tutorial = + %(dataimport)s all = %(dataimport)s %(DB)s From 403adbc9e0f4e5808a40c2be9e23a8f339af52aa Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 18 Nov 2024 22:54:53 +0100 Subject: [PATCH 42/52] Draft of start_tutorial.ipynb - a notebook to start from zero --- docs/tutorials/start_tutorial.ipynb | 93 +++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 docs/tutorials/start_tutorial.ipynb diff --git a/docs/tutorials/start_tutorial.ipynb b/docs/tutorials/start_tutorial.ipynb new file mode 100644 index 00000000..d70d6423 --- /dev/null +++ b/docs/tutorials/start_tutorial.ipynb @@ -0,0 +1,93 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Tutorial: Start Tutorial\n", + "\n", + "This notebook will help you to setup everything you need to run [Tutorial: Basics](./basics_tutorial.ipynb).\n", + "\n", + "To start open this notebook in [Google Colab](https://colab.research.google.com/github/OpenSemanticLab/osw-python/blob/enh-tutorial/docs/tutorials/start_tutorial.ipynb) or a similar Jupyter\n", + "web service and run the cells below." + ], + "id": "3daf975fa49d95e1" + }, + { + "metadata": {}, + "cell_type": "code", + "source": "# todo: google colab link and git clone commands need to be updated before merging the PR", + "id": "8c9d009ae8472e18", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Clone the repository and install the package with the tutorial dependencies\n", + "!git clone https://github.com/OpenSemanticLab/osw-python.git\n", + "%cd osw-python\n", + "!git fetch origin\n", + "!git checkout -b enh-tutorial origin/enh-tutorial\n", + "!pip install -e .[tutorial]" + ], + "id": "da4be67ad7e115c3", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Print current working directory\n", + "!pwd" + ], + "id": "f6beb7a048fed691", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# (Might not be required:) Make sure you are in osw-python/docs/tutorials directory\n", + "%cd osw-python/docs/tutorials\n", + "!pwd" + ], + "id": "3397cbdfd4b47653", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Open the tutorial\n", + "[Tutorial: Basics](./basics.ipynb)" + ], + "id": "c8e2f6168ee0e08" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From e093f4158ce1d9c7c0a4eaa78f7baccffc916685 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Thu, 21 Nov 2024 17:38:22 +0100 Subject: [PATCH 43/52] More content: semantic search --- docs/tutorials/basics.ipynb | 362 +++++++++++++++++++++++++++++------- 1 file changed, 290 insertions(+), 72 deletions(-) diff --git a/docs/tutorials/basics.ipynb b/docs/tutorials/basics.ipynb index d0a62605..837c7a4a 100644 --- a/docs/tutorials/basics.ipynb +++ b/docs/tutorials/basics.ipynb @@ -16,9 +16,10 @@ "- [Installation](#Installation)\n", "- [Connecting to an OSL instance](#Connecting-to-an-OSL-instance)\n", "- [Downloading data model dependencies](#Downloading-data-model-dependencies)\n", - "- [Interact with an entity](#Interact-with-an-entity) \n", - "- [Interact with files](#Interact-with-files)\n", - "- [Interface data sources](#Interface-data-sources)" + "- [Interacting with an entity](#Interacting-with-an-entity)\n", + "- [Interacting with files](#Interacting-with-files)\n", + "- [Interfacing data sources](#Interfacing-data-sources)\n", + "- [Querying data](#Querying-data)" ] }, { @@ -35,10 +36,11 @@ "\n", "
\n", " \n", - " \"Components\n", + " \"Components\n", + " \n", "
\n", "\n", "JSON serves as the central data storage element for structured data, including the definition of classes and forms\n", @@ -163,7 +165,7 @@ "\n", "### Object Oriented Linked Data (OO-LD)\n", "\n", - "The example above [JSON / JSON Schema](#JSON-/-JSON-Schema) already showed the integration of Object-Oriented \n", + "The example above [JSON / JSON Schema](#JSON--JSON-Schema) already showed the integration of Object-Oriented\n", "Programming (OOP) into JSON and JSON Schema. Adding the linked data component of [JSON-LD](https://json-ld.org/) \n", "enables the reusable annotation of datasets with well established vocabularies (ontologies), such as [schema.org] \n", "(https://schema.org/). Annotation have to be made at Category (=class) level only, and are available on export of \n", @@ -203,29 +205,30 @@ "id": "6e259e4f34e709ea", "metadata": {}, "source": [ - "## Downloading the library (optional)\n", "\n", + "\n", + "## Installation\n", + "\n", + "In case you just want to try the package in a throw-away environment, refer to the [Tutorial: Start tutorial](./start_tutorial.ipynb) Jupyter notebook.\n", + "\n", + "### Downloading the library (optional)\n", "The osw-python library is available as GitHub repository and can be downloaded as a ZIP file or via git:\n", "\n", "```bash\n", "git clone https://github.com/OpenSemanticLab/osw-python.git \n", "```\n", "\n", - "## Installation\n", - "\n", "### From PyPI\n", - "\n", "Preferably, you can install the library from the Python Package Index (PyPI) via pip, which is recommended for most users:\n", "\n", "```bash\n", - "conda activate # optional\n", + "conda activate # optional, if you use conda to manage environments\n", "pip install osw-python[tutorial]\n", "```\n", "The option `[tutorial]` installs the library with the dependencies required for data import, like `deepl`, `geopy`\n", "and `openpyxl`.\n", "\n", "### From source\n", - "\n", "If you want to install the library from source, you can clone the repository and install it via pip. The option `-e`\n", " installs the package in editable mode, which means that the source code is linked to the installed package. This is \n", " useful for development and testing.\n", @@ -233,7 +236,7 @@ "```bash\n", "git clone https://github.com/OpenSemanticLab/osw-python.git \n", "cd \n", - "conda activate # optional\n", + "conda activate # optional, if you use conda to manage environments\n", "pip install [-e] .[tutorial]\n", "```" ] @@ -285,7 +288,14 @@ "cell_type": "code", "id": "162b6208a105bbde", "metadata": {}, - "source": "from osw.express import OSW, OswExpress", + "source": [ + "from osw.express import OswExpress\n", + "# Some modules that will be required several times\n", + "import osw.params as prm\n", + "from pprint import pprint\n", + "from pathlib import Path\n", + "import pandas as pd" + ], "outputs": [], "execution_count": null }, @@ -435,13 +445,13 @@ "id": "7a8684bb1b8779fd", "metadata": {}, "source": [ - "### Install data models\n", + "### Installing data models\n", "\n", "Data models (data classes generated in osw.model.entity) can not be imported in Python scripts and modules prior to \n", "installation. Therefore, it is recommended to do this step either in a separate script, which is run before the main\n", "script, or in the main script itself, before the import statements.\n", "\n", - "#### Option 1: Install dependencies before import from osw.model.entity \n", + "#### Option 1: Installing dependencies before import from osw.model.entity\n", "\n", "This option is recommended to put in a separate script, which is run before the main script." ] @@ -455,7 +465,7 @@ "osw_obj.install_dependencies(dependencies)\n", "\n", "# Static code checker will note 'Module not found' before the installation:\n", - "from osw.model.entity import Organization, Person, Room, Tutorial" + "from osw.model.entity import Description, Label, Organization, Person, Room, Tutorial" ], "outputs": [], "execution_count": null @@ -477,6 +487,7 @@ "source": [ "from typing import TYPE_CHECKING\n", "from osw.express import import_with_fallback\n", + "from osw.model.entity import Description, Label # Available out of the box\n", "\n", "# Will fetch and install dependencies only if not already installed:\n", "import_with_fallback(dependencies, globals(), domain=wiki_domain)\n", @@ -493,7 +504,7 @@ "id": "40dc1839eeb1e503", "metadata": {}, "source": [ - "## Interact with an entity\n", + "## Interacting with an entity\n", "\n", "Data classes created by the code generator are based on Pydantic models, which provide validation and serialization.\n", "\n", @@ -511,7 +522,9 @@ "source": [ "# Create a person\n", "john = Person(\n", - " name=\"John Doe\",\n", + " label=[Label(text=\"John Doe\")], # Label, will be used as a display title\n", + " name=\"John Doe\", # Not required: Machine compatible name\n", + " # Machine compatible is auto-generated from the first label\n", " first_name=\"John\",\n", " last_name=\"Doe\",\n", " email=\"john.doe@example.com\"\n", @@ -544,7 +557,7 @@ "source": [ "# Should run without validation errors\n", "john = Person(\n", - " name=\"John Doe\",\n", + " label=[Label(text=\"John Doe\")], # Label, will be used as a display title\n", " first_name=\"John\",\n", " surname=\"Doe\",\n", " email=[\"john.doe@example.com\"],\n", @@ -567,7 +580,8 @@ "id": "d861fde54918bc05", "metadata": {}, "source": [ - "print(\"Page name:\", john.name)\n", + "print(\"Page label:\", john.label[0].text)\n", + "print(\"Machine-readable page name:\", john.name)\n", "print(\"Namespace:\", john.get_namespace())\n", "print(\"UUID:\", john.uuid)\n", "print(\"OSW-ID:\", john.get_osw_id())\n", @@ -591,9 +605,13 @@ "id": "64e0678b77f00772", "metadata": {}, "source": [ - "osw_obj.store_entity(john)\n", + "# Let's document all storing processes in a list\n", + "stored = []\n", + "# Let's store the entity\n", + "res = osw_obj.store_entity(john)\n", + "stored.append(res)\n", "# In this specific case equivalent to:\n", - "params = OswExpress.StoreEntityParam(\n", + "params = prm.StoreEntityParam(\n", " entities=[john],\n", " namespace=john.get_namespace(),\n", " parallel=False,\n", @@ -635,26 +653,18 @@ "cell_type": "code", "id": "ea1da2ff6d96808a", "metadata": {}, - "source": "john2 = osw_obj.load_entity(john.get_iri())", + "source": [ + "john2 = osw_obj.load_entity(john.get_iri())\n", + "# Let's have a look at the attributes of the downloaded entity" + ], "outputs": [], "execution_count": null }, - { - "cell_type": "markdown", - "id": "fe620279252b6886", - "metadata": {}, - "source": [ - "Lets have a look at the attributes of the downloaded entity" - ] - }, { "cell_type": "code", "id": "19d1c6767b2098b", "metadata": {}, - "source": [ - "from pprint import pprint\n", - "pprint(john2.dict())" - ], + "source": "pprint(john2.dict())", "outputs": [], "execution_count": null }, @@ -706,14 +716,12 @@ "source": [ "# Let's create an organization and link it to the person\n", "org = Organization(\n", - " name=\"Doe, Inc.\",\n", " label=[Label(text=\"Doe, Inc.\")],\n", " description=[Description(text=\"The John Doe Company\")],\n", " manager=[john2.get_iri()]\n", ")\n", "# And another one that is linked to both the person and the first organization\n", "org2 = Organization(\n", - " name=\"Doe Group, Inc.\",\n", " label=[Label(text=\"Doe Group, Inc.\")],\n", " description=[Description(text=\"The Doe Group\")],\n", " manager=[john2.get_iri()],\n", @@ -745,7 +753,7 @@ "### Storing an altered entity\n", "\n", "Here the same applies as for [Storing an entity](#Storing-an-entity). BUT: overwriting entities is not possible with \n", - "default settings \"keep existing\". Therefore, you need to call the method `store_entity` passing a StoreEntityParam with \n", + "default settings \"keep existing\". Therefore, you need to call the method `store_entity` passing a `StoreEntityParam` with\n", "the attribute `overwrite` set to \"overwrite\".\n" ] }, @@ -765,7 +773,8 @@ "cell_type": "code", "source": [ "# Option 1: Overwrite all entities\n", - "osw_obj.store_entity(OSW.StoreEntityParam(entities=entities2store, overwrite=True))" + "res = osw_obj.store_entity(prm.StoreEntityParam(entities=entities2store, overwrite=True))\n", + "stored.append(res)" ], "id": "77b507f13be1235b", "outputs": [], @@ -776,8 +785,9 @@ "cell_type": "code", "source": [ "# Option 2: Overwrite only entities of type Person\n", - "osw_obj.store_entity(OSW.StoreEntityParam(\n", - " entities=entities2store, overwrite_per_class=[OSW.OverwriteClassParam(model=Person, overwrite=True)]))" + "res = osw_obj.store_entity(prm.StoreEntityParam(\n", + " entities=entities2store, overwrite_per_class=[prm.OverwriteClassParam(model=Person, overwrite=True)]))\n", + "stored.append(res)" ], "id": "19d4d48e95e0fe82", "outputs": [], @@ -788,9 +798,10 @@ "cell_type": "code", "source": [ "# Option 3: Overwrite only the email attribute of entities of type Person\n", - "osw_obj.store_entity(OSW.StoreEntityParam(\n", + "res = osw_obj.store_entity(prm.StoreEntityParam(\n", " entities=entities2store, overwrite_per_class=[\n", - " OSW.OverwriteClassParam(model=Person, overwrite=False,per_property={\"email\": True})]))" + " prm.OverwriteClassParam(model=Person, overwrite=False,per_property={\"email\": True})]))\n", + "stored.append(res)" ], "id": "4fc037a10f95356a", "outputs": [], @@ -804,19 +815,19 @@ "entities of different classes in the list of entities to store.\n", "\n", "The param `overwrite` is applied to all entities handed to the method regardless of type / class. It is also possible\n", - " to specify the overwrite behavior per class, by providing a list of `OSW.OverwriteClassParam`s. Those can even be \n", + " to specify the overwrite behavior per class, by providing a list of `prm.OverwriteClassParam`s. Those can even be\n", " specific down to the property level. \n", - "- Available options for `OSW.StoreEntitParam.overwrite`, `OSW.OverwriteClassParam.overwrite` (per class) and `OSW\n", + "- Available options for `prm.StoreEntitParam.overwrite`, `prm.OverwriteClassParam.overwrite` (per class) and `OSW\n", ".OverwriteClassParam.per_property` are: \n", - " - `OSW.OverwriteOptions.true`: True - overwrite the remote entity or property with the local one\n", - " - `OSW.OverwriteOptions.false`: False - do not overwrite the remote entity or property with the local one\n", - " - `OSW.OverwriteOptions.only_empty`: \"only empty\" - overwrite the remote entity or property with the local one, \n", + " - `prm.OverwriteOptions.true`: True - overwrite the remote entity or property with the local one\n", + " - `prm.OverwriteOptions.false`: False - do not overwrite the remote entity or property with the local one\n", + " - `prm.OverwriteOptions.only_empty`: \"only empty\" - overwrite the remote entity or property with the local one,\n", " if the remote entity or property is empty\n", - "- Only available to `OSW.StoreEntitParam.overwrite` and `OSW.OverwriteClassParam.overwrite` (per class) are:\n", - " - `OSW.AddOverwriteClassOptions.replace_remote`: \"replace remote\" - replace the remote entity with the local one and\n", + "- Only available to `prm.StoreEntitParam.overwrite` and `prm.OverwriteClassParam.overwrite` (per class) are:\n", + " - `prm.AddOverwriteClassOptions.replace_remote`: \"replace remote\" - replace the remote entity with the local one and\n", " removes all properties not present in the local entity\n", - " - `OSW.AddOverwriteClassOptions.keep_existing`: \"keep existing\" - keep the remote entity, if one exists under this \n", - " OSW-ID " + " - `prm.AddOverwriteClassOptions.keep_existing`: \"keep existing\" - keep the remote entity, if one exists under this\n", + " OSW-ID" ], "id": "133a7c6940f23ae6" }, @@ -824,9 +835,37 @@ "metadata": {}, "cell_type": "markdown", "source": [ - "## Interact with files \n", + "## Exploring links and properties in the knowledge graph of the OSL instance\n", + "\n", + "The output of the code cell above should list pages where entities have been stored at. Follow one of the links\n", + "to the OSL instance you are connected to. You should end up on the page of an instance of either `Organization` or `Person`. Follow these steps to explore the knowledge graph of the OSL instance:\n", + "- Scroll down to the bottom of the page and un-collapse, by clicking, the `Graph` section. This will open a graph\n", + "representation of the entity. By default, only the property edges `HasSchema`/`HasType` and `SubClassOf` are shown.\n", + "- By right-clicking on a node, e.g., the person instance, the Person/Organization class, you have access to a list of properties that the node posses.\n", + "- By left-clicking on a property the nodes that are connected via this property will appear. By repeating this\n", + "process, you can follow property paths in this network and explore the graph.\n", + "\n", + "Later on in this tutorial, you can use the paths you have explored to create semantic queries.\n", + "\n", + "### Exercise\n", + "- If you are on the page `John Doe`\n", + " - Explore the property `HasName`, `HasFirstName` and `HasSurName` - these are properties of type `string`,\n", + " assigning a value to an entry.\n", + " - Explore the property `-HasManager` to find organizations, which state John as their manager. Here you are looking into an inverse property relation, of type `@id` - it links two entries within the knowledge graph.\n", + " - On the node `Doe Group, Inc.` explore the property `HasMember` to find the organization `Doe, Inc.`. This is also a property of type `@id`, but not an inverse relation.\n", + "- If you are on the page `Doe Group, Inc.`\n", + " - Explore the property `HasMember` to find the organization `Doe, Inc.`.\n", + " - Explore the property `HasManager` to find the person `John Doe`." + ], + "id": "2107b3efcd22837f" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Interacting with files\n", "\n", - "### Download a file\n", + "### Downloading a file\n", "\n", "Let's say you have already uploaded a file to the instance of OSL you are connected to and have the URL to the file \n", "available. (Execute the Upload a file section to upload a file to the OSL instance.) You can download the file with \n", @@ -905,6 +944,7 @@ " file.write(\"Hello, World!\")\n", "# Upload a file to an OSW instance\n", "wiki_file = osw_upload_file(fp, domain=wiki_domain)\n", + "stored.append(wiki_file)\n", "# Delete the local file\n", "fp.unlink()\n", "\n", @@ -922,7 +962,8 @@ " file.write(content)\n", "\n", "# Upload the modified file\n", - "modified_wiki_file = osw_upload_file(local_file)" + "modified_wiki_file = osw_upload_file(local_file)\n", + "stored.append(modified_wiki_file)" ], "id": "14a0ee12dde2961e", "outputs": [], @@ -944,7 +985,7 @@ "metadata": {}, "cell_type": "markdown", "source": [ - "## Interface data sources\n", + "## Interfacing data sources\n", "\n", "### Tabular data: Excel, CSV and others\n", "\n", @@ -976,7 +1017,6 @@ "metadata": {}, "cell_type": "code", "source": [ - "from pprint import pprint\n", "# Let's read in our example Excel file\n", "data_df = pd.read_excel(\"demo.xlsx\")\n", "# Pandas dict representation is optimal for converting to JSON\n", @@ -994,7 +1034,7 @@ "source": [ "# Let's convert the dict to a Person instance\n", "from osw.model.entity import Person\n", - "john = Person(**john_dict)\n", + "john_xl = Person(**john_dict)\n", "# This will cause ValidationError(s):\n", "# - first_name: field required\n", "# - surname: field required" @@ -1057,8 +1097,9 @@ " \"Email\": \"email\"\n", "}\n", "data_df.rename(columns=mapping, inplace=True)\n", - "# Auto create the name\n", + "# Auto create the name & label\n", "data_df[\"name\"] = data_df[\"first_name\"] + \" \" + data_df[\"surname\"]\n", + "data_df[\"label\"] = data_df[\"name\"].apply(lambda x: [{\"text\": x}])\n", "# Let's have a look at the first row\n", "john_dict = data_df.iloc[0].to_dict()\n", "print(john_dict)" @@ -1072,7 +1113,7 @@ "cell_type": "code", "source": [ "# Let's construct an instance of the Person data model\n", - "john = Person(**john_dict)\n", + "john_xl = Person(**john_dict)\n", "# This will cause a ValidationError:\n", "# - email: value is not a valid set" ], @@ -1086,8 +1127,8 @@ "source": [ "# Let's correct the email field\n", "john_dict[\"email\"] = [john_dict[\"email\"]]\n", - "john = Person(**john_dict)\n", - "pprint(john.dict())" + "john_xl = Person(**john_dict)\n", + "pprint(john_xl.dict())" ], "id": "9e5c3bc9ff17b2e4", "outputs": [], @@ -1113,6 +1154,7 @@ "source": [ "from typing import Any\n", "from osw.data.import_utility import HelperModel\n", + "from osw.utils.strings import pascal_case\n", "\n", "class PersonHelper(Person, HelperModel):\n", " # Attributes of the first base class are set to Optional[Any], default: None\n", @@ -1125,15 +1167,16 @@ " self.first_name = self.FirstName\n", " self.surname = self.LastName\n", " self.email = {self.Email}\n", - " self.name = f\"{self.first_name} {self.surname}\"\n", + " self.name = pascal_case(f\"{self.first_name} {self.surname}\")\n", + " self.label = [Label(text=self.name)]\n", " if self.middle_name:\n", - " self.name = f\"{self.first_name} {self.middle_name} {self.surname}\"\n", + " self.name = pascal_case(\"{self.first_name} {self.middle_name} {self.surname}\")\n", " return True\n", "\n", " def transform_references(self, dd: dict = None) -> bool:\n", " super().transform_references()\n", " if self.organization:\n", - " self.organization = set([org.get_iri() for org in self.organization])\n", + " self.organization = set([org_.get_iri() for org_ in self.organization])\n", " return True\n", "\n", "# Let's create a new instance of the PersonHelper class\n", @@ -1151,7 +1194,7 @@ "metadata": {}, "cell_type": "code", "source": [ - "# Let's see the effect of the transformation\n", + "# Let's see the effect of the transformation of attributes\n", "john_helper.transform_attributes()\n", "print(\"After transformation:\")\n", "pprint(john_helper.dict())" @@ -1167,9 +1210,9 @@ "# Actually we access 'transformed' directly\n", "# If the transformation operations had not been performed already,\n", "# accessing 'transformed' would trigger them\n", - "john = john_helper.transformed\n", + "local_john = john_helper.transformed\n", "print(\"After casting:\")\n", - "pprint(john.dict())" + "pprint(local_john.dict())" ], "id": "2ff76f2a76c097f6", "outputs": [], @@ -1193,11 +1236,186 @@ "cell_type": "code", "source": [ "# And store the entities in the OSL instance\n", - "osw_obj.store_entity(entities)" + "res = osw_obj.store_entity(entities)\n", + "stored.append(res)" ], "id": "15f2596aff7ac2e0", "outputs": [], "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Querying data\n", + "\n", + "### Querying instances of a category\n", + "osw-python offers methods to query semantic properties, which are set in the triplestore of the Open Semantic Lab\n", + "instance, and which were defined in `@context` of the JSON-Schema documents. We can therefore query for instances of\n", + "a category, which point to the category with the properties `HasType` and `HasSchema`. The method `query_instances`\n", + "is a convenience method, basically a wrapper around `semantic_search()`, querying: `[[HasType::Category:]]`. This query essentially queries all entries in the platform, which fulfill the condition." + ], + "id": "30d853a79d55057a" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# query_instances() returns a list of the full page titles (IRIs) of instances of the given class\n", + "# The method queries direct instances only, instances of subclasses are not returned\n", + "persons = osw_obj.query_instances(Person)\n", + "organizations = osw_obj.query_instances(Organization)\n", + "print(\"Number of persons stored in the OSL instance:\", len(persons))\n", + "print(\"Number of organizations stored in the OSL instance:\", len(organizations))" + ], + "id": "150a5af64977fb64", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Test if john is in the list of persons\n", + "john.get_iri() in persons" + ], + "id": "ea4ac48cd87af92a", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# To get the instances themselves, use load_entity() with the IRIs\n", + "print(\"Loading person with IRI:\", persons[0])\n", + "some_person = osw_obj.load_entity(persons[0])\n", + "pprint(some_person.dict())" + ], + "id": "5bf7f397f2cb48c", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Querying semantic properties\n", + "We can also query for the entries, a semantic properties links to, starting from a specific entity. Here, we are\n", + "using the syntax defined in the Semantic Mediawiki [user manual](https://www.semantic-mediawiki.org/wiki/Help:Semantic_search).\n", + "\n", + "#### In the browser\n", + "It might be a good idea to start with a query in the browser, to get a feeling for the structure of the data. You\n", + "have explored the graph tool already, so you know properties (edges) link entities (nodes). The semantic search uses\n", + "a Semantic Mediawiki specific syntax.\n", + "\n", + "If you are connected to the OSL instance with the domain `demo.open-semantic-lab.org`, you can use the following link:\n", + "[Special:Ask](https://demo.open-semantic-lab.org/w/index.php?title=Special:Ask&#search).\n", + "\n", + "If you are connected to another OSL instance, use the following cell to create a link for you:" + ], + "id": "8360dc23ce41c09b" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Creating a clickable link for you\n", + "from sys import stdout\n", + "stdout.write(f\"Interactive semantic search: 'https://{wiki_domain}/w/index.php?title=Special:Ask&#search'\")" + ], + "id": "91cf5af37c1face5", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Let's explore some queries. Enter the following queries in the field `Condition` and click `Find results`:\n", + "- [[HasName::John Doe]] - Query for all entries that posses the property `HasName` with the value `John Doe`\n", + "- [[HasManager.HasName::John Doe]] - Query for all entries that specify person, named `John Doe` as manager\n", + "\n", + "The queries should return:\n", + "- A list of all `John Doe` named entries in the platform\n", + "- A list of organizations, where `John Doe` is the manager - there should be at least one, the `Doe Group, Inc.` and\n", + "`Doe, Inc.` we have created earlier" + ], + "id": "b6122becf69e18ec" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "#### In Python\n", + "\n", + "The method `semantic_search` allows to do query for semantic properties in python. At the moment, the method is\n", + "only available for the `site` attribute of OSW / OswExpress." + ], + "id": "c814cd2e5bf56d96" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "managed_by_john = f\"[[HasManager::{john.get_iri()}]]\"\n", + "# Queries can also be performed on inverse properties\n", + "members_of_doe_group = f\"[[-HasMember::{org2.get_iri()}]]\"\n", + "# Multiple query conditions can be applied in one query\n", + "members_of_managed_by_john = f\"[[HasManager::{john.get_iri()}]][[HasMember::+]]\"\n", + "# Query them\n", + "for query in [managed_by_john, members_of_doe_group, members_of_managed_by_john]:\n", + " query_res = osw_obj.site.semantic_search(query)\n", + " print(f\"Querying '{query}' returned:\", query_res)" + ], + "id": "8c3f0532322e4b51", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Now at the very end, lets query everything that we have uploaded\n", + "# The change_id can be recovered from the store_entity returns, as we have preserved them in `stored`\n", + "change_ids = [res.change_id for res in stored]\n", + "print(\"Change Ids:\", change_ids)" + ], + "id": "be231b1bccee0b71", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Create queries for the change_ids\n", + "# We can query along a path of properties. Here each entry has a sub-object 'meta', which points to the change_id with\n", + "# 'HasChangeId'.\n", + "queries = [f\"[[HasMeta.HasChangeId::{change_id}]]\" for change_id in change_ids]\n", + "# We could also create a single query, that chains all queries with 'OR':\n", + "query = \"OR \".join(queries)\n", + "print(\"Alternative chained query:\", query)\n", + "search_param = prm.SearchParam(query=queries)\n", + "res_iris = osw_obj.site.semantic_search(search_param)\n", + "print(\"IRIs of the uploaded entities:\", res_iris)" + ], + "id": "7a3a8d820b939523", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Clean up - delete all uploaded entities (if you have permission to do so)\n", + "osw_obj.site.delete_page(res_iris)\n", + "# Will fail if you do not have the required rights - we will clean up for you :)" + ], + "id": "167bd0a450faef66", + "outputs": [], + "execution_count": null } ], "metadata": { From ac7f8f316730072cee90b26baa37e4ea01d12c1d Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 25 Nov 2024 10:01:12 +0100 Subject: [PATCH 44/52] Added optional section to install & run tutorial in online notebook service. Removed unusable start_tutorial.ipynb --- docs/tutorials/basics.ipynb | 24 +++++--- docs/tutorials/start_tutorial.ipynb | 93 ----------------------------- 2 files changed, 17 insertions(+), 100 deletions(-) delete mode 100644 docs/tutorials/start_tutorial.ipynb diff --git a/docs/tutorials/basics.ipynb b/docs/tutorials/basics.ipynb index 837c7a4a..690560af 100644 --- a/docs/tutorials/basics.ipynb +++ b/docs/tutorials/basics.ipynb @@ -22,6 +22,16 @@ "- [Querying data](#Querying-data)" ] }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Optional: Install & run in online notebook\n", + "In case you want to run this tutorial in a throw-away environment, you can use an online notebook service, e.g., \n", + "Google Colab. " + ], + "id": "3f70a55ea4a901c5" + }, { "cell_type": "markdown", "id": "64e0121de7a40c80", @@ -209,7 +219,7 @@ "\n", "## Installation\n", "\n", - "In case you just want to try the package in a throw-away environment, refer to the [Tutorial: Start tutorial](./start_tutorial.ipynb) Jupyter notebook.\n", + "In case you just want to try the package in a throw-away environment, refer to the section [Optional: Run in online notebook](#Optional-Run-in-online-notebook) Jupyter notebook.\n", "\n", "### Downloading the library (optional)\n", "The osw-python library is available as GitHub repository and can be downloaded as a ZIP file or via git:\n", @@ -601,9 +611,8 @@ ] }, { - "cell_type": "code", - "id": "64e0678b77f00772", "metadata": {}, + "cell_type": "code", "source": [ "# Let's document all storing processes in a list\n", "stored = []\n", @@ -620,13 +629,13 @@ ") # All default values included\n", "# osw_obj.store_entity(params)" ], + "id": "78ecbe8be2788f1d", "outputs": [], "execution_count": null }, { - "cell_type": "markdown", - "id": "78f959815e0fe8f", "metadata": {}, + "cell_type": "markdown", "source": [ "Like most methods and functions in the osw-python library, the `store_entity` takes only a single argument. Usually \n", "either a specific object type or a dedicated params object is accepted. If an object of type other than the params \n", @@ -635,7 +644,8 @@ "\n", "This ensures that the method signature is as simple as possible and at the same time allows full typing and validation \n", "of input parameters that the method is easy to use. " - ] + ], + "id": "2e30dd888a08ca78" }, { "cell_type": "markdown", @@ -1280,7 +1290,7 @@ "# Test if john is in the list of persons\n", "john.get_iri() in persons" ], - "id": "ea4ac48cd87af92a", + "id": "c47281593ceed2bf", "outputs": [], "execution_count": null }, diff --git a/docs/tutorials/start_tutorial.ipynb b/docs/tutorials/start_tutorial.ipynb deleted file mode 100644 index d70d6423..00000000 --- a/docs/tutorials/start_tutorial.ipynb +++ /dev/null @@ -1,93 +0,0 @@ -{ - "cells": [ - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "# Tutorial: Start Tutorial\n", - "\n", - "This notebook will help you to setup everything you need to run [Tutorial: Basics](./basics_tutorial.ipynb).\n", - "\n", - "To start open this notebook in [Google Colab](https://colab.research.google.com/github/OpenSemanticLab/osw-python/blob/enh-tutorial/docs/tutorials/start_tutorial.ipynb) or a similar Jupyter\n", - "web service and run the cells below." - ], - "id": "3daf975fa49d95e1" - }, - { - "metadata": {}, - "cell_type": "code", - "source": "# todo: google colab link and git clone commands need to be updated before merging the PR", - "id": "8c9d009ae8472e18", - "outputs": [], - "execution_count": null - }, - { - "metadata": {}, - "cell_type": "code", - "source": [ - "# Clone the repository and install the package with the tutorial dependencies\n", - "!git clone https://github.com/OpenSemanticLab/osw-python.git\n", - "%cd osw-python\n", - "!git fetch origin\n", - "!git checkout -b enh-tutorial origin/enh-tutorial\n", - "!pip install -e .[tutorial]" - ], - "id": "da4be67ad7e115c3", - "outputs": [], - "execution_count": null - }, - { - "metadata": {}, - "cell_type": "code", - "source": [ - "# Print current working directory\n", - "!pwd" - ], - "id": "f6beb7a048fed691", - "outputs": [], - "execution_count": null - }, - { - "metadata": {}, - "cell_type": "code", - "source": [ - "# (Might not be required:) Make sure you are in osw-python/docs/tutorials directory\n", - "%cd osw-python/docs/tutorials\n", - "!pwd" - ], - "id": "3397cbdfd4b47653", - "outputs": [], - "execution_count": null - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "## Open the tutorial\n", - "[Tutorial: Basics](./basics.ipynb)" - ], - "id": "c8e2f6168ee0e08" - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 0bb9ec9533864d4c4693087062e7bfb8ebec1ae4 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 25 Nov 2024 13:47:18 +0100 Subject: [PATCH 45/52] Moving Entity.__init__ to static.py to avoid overwrite by fetch_schema Has implications on the placement of methods and classes in osw.utils.strings, as this module imports osw.model.static.Basemodel - we need to avoid circular imports --- scripts/migration/file_page_migration.py | 2 +- src/osw/controller/page_package.py | 2 +- src/osw/data/import_utility.py | 8 +- src/osw/model/entity.py | 23 -- src/osw/model/static.py | 35 ++- src/osw/utils/regex.py | 357 ++++++++++++++++++++++ src/osw/utils/regex_pattern.py | 2 +- src/osw/utils/strings.py | 358 ----------------------- tests/utils/utils_test.py | 3 +- 9 files changed, 399 insertions(+), 391 deletions(-) create mode 100644 src/osw/utils/regex.py diff --git a/scripts/migration/file_page_migration.py b/scripts/migration/file_page_migration.py index ce8b4fb9..b60e6212 100644 --- a/scripts/migration/file_page_migration.py +++ b/scripts/migration/file_page_migration.py @@ -12,8 +12,8 @@ import osw.wiki_tools as wt from osw.core import OSW from osw.model.entity import Label, WikiFile +from osw.utils.regex import match_first_regex_pattern, test_regex_pattern from osw.utils.regex_pattern import REGEX_PATTERN_LIB, REGEX_PATTERN_LIST -from osw.utils.strings import match_first_regex_pattern, test_regex_pattern from osw.utils.util import parallelize from osw.wtsite import WtPage, WtSite diff --git a/src/osw/controller/page_package.py b/src/osw/controller/page_package.py index edc4a95d..dfdcbbec 100644 --- a/src/osw/controller/page_package.py +++ b/src/osw/controller/page_package.py @@ -14,7 +14,7 @@ from osw.model import page_package as package from osw.model.page_package import NAMESPACE_CONST_TO_NAMESPACE_MAPPING from osw.model.static import OswBaseModel -from osw.utils.strings import RegExPatternExtended +from osw.utils.regex import RegExPatternExtended from osw.wtsite import WtSite # Definition of constants diff --git a/src/osw/data/import_utility.py b/src/osw/data/import_utility.py index 2d52cf7a..9a0bb122 100644 --- a/src/osw/data/import_utility.py +++ b/src/osw/data/import_utility.py @@ -12,11 +12,11 @@ from jsonpath_ng import ext as jp from pydantic.v1.fields import ModelField -import osw.utils.strings as strutil from osw import wiki_tools as wt from osw.auth import CredentialManager from osw.core import OSW from osw.model import entity as model +from osw.utils.regex import MatchResult, RegExPatternExtended from osw.utils.regex_pattern import REGEX_PATTERN_LIB, REGEX_PATTERN_LIST from osw.wtsite import WtSite @@ -502,8 +502,8 @@ def nan_empty_or_none(inp: Any) -> bool: def regex_match_list( - pattern: Union[str, strutil.RegExPatternExtended], list_of_strings: List[str] -) -> List[Union[str, strutil.MatchResult]]: + pattern: Union[str, RegExPatternExtended], list_of_strings: List[str] +) -> List[Union[str, MatchResult]]: """Returns a subset of the 'list_of_strings' that matched the regex 'pattern'. Parameters @@ -522,7 +522,7 @@ def regex_match_list( if re.match(pattern=pattern, string=string): matches.append(string) return matches - elif isinstance(pattern, strutil.RegExPatternExtended): + elif isinstance(pattern, RegExPatternExtended): matches = [] for string in list_of_strings: match_result_obj = pattern.match(string) diff --git a/src/osw/model/entity.py b/src/osw/model/entity.py index 28174478..d2310b56 100644 --- a/src/osw/model/entity.py +++ b/src/osw/model/entity.py @@ -10,7 +10,6 @@ from pydantic.v1 import Field, constr from osw.model.static import OswBaseModel -from osw.utils.strings import pascal_case class ReadAccess(OswBaseModel): @@ -102,28 +101,6 @@ class Entity(OswBaseModel): attachments: Optional[List[str]] = Field(None, title="File attachments") meta: Optional[Meta] = None - def __init__(self, **data): - if data.get("label"): - if not isinstance(data["label"], list): - raise ValueError( - "label must be a list of Label objects", - ) - labels = [] - for label in data["label"]: - if isinstance(label, dict): - labels.append(Label(**label)) - else: - # The list element should be a Label object - labels.append(label) - data["label"] = labels - if not all(isinstance(label, Label) for label in data["label"]): - raise ValueError( - "label must be a list of Label objects", - ) - if data.get("name") is None and "label" in data: - data["name"] = pascal_case(data["label"][0].text) - super().__init__(**data) - class ObjectStatement(OswBaseModel): rdf_type: Optional[Any] = "rdf:Statement" diff --git a/src/osw/model/static.py b/src/osw/model/static.py index 02634ff8..efce2d7d 100644 --- a/src/osw/model/static.py +++ b/src/osw/model/static.py @@ -2,12 +2,13 @@ This module is to be imported in the dynamically created and updated entity.py module. """ -from typing import TYPE_CHECKING, Type, TypeVar, Union +from typing import TYPE_CHECKING, Literal, Optional, Type, TypeVar, Union from uuid import UUID -from pydantic.v1 import BaseModel +from pydantic.v1 import BaseModel, Field, constr from osw.custom_types import NoneType +from osw.utils.strings import pascal_case T = TypeVar("T", bound=BaseModel) @@ -88,6 +89,31 @@ class Config: validate_assignment = True # Ensures that the assignment of a value to a field is validated + def __init__(self, **data): + if data.get("label"): + if not isinstance(data["label"], list): + raise ValueError( + "label must be a list of Label objects", + ) + labels = [] + for label in data["label"]: + if isinstance(label, dict): + labels.append(Label(**label)) + else: + # The list element should be a Label object + labels.append(label) + data["label"] = labels + # Ensure that the label attribute is a list of Label objects, but use + # custom_isinstance to avoid circular imports and ValidationError since + # osw.model.entity defines its own Label class + if not all(custom_isinstance(label, "Label") for label in data["label"]): + raise ValueError( + "label must be a list of Label objects", + ) + if data.get("name") is None and "label" in data: + data["name"] = pascal_case(data["label"][0].text) + super().__init__(**data) + def full_dict(self, **kwargs): # extent BaseClass export function d = super().dict(**kwargs) for key in ("_osl_template", "_osl_footer"): @@ -300,3 +326,8 @@ class Ontology(OswBaseModel): name: str prefix_name: str link: str + + +class Label(OswBaseModel): + text: constr(min_length=1) = Field(..., title="Text") + lang: Optional[Literal["en", "de"]] = Field("en", title="Lang code") diff --git a/src/osw/utils/regex.py b/src/osw/utils/regex.py new file mode 100644 index 00000000..3012d2d3 --- /dev/null +++ b/src/osw/utils/regex.py @@ -0,0 +1,357 @@ +import re +from typing import Dict, List, Optional, Union + +from pydantic.v1 import validator + +from osw.model.static import OswBaseModel + + +# Classes +class RegExPatternExtended(OswBaseModel): + """A class that extends the re.Pattern class with additional attributes. + + Example + ------- + Definition + >>> pattern = RegExPatternExtended( + >>> description="File page full page title (new format) to label", + >>> pattern=r"File:(OS[WL]{1}[[a-f0-9]{32})" + >>> "(\\.[a-zA-Z0-9]*[a-zA-Z]+[a-zA-Z0-9]*)", + >>> group_keys=["Label", "Suffix(es)"], # ["OSW-ID", "Suffix(es)"] + >>> example_str="File:OSW11a53cdfbdc24524bf8ac435cbf65d9d.svg", + >>> expected_groups=["OSW11a53cdfbdc24524bf8ac435cbf65d9d", ".svg"], + >>> ) + Usage + >>> some_str = pattern.example_str + >>> match = pattern.match(some_str) + >>> label = match.groups["label"] + >>> suffix = match.groups["Suffix(es)"] + Testing + >>> pattern.test_pattern() + + + Resources + --------- + https://regex101.com/ + """ + + pattern: Union[str, re.Pattern] + """The regular expression pattern. Can be a string or a compiled pattern.""" + description: str + """A (unique!) description of the pattern. This is used as a key in a dictionary + containing all patterns and acting as a library.""" + group_keys: List[str] + """A list of keys that can be used to access the match groups in the match + result. The number of keys must match the number of match groups in the pattern. + So the first key (list index 0) will be used to access the first match group + (index 1!).""" + example_str: Optional[str] + """An example string that can be used to test the pattern.""" + expected_groups: Optional[List[str]] + """A list strings that are expected in the example_match.groups.values(). This is + used to test the pattern by asserting list(self.example_match.groups.values()) == + self.expected_groups""" + + class Config: + arbitrary_types_allowed = True + + def __init__(self, **data): + super().__init__(**data) + if isinstance(self.pattern, str): + self.pattern = re.compile(self.pattern) + + def match(self, string: str) -> "MatchResult": + """Return the match result of the given string and the pattern.""" + return MatchResult(match=re.match(self.pattern, string), pattern=self) + + def search(self, string: str) -> "SearchResult": + """Return the search result of the given string and the pattern.""" + return SearchResult(match=re.search(self.pattern, string), pattern=self) + + def finditer(self, string: str) -> List["MatchResult"]: + """Return a list of all match results of the given string and the pattern.""" + return [ + MatchResult(match=match, pattern=self) + for match in re.finditer(self.pattern, string) + ] + + def findall(self, string: str) -> Dict[str, List[str]]: + """Return a list of all match groups of the given string and the pattern.""" + result = {} + for grp_key in self.group_keys: + result[grp_key] = [mr.groups[grp_key] for mr in self.finditer(string)] + return result + + def findall_by_group_key(self, string: str, group_key: str) -> List[str]: + """Return a list of all match groups of the given string and the pattern by + group key.""" + if group_key not in self.group_keys: + raise ValueError(f"Group key '{group_key}' not found in pattern.") + return self.findall(string)[group_key] + + @property + def example_match(self) -> "MatchResult": + """Return the match result of the example string and the pattern.""" + return self.match(string=self.example_str) + + @validator("group_keys") + def validate_group_keys(cls, group_keys, values) -> List[str]: + """Validate that the number of group keys matches the number of match groups, + defined in the pattern.""" + pattern = values.get("pattern") + group_count = count_match_groups(pattern) + # Subtract non-capturing group count + if len(group_keys) != group_count: + raise ValueError( + f"The number of group keys ({len(group_keys)}) does not match " + f"the number of match groups ({group_count})." + ) + return group_keys + + def test_pattern(self) -> bool: + """Test the pattern by asserting self.example_match.groups.values()) == + self.expected_groups""" + if list(self.example_match.groups.values()) == self.expected_groups: + return True + else: + return False + + +class MatchResult(OswBaseModel): + match: Union[re.Match, None] + pattern: Union[RegExPatternExtended, None] + + class Config: + arbitrary_types_allowed = True + + @property + def groups(self): + """Return a dictionary representation of the object, enabling accessing the + match groups by key (as defined in RegExPatternExtended).""" + if self.match is None or self.pattern is None: + return None # [] + keys = self.pattern.group_keys + return { + key: get_match_group_by_key(match_result=self, group_key=key) + for key in keys + } + + +class SearchResult(MatchResult): + pass + + +# Functions +def count_match_groups(pattern: Union[str, re.Pattern]): + """Counts the number of match groups in a regular expression pattern. + + Parameters + ---------- + pattern: + The regular expression pattern. + + Returns + ------- + group_count: + The number of match groups in the pattern. + + Examples + -------- + >>> pat = r"(\\d+)(\\w+)\\((\\d+)\\)" + >>> grp_cnt = count_match_groups(pattern) + >>> print(f"Number of match groups: {group_count}") + """ + if isinstance(pattern, re.Pattern): + pattern = pattern.pattern + # Remove escaped parentheses from the pattern + pattern = re.sub(r"\\[()]", "", pattern) + + # Find all opening and closing parentheses + open_brackets = re.findall(r"\(", pattern) + close_brackets = re.findall(r"\)", pattern) + + # Count the number of valid match groups + group_count = 0 + unmatched_brackets = 0 + + for _ in open_brackets: + unmatched_brackets += 1 + + for _ in close_brackets: + if unmatched_brackets > 0: + group_count += 1 + unmatched_brackets -= 1 + + corrected_group_count = group_count - pattern.count("(?:") + return corrected_group_count + + +def match_regex_patterns( + patterns: Union[RegExPatternExtended, List[RegExPatternExtended]], + strings: Union[str, List[str]], + stop_at_first_match: bool = False, +) -> Dict[str, List[MatchResult]]: + """Match a list of strings against a list of regular expression patterns. If + stop_at_first_match is True, the function will stop matching a string as soon as it + finds a match.""" + if isinstance(patterns, RegExPatternExtended): + patterns = [patterns] + if isinstance(strings, str): + strings = [strings] + matching_results = dict() + for _k, string in enumerate(strings): + string_matched = False + matching_results[string] = list() + for _i, pattern in enumerate(patterns): + match = re.match(pattern.pattern, string) + if match is not None: + string_matched = True + matching_results[string].append( + MatchResult(match=match, pattern=pattern) + ) + if stop_at_first_match is True: + break + if string_matched is False: + matching_results[string].append(MatchResult(match=None, pattern=None)) + return matching_results + + +def match_first_regex_pattern( + patterns: Union[RegExPatternExtended, List[RegExPatternExtended]], + strings: Union[str, List[str]], +) -> List[MatchResult]: + """Match a list of strings against a list of regular expression patterns. Return + the first match for each string.""" + if isinstance(patterns, RegExPatternExtended): + patterns = [patterns] + if isinstance(strings, str): + strings = [strings] + matching_results = match_regex_patterns(patterns, strings, stop_at_first_match=True) + return [matching_results[string][0] for string in strings] + + +def get_match_group_by_key( + match_result: MatchResult, group_key: str +) -> Union[str, None]: + """Get the value of a named group in a match result.""" + if match_result.match is None: + return None + if group_key not in match_result.pattern.group_keys: + raise ValueError(f"Group '{group_key}' not found in pattern.") + # +1 is very important here, because the first group is the whole match + group_index = match_result.pattern.group_keys.index(group_key) + 1 + return match_result.match.group(group_index) + + +def test_regex_pattern( + patterns: Union[ + str, List[str], dict, RegExPatternExtended, List[RegExPatternExtended] + ], + strings: List[str], +) -> dict: + """Tests one or a list of regular expression patterns against a list of strings. + Returns a dictionary with the results. The first pattern is evaluated first, + and every following pattern is evaluated regarding the additional matches found. + + Parameters + ---------- + patterns: + A regular expression pattern string, a list of such strings or a dictionary + as in osw.data_import_utility.REGEX_PATTERNS + strings: + A list of strings to test the patterns against. + + Returns + ------- + + """ + if isinstance(patterns, str): + patterns = [ + RegExPatternExtended( + pattern=patterns, + description="No description", + group_keys=[], + ) + ] + if isinstance(patterns, list): + if isinstance(patterns[0], str): + patterns = [ + RegExPatternExtended( + pattern=pattern, + description="No description", + group_keys=[], + ) + for pattern in patterns + ] + if isinstance(patterns, dict): + patterns = [ + RegExPatternExtended( + pattern=val["Pattern"], + description=key, + group_keys=val["Groups"], + example_str=val.get("Example string", None), + ) + for key, val in patterns.items() + ] + matching_results = list() + for _i, pattern in enumerate(patterns): + result = { + "Pattern": pattern.pattern, + "Matching strings": [], + "Match objects": [], + "No match": [], + "Pattern description": pattern.description, + "Group keys": pattern.group_keys, + } + for string in strings: + match = re.match(pattern.pattern, string) + match_result = MatchResult(match=match, pattern=pattern) + if match is None: + result["No match"].append(string) + else: + result["Matching strings"].append(string) + result["Match objects"].append(match_result) + result["Coverage per pattern"] = len(result["Matching strings"]) / len(strings) + matching_results.append(result) + + coverage = dict() + former_matches = list() + for i, pattern in enumerate(patterns): + if pattern.description != "No description": + key = pattern.description + else: + key = i + previously_unmatched = list( + set(matching_results[i]["Matching strings"]) - set(former_matches) + ) + coverage[key] = { + "Pattern": pattern, + "Pattern description": pattern.description, + "Group keys": pattern.group_keys, + "Matching strings": matching_results[i]["Matching strings"], + "Matches, groups": [ + [group for group in match_result.match.groups()] + for match_result in matching_results[i]["Match objects"] + ], + "Additional matches": previously_unmatched, + "Additional matches, groups": [ + [group for group in match_result.match.groups()] + for match_result in matching_results[i]["Match objects"] + if match_result.match.string in previously_unmatched + ], + "Additional coverage": len(previously_unmatched) / len(strings), + "Coverage per pattern": matching_results[i]["Coverage per pattern"], + "Cumulative coverage": (len(former_matches) + len(previously_unmatched)) + / len(strings), + } + former_matches.extend(previously_unmatched) + + coverage["Accumulative coverage"] = sum( + [len(result["Matching strings"]) for result in matching_results] + ) / len(strings) + total_coverage = len(former_matches) / len(strings) + coverage["Total coverage"] = total_coverage + coverage["Not covered"] = 1 - total_coverage + coverage["Unmatched strings"] = list(set(strings) - set(former_matches)) + + return {"Results of regex matching": matching_results, "Coverage": coverage} diff --git a/src/osw/utils/regex_pattern.py b/src/osw/utils/regex_pattern.py index 37f5afb5..7aeae351 100644 --- a/src/osw/utils/regex_pattern.py +++ b/src/osw/utils/regex_pattern.py @@ -1,4 +1,4 @@ -from osw.utils.strings import RegExPatternExtended +from osw.utils.regex import RegExPatternExtended REGEX_PATTERN_LIST = [ RegExPatternExtended( diff --git a/src/osw/utils/strings.py b/src/osw/utils/strings.py index 639aa228..835b3e9d 100644 --- a/src/osw/utils/strings.py +++ b/src/osw/utils/strings.py @@ -1,362 +1,4 @@ -import re -from typing import Dict, List, Optional, Union - -from pydantic.v1 import validator - -from osw.model.static import OswBaseModel - - -# Classes -class RegExPatternExtended(OswBaseModel): - """A class that extends the re.Pattern class with additional attributes. - - Example - ------- - Definition - >>> pattern = RegExPatternExtended( - >>> description="File page full page title (new format) to label", - >>> pattern=r"File:(OS[WL]{1}[[a-f0-9]{32})" - >>> "(\\.[a-zA-Z0-9]*[a-zA-Z]+[a-zA-Z0-9]*)", - >>> group_keys=["Label", "Suffix(es)"], # ["OSW-ID", "Suffix(es)"] - >>> example_str="File:OSW11a53cdfbdc24524bf8ac435cbf65d9d.svg", - >>> expected_groups=["OSW11a53cdfbdc24524bf8ac435cbf65d9d", ".svg"], - >>> ) - Usage - >>> some_str = pattern.example_str - >>> match = pattern.match(some_str) - >>> label = match.groups["label"] - >>> suffix = match.groups["Suffix(es)"] - Testing - >>> pattern.test_pattern() - - - Resources - --------- - https://regex101.com/ - """ - - pattern: Union[str, re.Pattern] - """The regular expression pattern. Can be a string or a compiled pattern.""" - description: str - """A (unique!) description of the pattern. This is used as a key in a dictionary - containing all patterns and acting as a library.""" - group_keys: List[str] - """A list of keys that can be used to access the match groups in the match - result. The number of keys must match the number of match groups in the pattern. - So the first key (list index 0) will be used to access the first match group - (index 1!).""" - example_str: Optional[str] - """An example string that can be used to test the pattern.""" - expected_groups: Optional[List[str]] - """A list strings that are expected in the example_match.groups.values(). This is - used to test the pattern by asserting list(self.example_match.groups.values()) == - self.expected_groups""" - - class Config: - arbitrary_types_allowed = True - - def __init__(self, **data): - super().__init__(**data) - if isinstance(self.pattern, str): - self.pattern = re.compile(self.pattern) - - def match(self, string: str) -> "MatchResult": - """Return the match result of the given string and the pattern.""" - return MatchResult(match=re.match(self.pattern, string), pattern=self) - - def search(self, string: str) -> "SearchResult": - """Return the search result of the given string and the pattern.""" - return SearchResult(match=re.search(self.pattern, string), pattern=self) - - def finditer(self, string: str) -> List["MatchResult"]: - """Return a list of all match results of the given string and the pattern.""" - return [ - MatchResult(match=match, pattern=self) - for match in re.finditer(self.pattern, string) - ] - - def findall(self, string: str) -> Dict[str, List[str]]: - """Return a list of all match groups of the given string and the pattern.""" - result = {} - for grp_key in self.group_keys: - result[grp_key] = [mr.groups[grp_key] for mr in self.finditer(string)] - return result - - def findall_by_group_key(self, string: str, group_key: str) -> List[str]: - """Return a list of all match groups of the given string and the pattern by - group key.""" - if group_key not in self.group_keys: - raise ValueError(f"Group key '{group_key}' not found in pattern.") - return self.findall(string)[group_key] - - @property - def example_match(self) -> "MatchResult": - """Return the match result of the example string and the pattern.""" - return self.match(string=self.example_str) - - @validator("group_keys") - def validate_group_keys(cls, group_keys, values) -> List[str]: - """Validate that the number of group keys matches the number of match groups, - defined in the pattern.""" - pattern = values.get("pattern") - group_count = count_match_groups(pattern) - # Subtract non-capturing group count - if len(group_keys) != group_count: - raise ValueError( - f"The number of group keys ({len(group_keys)}) does not match " - f"the number of match groups ({group_count})." - ) - return group_keys - - def test_pattern(self) -> bool: - """Test the pattern by asserting self.example_match.groups.values()) == - self.expected_groups""" - if list(self.example_match.groups.values()) == self.expected_groups: - return True - else: - return False - - -class MatchResult(OswBaseModel): - match: Union[re.Match, None] - pattern: Union[RegExPatternExtended, None] - - class Config: - arbitrary_types_allowed = True - - @property - def groups(self): - """Return a dictionary representation of the object, enabling accessing the - match groups by key (as defined in RegExPatternExtended).""" - if self.match is None or self.pattern is None: - return None # [] - keys = self.pattern.group_keys - return { - key: get_match_group_by_key(match_result=self, group_key=key) - for key in keys - } - - -class SearchResult(MatchResult): - pass - - # Functions -def count_match_groups(pattern: Union[str, re.Pattern]): - """Counts the number of match groups in a regular expression pattern. - - Parameters - ---------- - pattern: - The regular expression pattern. - - Returns - ------- - group_count: - The number of match groups in the pattern. - - Examples - -------- - >>> pat = r"(\\d+)(\\w+)\\((\\d+)\\)" - >>> grp_cnt = count_match_groups(pattern) - >>> print(f"Number of match groups: {group_count}") - """ - if isinstance(pattern, re.Pattern): - pattern = pattern.pattern - # Remove escaped parentheses from the pattern - pattern = re.sub(r"\\[()]", "", pattern) - - # Find all opening and closing parentheses - open_brackets = re.findall(r"\(", pattern) - close_brackets = re.findall(r"\)", pattern) - - # Count the number of valid match groups - group_count = 0 - unmatched_brackets = 0 - - for _ in open_brackets: - unmatched_brackets += 1 - - for _ in close_brackets: - if unmatched_brackets > 0: - group_count += 1 - unmatched_brackets -= 1 - - corrected_group_count = group_count - pattern.count("(?:") - return corrected_group_count - - -def match_regex_patterns( - patterns: Union[RegExPatternExtended, List[RegExPatternExtended]], - strings: Union[str, List[str]], - stop_at_first_match: bool = False, -) -> Dict[str, List[MatchResult]]: - """Match a list of strings against a list of regular expression patterns. If - stop_at_first_match is True, the function will stop matching a string as soon as it - finds a match.""" - if isinstance(patterns, RegExPatternExtended): - patterns = [patterns] - if isinstance(strings, str): - strings = [strings] - matching_results = dict() - for _k, string in enumerate(strings): - string_matched = False - matching_results[string] = list() - for _i, pattern in enumerate(patterns): - match = re.match(pattern.pattern, string) - if match is not None: - string_matched = True - matching_results[string].append( - MatchResult(match=match, pattern=pattern) - ) - if stop_at_first_match is True: - break - if string_matched is False: - matching_results[string].append(MatchResult(match=None, pattern=None)) - return matching_results - - -def match_first_regex_pattern( - patterns: Union[RegExPatternExtended, List[RegExPatternExtended]], - strings: Union[str, List[str]], -) -> List[MatchResult]: - """Match a list of strings against a list of regular expression patterns. Return - the first match for each string.""" - if isinstance(patterns, RegExPatternExtended): - patterns = [patterns] - if isinstance(strings, str): - strings = [strings] - matching_results = match_regex_patterns(patterns, strings, stop_at_first_match=True) - return [matching_results[string][0] for string in strings] - - -def get_match_group_by_key( - match_result: MatchResult, group_key: str -) -> Union[str, None]: - """Get the value of a named group in a match result.""" - if match_result.match is None: - return None - if group_key not in match_result.pattern.group_keys: - raise ValueError(f"Group '{group_key}' not found in pattern.") - # +1 is very important here, because the first group is the whole match - group_index = match_result.pattern.group_keys.index(group_key) + 1 - return match_result.match.group(group_index) - - -def test_regex_pattern( - patterns: Union[ - str, List[str], dict, RegExPatternExtended, List[RegExPatternExtended] - ], - strings: List[str], -) -> dict: - """Tests one or a list of regular expression patterns against a list of strings. - Returns a dictionary with the results. The first pattern is evaluated first, - and every following pattern is evaluated regarding the additional matches found. - - Parameters - ---------- - patterns: - A regular expression pattern string, a list of such strings or a dictionary - as in osw.data_import_utility.REGEX_PATTERNS - strings: - A list of strings to test the patterns against. - - Returns - ------- - - """ - if isinstance(patterns, str): - patterns = [ - RegExPatternExtended( - pattern=patterns, - description="No description", - group_keys=[], - ) - ] - if isinstance(patterns, list): - if isinstance(patterns[0], str): - patterns = [ - RegExPatternExtended( - pattern=pattern, - description="No description", - group_keys=[], - ) - for pattern in patterns - ] - if isinstance(patterns, dict): - patterns = [ - RegExPatternExtended( - pattern=val["Pattern"], - description=key, - group_keys=val["Groups"], - example_str=val.get("Example string", None), - ) - for key, val in patterns.items() - ] - matching_results = list() - for _i, pattern in enumerate(patterns): - result = { - "Pattern": pattern.pattern, - "Matching strings": [], - "Match objects": [], - "No match": [], - "Pattern description": pattern.description, - "Group keys": pattern.group_keys, - } - for string in strings: - match = re.match(pattern.pattern, string) - match_result = MatchResult(match=match, pattern=pattern) - if match is None: - result["No match"].append(string) - else: - result["Matching strings"].append(string) - result["Match objects"].append(match_result) - result["Coverage per pattern"] = len(result["Matching strings"]) / len(strings) - matching_results.append(result) - - coverage = dict() - former_matches = list() - for i, pattern in enumerate(patterns): - if pattern.description != "No description": - key = pattern.description - else: - key = i - previously_unmatched = list( - set(matching_results[i]["Matching strings"]) - set(former_matches) - ) - coverage[key] = { - "Pattern": pattern, - "Pattern description": pattern.description, - "Group keys": pattern.group_keys, - "Matching strings": matching_results[i]["Matching strings"], - "Matches, groups": [ - [group for group in match_result.match.groups()] - for match_result in matching_results[i]["Match objects"] - ], - "Additional matches": previously_unmatched, - "Additional matches, groups": [ - [group for group in match_result.match.groups()] - for match_result in matching_results[i]["Match objects"] - if match_result.match.string in previously_unmatched - ], - "Additional coverage": len(previously_unmatched) / len(strings), - "Coverage per pattern": matching_results[i]["Coverage per pattern"], - "Cumulative coverage": (len(former_matches) + len(previously_unmatched)) - / len(strings), - } - former_matches.extend(previously_unmatched) - - coverage["Accumulative coverage"] = sum( - [len(result["Matching strings"]) for result in matching_results] - ) / len(strings) - total_coverage = len(former_matches) / len(strings) - coverage["Total coverage"] = total_coverage - coverage["Not covered"] = 1 - total_coverage - coverage["Unmatched strings"] = list(set(strings) - set(former_matches)) - - return {"Results of regex matching": matching_results, "Coverage": coverage} - - def pascal_case(st: str) -> str: """converts a string to PascalCase diff --git a/tests/utils/utils_test.py b/tests/utils/utils_test.py index e07953cc..c1b2824e 100644 --- a/tests/utils/utils_test.py +++ b/tests/utils/utils_test.py @@ -1,7 +1,8 @@ import uuid import osw.model.entity as model -from osw.utils.strings import count_match_groups, pascal_case +from osw.utils.regex import count_match_groups +from osw.utils.strings import pascal_case from osw.utils.wiki import ( get_full_title, get_namespace, From 95fe87ef6afa51212038d606fa9a671f74e9a00c Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 25 Nov 2024 13:49:19 +0100 Subject: [PATCH 46/52] applying remove_empty instead of remove_empty_string --- src/osw/core.py | 33 +++++++++++++++++---------------- src/osw/utils/wiki.py | 32 ++++++++++++++++++++++++++++++++ tests/test_osl.py | 15 +++++++++------ 3 files changed, 58 insertions(+), 22 deletions(-) diff --git a/src/osw/core.py b/src/osw/core.py index db6d992f..982963ca 100644 --- a/src/osw/core.py +++ b/src/osw/core.py @@ -31,7 +31,7 @@ get_uuid, is_empty, namespace_from_full_title, - remove_empty_strings, + remove_empty, title_from_full_title, ) from osw.wiki_tools import SearchParam @@ -605,8 +605,9 @@ class LoadEntityParam(BaseModel): autofetch_schema: Optional[bool] = True """If true, load the corresponding schemas / categories ad-hoc if not already present""" - remove_empty_strings: Optional[bool] = True - """If true, remove key with an empty string value from the jsondata.""" + remove_empty: Optional[bool] = True + """If true, remove key with an empty string, list, dict or set as value + from the jsondata.""" disable_cache: bool = False """If true, disable the cache for the loading process""" @@ -662,8 +663,8 @@ def load_entity( schemas = [] schemas_fetched = True jsondata = page.get_slot_content("jsondata") - if param.remove_empty_strings: - remove_empty_strings(jsondata) + if param.remove_empty: + remove_empty(jsondata) if jsondata: for category in jsondata["type"]: schema = ( @@ -780,7 +781,7 @@ class _ApplyOverwriteParam(OswBaseModel): namespace: Optional[str] meta_category_title: Optional[str] meta_category_template_str: Optional[str] - remove_empty_strings: Optional[bool] = True + remove_empty: Optional[bool] = True inplace: Optional[bool] = False debug: Optional[bool] = False @@ -861,8 +862,8 @@ def set_content(content_to_set: dict) -> None: ): # Use pydantic serialization, skip none values: new_content["jsondata"] = json.loads(param.entity.json(exclude_none=True)) - if param.remove_empty_strings: - remove_empty_strings(new_content["jsondata"]) + if param.remove_empty: + remove_empty(new_content["jsondata"]) set_content(new_content) page.changed = True return page # Guard clause --> exit function @@ -892,8 +893,8 @@ def set_content(content_to_set: dict) -> None: for slot in ["jsondata", "header", "footer"]: # SLOTS: remote_content[slot] = page.get_slot_content(slot) # Todo: remote content does not contain properties that are not set - if param.remove_empty_strings: - remove_empty_strings(remote_content["jsondata"]) + if param.remove_empty: + remove_empty(remote_content["jsondata"]) if remote_content["header"]: # not None or {} or "" new_content["header"] = remote_content["header"] if remote_content["footer"]: @@ -904,8 +905,8 @@ def set_content(content_to_set: dict) -> None: # Properties that are not set in the local content will be set to None # We want those not to be listed as keys local_content["jsondata"] = json.loads(param.entity.json(exclude_none=True)) - if param.remove_empty_strings: - remove_empty_strings(local_content["jsondata"]) + if param.remove_empty: + remove_empty(local_content["jsondata"]) if param.debug: print(f"'local_content': {str(remote_content)}") # Apply the overwrite logic @@ -981,7 +982,7 @@ class StoreEntityParam(OswBaseModel): """A list of OverwriteClassParam objects. If a class specific overwrite setting is set, this setting is used. """ - remove_empty_strings: Optional[bool] = True + remove_empty: Optional[bool] = True """If true, remove key with an empty string value from the jsondata.""" change_id: Optional[str] = None """ID to document the change. Entities within the same store_entity() call will @@ -1101,15 +1102,15 @@ def store_entity_( namespace=namespace_, policy=overwrite_class_param, meta_category_template_str=meta_category_template_str, - remove_empty_strings=param.remove_empty_strings, + remove_empty=param.remove_empty, debug=param.debug, ) ) if meta_category_template: try: jsondata = page.get_slot_content("jsondata") - if param.remove_empty_strings: - remove_empty_strings(jsondata) + if param.remove_empty: + remove_empty(jsondata) schema_str = eval_compiled_handlebars_template( meta_category_template, jsondata, diff --git a/src/osw/utils/wiki.py b/src/osw/utils/wiki.py index 3590171a..866df235 100644 --- a/src/osw/utils/wiki.py +++ b/src/osw/utils/wiki.py @@ -106,3 +106,35 @@ def remove_empty_strings(d: dict, inplace: bool = True) -> dict: if value == "": del d[key] return d + + +def remove_empty(d: dict, inplace: bool = True) -> dict: + """Iterates through the dictionary structure and removes key-value pairs + where the value is an empty string, list, set or dictionary + + Parameters + ---------- + d: + The dictionary to perform the operation on + inplace: + Whether to perform the operation in place or return a new dictionary + + Returns + ------- + result: + The modified dictionary + """ + if not inplace: + d = deepcopy(d) + keys = list(d.keys()) + for key in keys: + value = d[key] + if isinstance(value, dict): + remove_empty(value) + elif isinstance(value, list): + for item in value: + if isinstance(item, dict): + remove_empty(item) + if value in ["", [], {}, set()]: + del d[key] + return d diff --git a/tests/test_osl.py b/tests/test_osl.py index b5f826da..af33cb2e 100644 --- a/tests/test_osl.py +++ b/tests/test_osl.py @@ -4,7 +4,7 @@ import osw.model.entity as model from osw.core import OSW, AddOverwriteClassOptions, OverwriteOptions -from osw.utils.wiki import remove_empty_strings +from osw.utils.wiki import remove_empty from osw.wtsite import WtPage @@ -58,8 +58,8 @@ def check_false(original: model.Entity, altered: model.Entity, stored: model.Ent assert stored.name == original.name # empty string property is removed on store and load: assert stored.iri == altered.iri - if len(original.description) == 0: - assert stored.description == original.description + if original.description is None or len(original.description) == 0: + assert stored.description == altered.description else: assert stored.description[0].text == original.description[0].text assert stored.query_label == altered.query_label # value == None --> @@ -108,7 +108,10 @@ def check_keep_existing( # assert stored.iri == original.iri # empty string property is removed on store and load: assert getattr(stored, "iri", None) is None - assert stored.description == original.description # empty list + if original.description is None or len(original.description) == 0: # empty list + assert stored.description is None + else: + assert stored.description[0].text == original.description[0].text assert stored.query_label == original.query_label assert stored.image == original.image assert getattr(stored, "attachments", None) is None @@ -154,9 +157,9 @@ def test_apply_overwrite_policy(): + OSW.get_osw_id(original_item_local.uuid) ) jsondata = json.loads(original_item_local.json(exclude_none=True)) - # Emulate the default setting for StoreEntityParam.remove_empty_strings, + # Emulate the default setting for StoreEntityParam.remove_empty, # which is True and applied on store_entity - remove_empty_strings(jsondata) + remove_empty(jsondata) original_page.set_slot_content("jsondata", jsondata) # To reproduce what happens in the OSL instance, we need to get the content From aa7a23844fc2d6722140c45f2ec2cad159d6a811 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 25 Nov 2024 14:10:43 +0100 Subject: [PATCH 47/52] Renaming custom FilePath to PossibleFilePath --- src/osw/auth.py | 12 ++++++++---- src/osw/custom_types.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/osw/auth.py b/src/osw/auth.py index 420b6a13..85b8aff1 100644 --- a/src/osw/auth.py +++ b/src/osw/auth.py @@ -9,7 +9,7 @@ import yaml from pydantic.v1 import PrivateAttr -from osw.custom_types import FilePath +from osw.custom_types import PossibleFilePath from osw.model.static import OswBaseModel CREDENTIALS_FN_DEFAULT = "credentials.pwd.yaml" @@ -18,9 +18,13 @@ class CredentialManager(OswBaseModel): """Handles credentials""" - cred_filepath: Optional[Union[Union[str, FilePath], List[Union[str, FilePath]]]] + cred_filepath: Optional[ + Union[Union[str, PossibleFilePath], List[Union[str, PossibleFilePath]]] + ] """Filepath to yaml file with credentials for osw and connected services""" - cert_filepath: Optional[Union[Union[str, FilePath], List[Union[str, FilePath]]]] + cert_filepath: Optional[ + Union[Union[str, PossibleFilePath], List[Union[str, PossibleFilePath]]] + ] """Filepath to the certificates for osw and connected services""" _credentials: List[BaseCredential] = PrivateAttr([]) @@ -234,7 +238,7 @@ def iri_in_file(self, iri: str) -> bool: def save_credentials_to_file( self, - filepath: Union[str, FilePath] = None, + filepath: Union[str, PossibleFilePath] = None, set_cred_filepath: bool = False, ): """Saves the in memory credentials to a file diff --git a/src/osw/custom_types.py b/src/osw/custom_types.py index 8a2aa53b..f785bc80 100644 --- a/src/osw/custom_types.py +++ b/src/osw/custom_types.py @@ -10,7 +10,7 @@ from types import NoneType # noqa: F401 -class FilePath(PydanticFilePath): +class PossibleFilePath(PydanticFilePath): # Overwrite the Pydantic FilePath class to allow non-existing paths @classmethod def __get_validators__(cls): From ed7f4de406df59b2c3bf7b942cc2d1629f0ed5cc Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 25 Nov 2024 14:30:20 +0100 Subject: [PATCH 48/52] Additional info on usage in Google Colab --- docs/tutorials/basics.ipynb | 348 +++++++++++++++++++----------------- 1 file changed, 183 insertions(+), 165 deletions(-) diff --git a/docs/tutorials/basics.ipynb b/docs/tutorials/basics.ipynb index 690560af..2ba36980 100644 --- a/docs/tutorials/basics.ipynb +++ b/docs/tutorials/basics.ipynb @@ -27,14 +27,31 @@ "cell_type": "markdown", "source": [ "## Optional: Install & run in online notebook\n", - "In case you want to run this tutorial in a throw-away environment, you can use an online notebook service, e.g., \n", - "Google Colab. " + "In case you want to run this tutorial in a throw-away environment, you can use an online notebook service, e.g.,\n", + "Google Colab.\n", + "- (If you haven't already) Open this tutorial in the notebook service\n", + " - Select `Open from GitHub` and insert https://github.com/OpenSemanticLab/osw-python/blob/main/docs/tutorials/basics.ipynb\n", + " - For Google Colab just click [Tutorial: Basics](https://colab.research.google.com/github/OpenSemanticLab/osw-python/blob/main/docs/tutorials/basics.ipynb)\n", + "- Execute the following cell" ], - "id": "3f70a55ea4a901c5" + "id": "b5fac358813d0bd6" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "# Optional: Clone the repository and install the package with the tutorial dependencies\n", + "!git clone https://github.com/OpenSemanticLab/osw-python.git\n", + "%cd osw-python\n", + "!pip install -e .[tutorial]" + ], + "id": "a805b5dcaf0beaf7" }, { "cell_type": "markdown", - "id": "64e0121de7a40c80", + "id": "6bef0569b5824ea0", "metadata": {}, "source": [ "## OSL data model\n", @@ -106,7 +123,7 @@ }, { "cell_type": "markdown", - "id": "6bef0569b5824ea0", + "id": "6e259e4f34e709ea", "metadata": {}, "source": [ "### JSON / JSON-Schema\n", @@ -212,14 +229,15 @@ }, { "cell_type": "markdown", - "id": "6e259e4f34e709ea", + "id": "c089a8c6224ff1be", "metadata": {}, "source": [ "\n", "\n", "## Installation\n", "\n", - "In case you just want to try the package in a throw-away environment, refer to the section [Optional: Run in online notebook](#Optional-Run-in-online-notebook) Jupyter notebook.\n", + "In case you just want to try the package in a throw-away environment, refer to the section [Optional: Install & run\n", + "in online notebook](#optional-install--run-in-online-notebook)) Jupyter notebook.\n", "\n", "### Downloading the library (optional)\n", "The osw-python library is available as GitHub repository and can be downloaded as a ZIP file or via git:\n", @@ -252,9 +270,8 @@ ] }, { - "cell_type": "markdown", - "id": "c089a8c6224ff1be", "metadata": {}, + "cell_type": "markdown", "source": [ "## Connecting to an OSL instance\n", "\n", @@ -292,12 +309,12 @@ "credentials file. \n", "\n", "This step is required to download all dependencies (data models) of OswExpress from the OSL instance." - ] + ], + "id": "137e9b3144a4db96" }, { - "cell_type": "code", - "id": "162b6208a105bbde", "metadata": {}, + "cell_type": "code", "source": [ "from osw.express import OswExpress\n", "# Some modules that will be required several times\n", @@ -306,19 +323,20 @@ "from pathlib import Path\n", "import pandas as pd" ], + "id": "509040ca218924ed", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# Define the wiki_domain for later reuse:\n", "wiki_domain = \"demo.open-semantic-lab.org\" # Replace with the domain of your OSL instance" ], - "id": "509040ca218924ed", - "outputs": [], - "execution_count": null + "id": "7d0db4de67f448b9" }, { "metadata": {}, @@ -330,38 +348,38 @@ "\n", "Else you will be prompted to enter your username and password." ], - "id": "4728ef9d8456dc26" + "id": "5e7595bf60e9a247" }, { - "cell_type": "code", - "id": "54f082d780c3dd79", "metadata": {}, - "source": "osw_obj = OswExpress(domain=wiki_domain) ", + "cell_type": "code", "outputs": [], - "execution_count": null + "execution_count": null, + "source": "osw_obj = OswExpress(domain=wiki_domain) ", + "id": "1519ec4f2ef7fde0" }, { - "cell_type": "markdown", - "id": "c735e2f5c4f55c05", "metadata": {}, + "cell_type": "markdown", "source": [ "#### Option 2: Provide a credentials file (path)\n", "\n", "If the file does not exist or the domain is not in the file, you will be prompted to enter your username and password.\n", "Unknown domains will be appended to the file." - ] + ], + "id": "f9754e95cfa0d483" }, { - "cell_type": "code", - "id": "24810957b39bfd3a", "metadata": {}, - "source": "osw_obj = OswExpress(domain=wiki_domain, cred_filepath=\"credentials.pwd.yaml\")", + "cell_type": "code", "outputs": [], - "execution_count": null + "execution_count": null, + "source": "osw_obj = OswExpress(domain=wiki_domain, cred_filepath=\"credentials.pwd.yaml\")", + "id": "a2be66e5d7ecc1cc" }, { "cell_type": "markdown", - "id": "9022d39338ed267f", + "id": "537cdcc821e4803e", "metadata": {}, "source": [ "## Downloading data model dependencies\n", @@ -378,9 +396,8 @@ ] }, { - "cell_type": "markdown", - "id": "537cdcc821e4803e", "metadata": {}, + "cell_type": "markdown", "source": [ "### Identify required data models\n", "\n", @@ -423,12 +440,14 @@ "> [!NOTE]\n", "> If you find no category, ask your administrator to install page packages via the special page 'Special:Packages'. \n", "> Page packages are maintained via [GitHub](https://github.com/OpenSemanticWorld-Packages/osw-package-maintenance)" - ] + ], + "id": "1885ed16f44d97b1" }, { - "cell_type": "code", - "id": "7381372e09b55c7a", "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "dependencies = {\n", " \"Organization\": \"Category:OSW1969007d5acf40539642877659a02c23\", # Will fetch: Organization, OrganizationalUnit\n", @@ -437,12 +456,11 @@ " \"Tutorial\": \"Category:OSW494f660e6a714a1a9681c517bbb975da\", # Will fetch: Tutorial, Article, CreativeWork\n", "}" ], - "outputs": [], - "execution_count": null + "id": "f94aec7bab90dfc4" }, { "cell_type": "markdown", - "id": "da879e15b57f0c3e", + "id": "7a8684bb1b8779fd", "metadata": {}, "source": [ "> [!NOTE]\n", @@ -451,9 +469,8 @@ ] }, { - "cell_type": "markdown", - "id": "7a8684bb1b8779fd", "metadata": {}, + "cell_type": "markdown", "source": [ "### Installing data models\n", "\n", @@ -464,12 +481,14 @@ "#### Option 1: Installing dependencies before import from osw.model.entity\n", "\n", "This option is recommended to put in a separate script, which is run before the main script." - ] + ], + "id": "75206bbf0a1e0dc9" }, { - "cell_type": "code", - "id": "39260a2e792deb47", "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# Will run everytime the script is executed:\n", "osw_obj.install_dependencies(dependencies)\n", @@ -477,23 +496,23 @@ "# Static code checker will note 'Module not found' before the installation:\n", "from osw.model.entity import Description, Label, Organization, Person, Room, Tutorial" ], - "outputs": [], - "execution_count": null + "id": "369bb36b7c805028" }, { - "cell_type": "markdown", - "id": "eb01d61d5c5586ee", "metadata": {}, + "cell_type": "markdown", "source": [ "#### Option 2: Use OswExpress comfort function for imports\n", "\n", "This option is recommended to put in the main script, before the first `from osw.model.entity import` statement." - ] + ], + "id": "3b63af956676181b" }, { - "cell_type": "code", - "id": "a7c8b1233c5817b3", "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "from typing import TYPE_CHECKING\n", "from osw.express import import_with_fallback\n", @@ -506,13 +525,11 @@ "if TYPE_CHECKING:\n", " from osw.model.entity import Description, Label, Organization, Person, Room, Tutorial" ], - "outputs": [], - "execution_count": null + "id": "30a3c01428c6120d" }, { - "cell_type": "markdown", - "id": "40dc1839eeb1e503", "metadata": {}, + "cell_type": "markdown", "source": [ "## Interacting with an entity\n", "\n", @@ -523,12 +540,14 @@ "To create an entity, you need to create an instance of the respective data class. The `__init__` method of the data \n", "class expects keyword arguments for all fields. As per usual for Pydantic models, positional arguments are not \n", "permitted and the input data is validated during initialization." - ] + ], + "id": "35b4a3429feaf2d8" }, { - "cell_type": "code", - "id": "fb10f35fd3927b6a", "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# Create a person\n", "john = Person(\n", @@ -543,13 +562,11 @@ "# - surname: field required\n", "# - email: value is not a valid set" ], - "outputs": [], - "execution_count": null + "id": "459d6ac5804389a2" }, { - "cell_type": "markdown", - "id": "2beecba33c4647ce", "metadata": {}, + "cell_type": "markdown", "source": [ "Lets breakdown what happened here\n", "\n", @@ -558,12 +575,14 @@ "- The extra field `last_name` was provided, but it was not expected. By default, Pydantic models disregard extra \n", "fields without warning.\n", "- The `email` field is expected to be a list of strings, but a string was provided. " - ] + ], + "id": "f634abd07a3568a" }, { - "cell_type": "code", - "id": "eaaf78110e18c493", "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# Should run without validation errors\n", "john = Person(\n", @@ -573,22 +592,22 @@ " email=[\"john.doe@example.com\"],\n", ")" ], - "outputs": [], - "execution_count": null + "id": "9dd23162dbe110ef" }, { - "cell_type": "markdown", - "id": "9aa534b206d8b486", "metadata": {}, + "cell_type": "markdown", "source": [ "Before storing the entity in the OSL instance, lets check at which IRI (full page title) it will be stored.\n", "The IRI is derived from the `uuid` and the `namespace` of the entity." - ] + ], + "id": "fffbf93fcb44ff57" }, { - "cell_type": "code", - "id": "d861fde54918bc05", "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "print(\"Page label:\", john.label[0].text)\n", "print(\"Machine-readable page name:\", john.name)\n", @@ -597,22 +616,23 @@ "print(\"OSW-ID:\", john.get_osw_id())\n", "print(\"IRI:\", john.get_iri())" ], - "outputs": [], - "execution_count": null + "id": "89b4c364b9f365c2" }, { - "cell_type": "markdown", - "id": "f6dace23bf99c7f3", "metadata": {}, + "cell_type": "markdown", "source": [ "### Storing an entity\n", "\n", "We can now store this entity in the OSL instance. The `store_entity` method uploads the entity to the OSL instance. " - ] + ], + "id": "2c88920949485b7" }, { "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# Let's document all storing processes in a list\n", "stored = []\n", @@ -629,13 +649,12 @@ ") # All default values included\n", "# osw_obj.store_entity(params)" ], - "id": "78ecbe8be2788f1d", - "outputs": [], - "execution_count": null + "id": "99cf8828bee651ee" }, { - "metadata": {}, "cell_type": "markdown", + "id": "639db64348bbd905", + "metadata": {}, "source": [ "Like most methods and functions in the osw-python library, the `store_entity` takes only a single argument. Usually \n", "either a specific object type or a dedicated params object is accepted. If an object of type other than the params \n", @@ -644,24 +663,23 @@ "\n", "This ensures that the method signature is as simple as possible and at the same time allows full typing and validation \n", "of input parameters that the method is easy to use. " - ], - "id": "2e30dd888a08ca78" + ] }, { - "cell_type": "markdown", - "id": "639db64348bbd905", "metadata": {}, + "cell_type": "markdown", "source": [ "### Downloading an entity\n", "\n", "To download an entity, you need to provide the IRI of the entity. The `download_entity` method downloads the\n", "entity from the OSL instance and returns an instance of the respective data class. If the respective data class is \n", "not already part of `osw.model.entity`, the data class is generated on-the-fly by default." - ] + ], + "id": "ae4da6692096e045" }, { "cell_type": "code", - "id": "ea1da2ff6d96808a", + "id": "19d1c6767b2098b", "metadata": {}, "source": [ "john2 = osw_obj.load_entity(john.get_iri())\n", @@ -671,16 +689,16 @@ "execution_count": null }, { - "cell_type": "code", - "id": "19d1c6767b2098b", "metadata": {}, - "source": "pprint(john2.dict())", + "cell_type": "code", "outputs": [], - "execution_count": null + "execution_count": null, + "source": "pprint(john2.dict())", + "id": "5e98d8915a3bf440" }, { "cell_type": "markdown", - "id": "2ab58db05b3b004b", + "id": "d140b974b54155ce", "metadata": {}, "source": [ "Besides the attributes that we set (first_name, surname, email), the downloaded entity has additional attributes, \n", @@ -693,20 +711,19 @@ ] }, { - "cell_type": "markdown", - "id": "d140b974b54155ce", "metadata": {}, + "cell_type": "markdown", "source": [ "### Modifying an entity\n", "\n", "To modify an entity, you can change the attributes of the entity instance. The attributes can be accessed and modified \n", "like any other attribute of a Python object." - ] + ], + "id": "b8713c64622ea4fe" }, { - "cell_type": "code", - "id": "c0511efb825acf10", "metadata": {}, + "cell_type": "code", "source": [ "# Adding a new attribute\n", "john2.middle_name = {\"R.\"}\n", @@ -717,12 +734,15 @@ "# Checking the made changes:\n", "pprint(john2.dict())" ], + "id": "b4425d8e6b1cbe62", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# Let's create an organization and link it to the person\n", "org = Organization(\n", @@ -738,13 +758,12 @@ " member_ous=[org.get_iri()]\n", ")" ], - "id": "b4425d8e6b1cbe62", - "outputs": [], - "execution_count": null + "id": "1247b70481054611" }, { - "metadata": {}, "cell_type": "markdown", + "id": "f42c1600ed17a059", + "metadata": {}, "source": [ "Here we have created links between Entities for the first time. The `get_iri` method returns the full title of the to\n", " be created page(s), which replaces the pointers Python use where ever you reference an object by variable. JSON has\n", @@ -752,20 +771,19 @@ "\n", "Note: We are already working on a feature to automatically resolve these references, so you can use the Python\n", "pointers in the future!" - ], - "id": "df52cc2f7dd2f13" + ] }, { - "cell_type": "markdown", - "id": "f42c1600ed17a059", "metadata": {}, + "cell_type": "markdown", "source": [ "### Storing an altered entity\n", "\n", "Here the same applies as for [Storing an entity](#Storing-an-entity). BUT: overwriting entities is not possible with \n", "default settings \"keep existing\". Therefore, you need to call the method `store_entity` passing a `StoreEntityParam` with\n", "the attribute `overwrite` set to \"overwrite\".\n" - ] + ], + "id": "bd30cfbba5d56386" }, { "metadata": {}, @@ -774,7 +792,7 @@ "# Define entities to be stored\n", "entities2store = [john2, org, org2]" ], - "id": "a7f9540af0850525", + "id": "77b507f13be1235b", "outputs": [], "execution_count": null }, @@ -786,7 +804,7 @@ "res = osw_obj.store_entity(prm.StoreEntityParam(entities=entities2store, overwrite=True))\n", "stored.append(res)" ], - "id": "77b507f13be1235b", + "id": "19d4d48e95e0fe82", "outputs": [], "execution_count": null }, @@ -799,13 +817,15 @@ " entities=entities2store, overwrite_per_class=[prm.OverwriteClassParam(model=Person, overwrite=True)]))\n", "stored.append(res)" ], - "id": "19d4d48e95e0fe82", + "id": "4fc037a10f95356a", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# Option 3: Overwrite only the email attribute of entities of type Person\n", "res = osw_obj.store_entity(prm.StoreEntityParam(\n", @@ -813,9 +833,7 @@ " prm.OverwriteClassParam(model=Person, overwrite=False,per_property={\"email\": True})]))\n", "stored.append(res)" ], - "id": "4fc037a10f95356a", - "outputs": [], - "execution_count": null + "id": "2019c0d5301ab1de" }, { "metadata": {}, @@ -839,7 +857,7 @@ " - `prm.AddOverwriteClassOptions.keep_existing`: \"keep existing\" - keep the remote entity, if one exists under this\n", " OSW-ID" ], - "id": "133a7c6940f23ae6" + "id": "2107b3efcd22837f" }, { "metadata": {}, @@ -867,7 +885,7 @@ " - Explore the property `HasMember` to find the organization `Doe, Inc.`.\n", " - Explore the property `HasManager` to find the person `John Doe`." ], - "id": "2107b3efcd22837f" + "id": "2dc425de23d5e2ce" }, { "metadata": {}, @@ -881,11 +899,13 @@ "available. (Execute the Upload a file section to upload a file to the OSL instance.) You can download the file with \n", "just two lines:\n" ], - "id": "2dc425de23d5e2ce" + "id": "8611c346fba59805" }, { "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "from osw.express import osw_download_file\n", "local_file = osw_download_file(\n", @@ -894,9 +914,7 @@ " # , overwrite=True # Can be used to avoid overwriting an existing file\n", ")" ], - "id": "9188d8aac00ef04", - "outputs": [], - "execution_count": null + "id": "49d395dd958beff7" }, { "metadata": {}, @@ -905,7 +923,7 @@ "The object `local_file` is an instance of `OswExpress.DownloadFileResult` and contains the path to the downloaded file, \n", "which is accessible via:" ], - "id": "59162da197057ee8" + "id": "5215e1b89b4ba89f" }, { "metadata": {}, @@ -914,11 +932,13 @@ "The class `OswExpress.DownloadFileResult` implements all dunder methods required for a context manager. Therefore, it\n", " can be used with the `with` statement to ensure the file is closed properly after use:" ], - "id": "5215e1b89b4ba89f" + "id": "c4a6ed35e633a34c" }, { "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "with osw_download_file(\n", " \"https://demo.open-semantic-lab.org/wiki/File:OSW2c08f45151ee4b518485ed68cf73c5e5.txt\", overwrite=True\n", @@ -926,9 +946,7 @@ " file.open(\"r\")\n", " print(file.read())" ], - "id": "ff674c1413ca6b4b", - "outputs": [], - "execution_count": null + "id": "861724df8468d97" }, { "metadata": {}, @@ -939,7 +957,7 @@ "Let's create a file, upload it to the OSL instance, download it and read and alter its content, before uploading it \n", "again.\n" ], - "id": "418b47e58febe97d" + "id": "efcb9b2381ad388d" }, { "metadata": {}, @@ -975,21 +993,21 @@ "modified_wiki_file = osw_upload_file(local_file)\n", "stored.append(modified_wiki_file)" ], - "id": "14a0ee12dde2961e", + "id": "9647b490f90d2a0d", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# Delete WikiFile from OSW instance after you are done with it\n", "wiki_file.delete()\n", "# Error: InsufficientPermission, if you do not have the required rights" ], - "id": "9647b490f90d2a0d", - "outputs": [], - "execution_count": null + "id": "ca65732aeed620f6" }, { "metadata": {}, @@ -1001,7 +1019,7 @@ "\n", "Let's create a demo table and save it to an Excel file. " ], - "id": "2095cc63bd256a2c" + "id": "e7e9cc49527fab6b" }, { "metadata": {}, @@ -1019,7 +1037,7 @@ "df.to_excel(\"demo.xlsx\", index=False)\n", "del df" ], - "id": "64154efbfd92f87", + "id": "ed6cd2984e10baca", "outputs": [], "execution_count": null }, @@ -1034,13 +1052,15 @@ "john_dict = data_df.iloc[0].to_dict()\n", "pprint(john_dict)" ], - "id": "ed6cd2984e10baca", + "id": "840449d1e08ea7f2", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# Let's convert the dict to a Person instance\n", "from osw.model.entity import Person\n", @@ -1049,9 +1069,7 @@ "# - first_name: field required\n", "# - surname: field required" ], - "id": "840449d1e08ea7f2", - "outputs": [], - "execution_count": null + "id": "cea026747cf19c54" }, { "metadata": {}, @@ -1072,7 +1090,7 @@ "- Create a HelperClass, which inherits from the target data class and `osw.data.import_utility.HelperModel`, and \n", " implements a transformation function to create the target data class instance from the dictionary " ], - "id": "de2cd483287d86a9" + "id": "d98830f43fd2a405" }, { "metadata": {}, @@ -1083,7 +1101,7 @@ "This option is very simple and will serve you well for cases of low complexity, where the datatypes in the DataFrame \n", "columns already match the datatypes of the target data class." ], - "id": "d98830f43fd2a405" + "id": "829bb56909333cb1" }, { "metadata": {}, @@ -1092,7 +1110,7 @@ "# Let's print out the columns first\n", "print(data_df.columns)" ], - "id": "88760a60e93fc99e", + "id": "90f7efe362d1fd52", "outputs": [], "execution_count": null }, @@ -1114,7 +1132,7 @@ "john_dict = data_df.iloc[0].to_dict()\n", "print(john_dict)" ], - "id": "90f7efe362d1fd52", + "id": "469e3f2514a16e50", "outputs": [], "execution_count": null }, @@ -1127,22 +1145,22 @@ "# This will cause a ValidationError:\n", "# - email: value is not a valid set" ], - "id": "469e3f2514a16e50", + "id": "9e5c3bc9ff17b2e4", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# Let's correct the email field\n", "john_dict[\"email\"] = [john_dict[\"email\"]]\n", "john_xl = Person(**john_dict)\n", "pprint(john_xl.dict())" ], - "id": "9e5c3bc9ff17b2e4", - "outputs": [], - "execution_count": null + "id": "5d75c837ffc91987" }, { "metadata": {}, @@ -1156,7 +1174,7 @@ "[!NOTE] Property and variable names in Python must not contain spaces, so the column names in the DataFrame have \n", "to be transformed accordingly." ], - "id": "7401bbbb658bc631" + "id": "f73419919649608d" }, { "metadata": {}, @@ -1196,7 +1214,7 @@ "print(\"Before transformation:\")\n", "pprint(john_helper.dict())" ], - "id": "ee7efd2374c3f87e", + "id": "872b9e65d606d544", "outputs": [], "execution_count": null }, @@ -1209,7 +1227,7 @@ "print(\"After transformation:\")\n", "pprint(john_helper.dict())" ], - "id": "872b9e65d606d544", + "id": "2ff76f2a76c097f6", "outputs": [], "execution_count": null }, @@ -1224,7 +1242,7 @@ "print(\"After casting:\")\n", "pprint(local_john.dict())" ], - "id": "2ff76f2a76c097f6", + "id": "30e0620c9ef49fdf", "outputs": [], "execution_count": null }, @@ -1237,21 +1255,21 @@ "for ii in data_df.index:\n", " entities.append(PersonHelper(**data_df.iloc[ii].to_dict()).transformed)" ], - "id": "30e0620c9ef49fdf", + "id": "15f2596aff7ac2e0", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# And store the entities in the OSL instance\n", "res = osw_obj.store_entity(entities)\n", "stored.append(res)" ], - "id": "15f2596aff7ac2e0", - "outputs": [], - "execution_count": null + "id": "53e1042422f2f0ed" }, { "metadata": {}, @@ -1266,7 +1284,7 @@ "is a convenience method, basically a wrapper around `semantic_search()`, querying: `[[HasType::Category:]]`. This query essentially queries all entries in the platform, which fulfill the condition." ], - "id": "30d853a79d55057a" + "id": "d4fe2e79455f2f04" }, { "metadata": {}, @@ -1279,7 +1297,7 @@ "print(\"Number of persons stored in the OSL instance:\", len(persons))\n", "print(\"Number of organizations stored in the OSL instance:\", len(organizations))" ], - "id": "150a5af64977fb64", + "id": "c47281593ceed2bf", "outputs": [], "execution_count": null }, @@ -1290,22 +1308,22 @@ "# Test if john is in the list of persons\n", "john.get_iri() in persons" ], - "id": "c47281593ceed2bf", + "id": "5bf7f397f2cb48c", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# To get the instances themselves, use load_entity() with the IRIs\n", "print(\"Loading person with IRI:\", persons[0])\n", "some_person = osw_obj.load_entity(persons[0])\n", "pprint(some_person.dict())" ], - "id": "5bf7f397f2cb48c", - "outputs": [], - "execution_count": null + "id": "1ae5a4da1418c1" }, { "metadata": {}, @@ -1325,19 +1343,19 @@ "\n", "If you are connected to another OSL instance, use the following cell to create a link for you:" ], - "id": "8360dc23ce41c09b" + "id": "8f816e2fc97251d0" }, { "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# Creating a clickable link for you\n", "from sys import stdout\n", "stdout.write(f\"Interactive semantic search: 'https://{wiki_domain}/w/index.php?title=Special:Ask&#search'\")" ], - "id": "91cf5af37c1face5", - "outputs": [], - "execution_count": null + "id": "39b2ce4f24ac5793" }, { "metadata": {}, @@ -1352,7 +1370,7 @@ "- A list of organizations, where `John Doe` is the manager - there should be at least one, the `Doe Group, Inc.` and\n", "`Doe, Inc.` we have created earlier" ], - "id": "b6122becf69e18ec" + "id": "c814cd2e5bf56d96" }, { "metadata": {}, @@ -1363,7 +1381,7 @@ "The method `semantic_search` allows to do query for semantic properties in python. At the moment, the method is\n", "only available for the `site` attribute of OSW / OswExpress." ], - "id": "c814cd2e5bf56d96" + "id": "43df2da7095a1511" }, { "metadata": {}, @@ -1379,7 +1397,7 @@ " query_res = osw_obj.site.semantic_search(query)\n", " print(f\"Querying '{query}' returned:\", query_res)" ], - "id": "8c3f0532322e4b51", + "id": "be231b1bccee0b71", "outputs": [], "execution_count": null }, @@ -1392,7 +1410,7 @@ "change_ids = [res.change_id for res in stored]\n", "print(\"Change Ids:\", change_ids)" ], - "id": "be231b1bccee0b71", + "id": "7a3a8d820b939523", "outputs": [], "execution_count": null }, @@ -1411,21 +1429,21 @@ "res_iris = osw_obj.site.semantic_search(search_param)\n", "print(\"IRIs of the uploaded entities:\", res_iris)" ], - "id": "7a3a8d820b939523", + "id": "167bd0a450faef66", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "# Clean up - delete all uploaded entities (if you have permission to do so)\n", "osw_obj.site.delete_page(res_iris)\n", "# Will fail if you do not have the required rights - we will clean up for you :)" ], - "id": "167bd0a450faef66", - "outputs": [], - "execution_count": null + "id": "4fff01bc5e87752b" } ], "metadata": { From 2edc49b846165262f0985fe8cf93cbf5e5f23979 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 25 Nov 2024 14:30:44 +0100 Subject: [PATCH 49/52] Fixing jsonschema in example --- docs/tutorials/basics.ipynb | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/tutorials/basics.ipynb b/docs/tutorials/basics.ipynb index 2ba36980..30931779 100644 --- a/docs/tutorials/basics.ipynb +++ b/docs/tutorials/basics.ipynb @@ -143,6 +143,7 @@ "```json\n", "{\n", " \"type\": \"object\",\n", + " \"allOf\": \"/wiki/Category:Entity?action=raw&slot=jsonschema\",\n", " \"properties\": {\n", " \"text\": { \"type\": \"string\" },\n", " \"number\": { \"type\": \"number\" },\n", From 02e62739a50c8aaa75f4ce79d8a61a0682277ffb Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 25 Nov 2024 15:21:17 +0100 Subject: [PATCH 50/52] Deactivating test_statement_creation() for now --- tests/integration/store_and_load_test.py | 63 ++++++++++++------------ 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/tests/integration/store_and_load_test.py b/tests/integration/store_and_load_test.py index e8d54ee4..9b9543a6 100644 --- a/tests/integration/store_and_load_test.py +++ b/tests/integration/store_and_load_test.py @@ -5,7 +5,8 @@ from osw.auth import CredentialManager from osw.core import OSW, AddOverwriteClassOptions, OverwriteOptions from osw.utils.wiki import get_full_title -from osw.wiki_tools import SearchParam + +# from osw.wiki_tools import SearchParam from osw.wtsite import WtSite # run with: tox -e test -- --wiki_domain domain --wiki_username user --wiki_password pass @@ -141,36 +142,36 @@ def test_query_instances(wiki_domain, wiki_username, wiki_password): osw.delete_entity(my_item) -def test_statement_creation(wiki_domain, wiki_username, wiki_password): - cm = CredentialManager() - cm.add_credential( - CredentialManager.UserPwdCredential( - iri=wiki_domain, username=wiki_username, password=wiki_password - ) - ) - wtsite = WtSite(WtSite.WtSiteConfig(iri=wiki_domain, cred_mngr=cm)) - osw = OSW(site=wtsite) - - my_entity = model.Item( - label=[model.Label(text="MyItem")], - statements=[ - model.DataStatement(property="Property:TestProperty", value="TestValue") - ], - ) - - osw.store_entity(my_entity) - - search_param = SearchParam(query="[[TestProperty::TestValue]]") - full_page_titles = osw.site.semantic_search(search_param) - assert f"Item:{OSW.get_osw_id(my_entity.uuid)}" in full_page_titles - - search_param = SearchParam( - query="[[HasStatement.HasProperty::Property:TestProperty]]" - ) - full_page_titles = osw.site.semantic_search(search_param) - assert f"Item:{OSW.get_osw_id(my_entity.uuid)}" in full_page_titles - - osw.delete_entity(my_entity) +# def test_statement_creation(wiki_domain, wiki_username, wiki_password): +# cm = CredentialManager() +# cm.add_credential( +# CredentialManager.UserPwdCredential( +# iri=wiki_domain, username=wiki_username, password=wiki_password +# ) +# ) +# wtsite = WtSite(WtSite.WtSiteConfig(iri=wiki_domain, cred_mngr=cm)) +# osw = OSW(site=wtsite) +# +# my_entity = model.Item( +# label=[model.Label(text="MyItem")], +# statements=[ +# model.DataStatement(property="Property:TestProperty", value="TestValue") +# ], +# ) +# +# osw.store_entity(my_entity) +# +# search_param = SearchParam(query="[[TestProperty::TestValue]]") +# full_page_titles = osw.site.semantic_search(search_param) +# assert f"Item:{OSW.get_osw_id(my_entity.uuid)}" in full_page_titles +# +# search_param = SearchParam( +# query="[[HasStatement.HasProperty::Property:TestProperty]]" +# ) +# full_page_titles = osw.site.semantic_search(search_param) +# assert f"Item:{OSW.get_osw_id(my_entity.uuid)}" in full_page_titles +# +# osw.delete_entity(my_entity) def test_characteristic_creation(wiki_domain, wiki_username, wiki_password): From 80e7004809c6da52d0b890f8c1ef504bf068e525 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 25 Nov 2024 15:44:14 +0100 Subject: [PATCH 51/52] Some usability features --- examples/clean_demo_platform.py | 19 +++++++++++++++++++ src/osw/express.py | 2 ++ src/osw/wtsite.py | 1 + 3 files changed, 22 insertions(+) create mode 100644 examples/clean_demo_platform.py diff --git a/examples/clean_demo_platform.py b/examples/clean_demo_platform.py new file mode 100644 index 00000000..eda6899c --- /dev/null +++ b/examples/clean_demo_platform.py @@ -0,0 +1,19 @@ +from pathlib import Path + +import osw.params as prm +from osw.express import OswExpress + +osw_obj = OswExpress( + domain="demo.open-semantic-lab.org", cred_filepath=Path("accounts.pwd.yaml") +) +orgs = osw_obj.site.semantic_search( + prm.SearchParam( + query="[[HasType::Category:OSW1969007d5acf40539642877659a02c23]]", debug=False + ) +) +persons = osw_obj.site.semantic_search( + prm.SearchParam( + query="[[HasType::Category:OSW44deaa5b806d41a2a88594f562b110e9]]", debug=False + ) +) +osw_obj.site.delete_page(orgs + persons) diff --git a/src/osw/express.py b/src/osw/express.py index a3b9b389..e4bc6774 100644 --- a/src/osw/express.py +++ b/src/osw/express.py @@ -71,6 +71,8 @@ def __repr__(self): def __eq__(self, other): if isinstance(other, CredentialsFpDefault): return self.default == other.default + elif isinstance(other, DownloadDirDefault): + return self.default == other.default return False @property diff --git a/src/osw/wtsite.py b/src/osw/wtsite.py index b62ecc6f..5ae94206 100644 --- a/src/osw/wtsite.py +++ b/src/osw/wtsite.py @@ -639,6 +639,7 @@ def delete_page( if comment: param.comment = comment pages = [] + print("Getting pages to delete ...") for page in param.page: if isinstance(page, str): pages.append(WtPage(self, title=page, do_init=True)) From 48f1c818090083cf968e8aecd8400ee49783a6e5 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Mon, 25 Nov 2024 16:41:03 +0100 Subject: [PATCH 52/52] Improved readability --- docs/tutorials/basics.ipynb | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/tutorials/basics.ipynb b/docs/tutorials/basics.ipynb index 30931779..3fc32974 100644 --- a/docs/tutorials/basics.ipynb +++ b/docs/tutorials/basics.ipynb @@ -637,18 +637,12 @@ "source": [ "# Let's document all storing processes in a list\n", "stored = []\n", - "# Let's store the entity\n", + "\n", + "# Storing an entity requires only one line:\n", "res = osw_obj.store_entity(john)\n", - "stored.append(res)\n", - "# In this specific case equivalent to:\n", - "params = prm.StoreEntityParam(\n", - " entities=[john],\n", - " namespace=john.get_namespace(),\n", - " parallel=False,\n", - " overwrite=\"keep existing\",\n", - " overwrite_per_class=None,\n", - ") # All default values included\n", - "# osw_obj.store_entity(params)" + "\n", + "# But to be able to access all storage event later on, we will append the result to the list\n", + "stored.append(res)" ], "id": "99cf8828bee651ee" }, @@ -657,7 +651,18 @@ "id": "639db64348bbd905", "metadata": {}, "source": [ - "Like most methods and functions in the osw-python library, the `store_entity` takes only a single argument. Usually \n", + "```python\n", + "# In this specific case equivalent to:\n", + "params = prm.StoreEntityParam(\n", + " entities=[john],\n", + " namespace=john.get_namespace(),\n", + " parallel=False,\n", + " overwrite=\"keep existing\",\n", + " overwrite_per_class=None,\n", + ") # All default values included\n", + "stored.append(osw_obj.store_entity(params))\n", + "```\n", + "Like most methods and functions in the osw-python library, the `store_entity` takes only a single argument. Usually\n", "either a specific object type or a dedicated params object is accepted. If an object of type other than the params \n", "object is passed, it is usually tested for compatibility and put inside a params object, filling the other parameters \n", "with default values. \n",