From 452bb3eb8b3c31bfae4607379f41bf601b327b08 Mon Sep 17 00:00:00 2001 From: SimonTaurus Date: Thu, 12 Dec 2024 06:30:32 +0100 Subject: [PATCH 1/6] feature: store all custom schema annotations in generated classes --- src/osw/core.py | 51 +++++++++++++- src/osw/model/entity.py | 147 +++++++++++++++++++++++++++++++++++++-- src/osw/utils/codegen.py | 59 ++++++++++++++++ 3 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 src/osw/utils/codegen.py diff --git a/src/osw/core.py b/src/osw/core.py index 0e492c58..25eadfd7 100644 --- a/src/osw/core.py +++ b/src/osw/core.py @@ -22,6 +22,7 @@ import osw.model.entity as model from osw.model.static import OswBaseModel +from osw.utils.codegen import OOLDJsonSchemaParser from osw.utils.templates import ( compile_handlebars_template, eval_compiled_handlebars_template, @@ -334,6 +335,9 @@ class FetchSchemaParam(BaseModel): ) legacy_generator: Optional[bool] = False """uses legacy command line for code generation if true""" + generate_annotations: Optional[bool] = True + """generate custom schema keywords in Fields and Classes. + Required to update the schema in OSW without information loss""" def fetch_schema(self, fetchSchemaParam: FetchSchemaParam = None) -> None: """Loads the given schemas from the OSW instance and autogenerates python @@ -356,6 +360,7 @@ def fetch_schema(self, fetchSchemaParam: FetchSchemaParam = None) -> None: schema_title=schema_title, mode=mode, legacy_generator=fetchSchemaParam.legacy_generator, + generate_annotations=fetchSchemaParam.generate_annotations, ) ) first = False @@ -382,6 +387,9 @@ class _FetchSchemaParam(BaseModel): ) legacy_generator: Optional[bool] = False """uses legacy command line for code generation if true""" + generate_annotations: Optional[bool] = False + """generate custom schema keywords in Fields and Classes. + Required to update the schema in OSW without information loss""" def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: """Loads the given schema from the OSW instance and autogenerates python @@ -502,6 +510,11 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: " ) else: + if fetchSchemaParam.generate_annotations: + # monkey patch class + datamodel_code_generator.parser.jsonschema.JsonSchemaParser = ( + OOLDJsonSchemaParser + ) datamodel_code_generator.generate( input_=pathlib.Path(schema_path), input_file_type="jsonschema", @@ -520,6 +533,43 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: reuse_model=True, ) + # note: we could use OOLDJsonSchemaParser directly (see below), + # but datamodel_code_generator.generate + # does some pre- and postprocessing we do not want to duplicate + + # data_model_type = datamodel_code_generator.DataModelType.PydanticBaseModel + # #data_model_type = DataModelType.PydanticV2BaseModel + # target_python_version = datamodel_code_generator.PythonVersion.PY_38 + # data_model_types = datamodel_code_generator.model.get_data_model_types( + # data_model_type, target_python_version + # ) + # parser = OOLDJsonSchemaParserFixedRefs( + # source=pathlib.Path(schema_path), + + # base_class="osw.model.static.OswBaseModel", + # data_model_type=data_model_types.data_model, + # data_model_root_type=data_model_types.root_model, + # data_model_field_type=data_model_types.field_model, + # data_type_manager_type=data_model_types.data_type_manager, + # target_python_version=target_python_version, + + # #use_default=True, + # apply_default_values_for_required_fields=True, + # use_unique_items_as_set=True, + # enum_field_as_literal=datamodel_code_generator.LiteralType.All, + # use_title_as_name=True, + # use_schema_description=True, + # use_field_description=True, + # encoding="utf-8", + # use_double_quotes=True, + # collapse_root_models=True, + # reuse_model=True, + # #field_include_all_keys=True + # ) + # result = parser.parse() + # with open(temp_model_path, "w", encoding="utf-8") as f: + # f.write(result) + # see https://koxudaxi.github.io/datamodel-code-generator/ # --base-class OswBaseModel: use a custom base class # --custom-template-dir src/model/template_data/ @@ -593,7 +643,6 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: r"class\s*([\S]*)\s*\(\s*\S*\s*\)\s*:.*\n" ) # match class definition [\s\S]*(?:[^\S\n]*\n){2,} for cls in re.findall(pattern, org_content): - print(cls) content = re.sub( r"(class\s*" + cls diff --git a/src/osw/model/entity.py b/src/osw/model/entity.py index d2310b56..2a6606b7 100644 --- a/src/osw/model/entity.py +++ b/src/osw/model/entity.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: Item.json -# timestamp: 2024-09-12T12:35:11+00:00 +# timestamp: 2024-12-10T05:09:51+00:00 from __future__ import annotations @@ -13,22 +13,58 @@ class ReadAccess(OswBaseModel): + class Config: + schema_extra = {"title": "Read access", "title*": {"de": "Lesezugriff"}} + level: Optional[Literal["public", "internal", "restricted"]] = Field( None, title="Level" ) class AccessRestrictions(OswBaseModel): + class Config: + schema_extra = { + "title": "Access restrictions", + "title*": {"de": "Zugriffsbeschränkungen"}, + "eval_template": { + "$comment": "See https://www.mediawiki.org/wiki/Extension:Semantic_ACL", + "type": "mustache-wikitext", + "mode": "render", + "value": "{{entry_access.read.level}} {{=<% %>=}} {{#set: |Visible to= {{#switch: <%={{ }}=%> {{{entry_access.read.level}}} {{=<% %>=}} |public=public |internal=users |restricted=whitelist |#default=}} }} <%={{ }}=%>", + }, + } + read: Optional[ReadAccess] = Field(None, title="Read access") class Label(OswBaseModel): + class Config: + schema_extra = { + "@context": { + "rdf": "http://www.w3.org/2000/01/rdf-schema#", + "text": {"@id": "@value"}, + "lang": {"@id": "@language"}, + }, + "title": "Label", + } + text: constr(min_length=1) = Field(..., title="Text") lang: Optional[Literal["en", "de"]] = Field("en", title="Lang code") -class Description(Label): - pass +class Description(OswBaseModel): + class Config: + schema_extra = { + "@context": { + "rdf": "http://www.w3.org/2000/01/rdf-schema#", + "text": {"@id": "@value"}, + "lang": {"@id": "@language"}, + }, + "title": "Description", + } + + text: constr(min_length=1) = Field(..., title="Text") + lang: Optional[Literal["en", "de"]] = Field("en", title="Lang code") class WikiPage(OswBaseModel): @@ -36,6 +72,12 @@ class WikiPage(OswBaseModel): The wiki page containing this entity """ + class Config: + schema_extra = { + "title": "Wiki page", + "description": "The wiki page containing this entity", + } + title: Optional[str] = Field(None, title="Title") """ The page title @@ -47,6 +89,14 @@ class WikiPage(OswBaseModel): class Meta(OswBaseModel): + class Config: + schema_extra = { + "@context": { + "change_id": {"@id": "Property:HasChangeId", "@type": "xsd:string"} + }, + "title": "Meta", + } + uuid: UUID = Field(default_factory=uuid4, title="UUID") wiki_page: Optional[WikiPage] = Field(None, title="Wiki page") """ @@ -59,6 +109,70 @@ class Meta(OswBaseModel): class Entity(OswBaseModel): + class Config: + schema_extra = { + "@context": { + "bvco": "https://bvco.ontology.link/", + "databatt": "http://www.databatt.org/", + "emmo": "https://w3id.org/emmo#", + "emmobattery": "https://w3id.org/emmo/domain/battery#", + "emmochameo": "https://w3id.org/emmo/domain/chameo#", + "emmochemicals": "https://w3id.org/emmo/domain/chemicalsubstance#", + "emmoelch": "https://w3id.org/emmo/domain/electrochemistry#", + "schema": "https://schema.org/", + "skos": "https://www.w3.org/TR/skos-reference/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "wiki": "https://wiki-dev.open-semantic-lab.org/id/", + "Category": {"@id": "wiki:Category-3A", "@prefix": True}, + "File": { + "@id": "https://wiki-dev.open-semantic-lab.org/wiki/Special:Redirect/file/", + "@prefix": True, + }, + "Property": {"@id": "wiki:Property-3A", "@prefix": True}, + "Item": {"@id": "wiki:Item-3A", "@prefix": True}, + "attachments*": {"@id": "Property:HasFileAttachment", "@type": "@id"}, + "based_on": {"@id": "skos:isBasedOn", "@type": "@id"}, + "based_on*": {"@id": "Property:IsBasedOn", "@type": "@id"}, + "description": {"@id": "skos:definition", "@type": "@id"}, + "description*": {"@id": "Property:HasDescription", "@type": "@id"}, + "image": {"@id": "schema:image", "@type": "@id"}, + "image*": {"@id": "Property:HasImage", "@type": "@id"}, + "label": {"@id": "skos:prefLabel", "@type": "@id"}, + "label*": {"@id": "Property:HasLabel", "@type": "@id"}, + "lang": {"@id": "@language"}, + "meta": { + "@id": "Property:HasMeta", + "@type": "@id", + "@context": { + "change_id": { + "@id": "Property:HasChangeId", + "@type": "xsd:string", + } + }, + }, + "name*": {"@id": "Property:HasName"}, + "ordering_categories": {"@id": "Property:Category", "@type": "@id"}, + "ordering_categories*": { + "@id": "Property:HasClassificationCategory", + "@type": "@id", + }, + "query_label": {"@id": "Property:HasLabel", "@type": "@id"}, + "rdf_type": {"@id": "@type", "@type": "@id"}, + "rdf_type*": {"@id": "schema:additionalType", "@type": "@id"}, + "rdf_type**": {"@id": "owl:sameAs", "@type": "@id"}, + "rdf_type***": {"@id": "Property:Equivalent_URI", "@type": "@id"}, + "short_name": {"@id": "Property:HasShortName"}, + "keywords": {"@id": "schema:keywords", "@type": "@id"}, + "keywords*": {"@id": "Property:HasKeyword", "@type": "@id"}, + "statements": {"@id": "Property:HasStatement", "@type": "@id"}, + "text": {"@id": "@value"}, + "uuid*": {"@id": "Property:HasUuid"}, + }, + "uuid": "ce353767-c628-45bd-9d88-d6eb3009aec0", + "title": "Entity", + "defaultProperties": ["description"], + } + rdf_type: Optional[Set[str]] = Field(None, title="Additional RDF type(s)") """ Declares additional type(s) for this entity, e.g., to state that this entity has the same meaning as a term in a controlled vocabulary or ontology. This property is synonymous to the schema:additionalType and owl:sameAs. The default syntax is ontology:TermName. The ontology prefix has to be defined in the @context of the Entity, the category or any of the parent categories. The term name has to be a valid identifier in the ontology. @@ -83,9 +197,11 @@ class Entity(OswBaseModel): query_label: Optional[str] = Field(None, title="Query label") description: Optional[List[Description]] = Field(None, title="Description") image: Optional[str] = Field(None, title="Image") - ordering_categories: Optional[List[str]] = Field(None, title="Ordering categories") + ordering_categories: Optional[List[str]] = Field( + None, title="Classification categories" + ) """ - Ordering categories are used to categorize instances, e.g., according to their use but not their properties. When querying for instances of a here listed ordering category, this instance will be returned. Note: Ordering categories define no properties, while 'regular' categories define properties, which an instance assigns values to. + Classification categories are used to categorize instances, e.g., according to their use but not their properties. When querying for instances of a here listed classification category, this instance will be returned. Note: Classification categories define no properties, while 'regular' categories define properties, which an instance assigns values to. """ keywords: Optional[List[str]] = Field(None, title="Keywords / Tags") """ @@ -103,6 +219,9 @@ class Entity(OswBaseModel): class ObjectStatement(OswBaseModel): + class Config: + schema_extra = {"title": "Object Statement"} + rdf_type: Optional[Any] = "rdf:Statement" uuid: UUID = Field(default_factory=uuid4, title="UUID") label: Optional[List[Label]] = Field(None, title="Label") @@ -118,6 +237,9 @@ class ObjectStatement(OswBaseModel): class DataStatement(OswBaseModel): + class Config: + schema_extra = {"title": "Data Statement"} + rdf_type: Optional[Any] = "rdf:Statement" uuid: UUID = Field(default_factory=uuid4, title="UUID") label: Optional[List[Label]] = Field(None, title="Label") @@ -133,6 +255,9 @@ class DataStatement(OswBaseModel): class QuantityStatement(OswBaseModel): + class Config: + schema_extra = {"title": "Quantity Statement"} + rdf_type: Optional[Any] = "rdf:Statement" uuid: UUID = Field(default_factory=uuid4, title="UUID") label: Optional[List[Label]] = Field(None, title="Label") @@ -151,6 +276,18 @@ class QuantityStatement(OswBaseModel): class Item(Entity): + class Config: + schema_extra = { + "@context": [ + "/wiki/Category:Entity?action=raw&slot=jsonschema", + { + "type": {"@id": "Property:HasType", "@type": "@id"}, + "type*": {"@id": "Property:HasSchema", "@type": "@id"}, + }, + ], + "title": "Item", + } + type: Optional[List[str]] = Field( ["Category:Item"], min_items=1, title="Types/Categories" ) diff --git a/src/osw/utils/codegen.py b/src/osw/utils/codegen.py new file mode 100644 index 00000000..6c467fcc --- /dev/null +++ b/src/osw/utils/codegen.py @@ -0,0 +1,59 @@ +from pathlib import Path +from typing import Any, Dict + +from datamodel_code_generator import load_yaml_from_path +from datamodel_code_generator.model import pydantic as pydantic_v1_model +from datamodel_code_generator.model import pydantic_v2 as pydantic_v2_model +from datamodel_code_generator.parser.jsonschema import ( + JsonSchemaObject, + JsonSchemaParser, +) + +# https://docs.pydantic.dev/1.10/usage/schema/#schema-customization +# https://docs.pydantic.dev/latest/concepts/json_schema/#using-json_schema_extra-with-a-dict +# https://docs.pydantic.dev/latest/concepts/json_schema/#field-level-customization + + +class PydanticV1Config(pydantic_v1_model.Config): + # schema_extra: Optional[Dict[str, Any]] = None + schema_extra: str = None + + +class PydanticV2Config(pydantic_v2_model.ConfigDict): + # schema_extra: Optional[Dict[str, Any]] = None + json_schema_extra: str = None + + +class OOLDJsonSchemaParser(JsonSchemaParser): + """Custom parser for OO-LD schemas. + You can use this class directly or monkey-patch the datamodel_code_generator module: + `datamodel_code_generator.parser.jsonschema.JsonSchemaParser = OOLDJsonSchemaParser` + """ + + def set_additional_properties(self, name: str, obj: JsonSchemaObject) -> None: + schema_extras = repr(obj.extras) # keeps 'False' and 'True' boolean literals + if self.data_model_type == pydantic_v1_model.BaseModel: + self.extra_template_data[name]["config"] = PydanticV1Config( + schema_extra=schema_extras + ) + if self.data_model_type == pydantic_v2_model.BaseModel: + self.extra_template_data[name]["config"] = PydanticV2Config( + json_schema_extra=schema_extras + ) + return super().set_additional_properties(name, obj) + + +class OOLDJsonSchemaParserFixedRefs(OOLDJsonSchemaParser): + """Overwrite # overwrite the original `_get_ref_body_from_remote` function + to fix wrongly composed paths. This issue occurs only when using this parser class directy + and occurs not if used through mokey patching and `datamodel_code_generator.generate()` + """ + + def _get_ref_body_from_remote(self, resolved_ref: str) -> Dict[Any, Any]: + # full_path = self.base_path / resolved_ref + # fix: merge the paths correctly + full_path = self.base_path / Path(resolved_ref).parts[-1] + return self.remote_object_cache.get_or_put( + str(full_path), + default_factory=lambda _: load_yaml_from_path(full_path, self.encoding), + ) From 6a21aa0c01feb2dd53b3f4bd2a18df5f81987f67 Mon Sep 17 00:00:00 2001 From: SimonTaurus Date: Fri, 25 Apr 2025 04:05:21 +0200 Subject: [PATCH 2/6] feat: base OswBaseModel on LinkedBaseModel --- setup.cfg | 1 + src/osw/core.py | 56 ++++++++++++++++++++++++++++++++++++++--- src/osw/model/static.py | 3 ++- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index fa544bc9..3b0b2345 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,6 +48,7 @@ package_dir = # new major versions. This works if the required packages follow Semantic Versioning. # For more information, check out https://semver.org/. install_requires = + oold pydantic>=1.10.17 datamodel-code-generator>=0.25 mwclient>=0.11.0 diff --git a/src/osw/core.py b/src/osw/core.py index 2e35618b..529688dd 100644 --- a/src/osw/core.py +++ b/src/osw/core.py @@ -18,6 +18,14 @@ import rdflib from jsonpath_ng.ext import parse from mwclient.client import Site +from oold.generator import Generator +from oold.model.v1 import ( + ResolveParam, + Resolver, + ResolveResult, + SetResolverParam, + set_resolver, +) from pydantic import PydanticDeprecatedSince20 from pydantic.v1 import BaseModel, Field, PrivateAttr, create_model, validator from pyld import jsonld @@ -99,6 +107,30 @@ class Config: site: WtSite + def __init__(self, **data: Any): + super().__init__(**data) + + # implement resolver backend with osw.load_entity + class MyResolver(Resolver): + + osw_obj: OSW + + def resolve(self, request: ResolveParam): + print("RESOLVE", request) + entities = self.osw_obj.load_entity( + OSW.LoadEntityParam(titles=request.iris) + ).entities + # create a dict with request.iris as keys and the loaded entities as values + # by iterating over both lists + nodes = {} + for iri, entity in zip(request.iris, entities): + nodes[iri] = entity + return ResolveResult(nodes=nodes) + + r = MyResolver(osw_obj=self) + set_resolver(SetResolverParam(iri="Item", resolver=r)) + set_resolver(SetResolverParam(iri="Category", resolver=r)) + @property def mw_site(self) -> Site: """Returns the mwclient Site object of the OSW instance.""" @@ -396,6 +428,8 @@ class _FetchSchemaParam(BaseModel): ) legacy_generator: Optional[bool] = False """uses legacy command line for code generation if true""" + fetched_schema_titles: Optional[List[str]] = [] + """keep track of fetched schema titles to prevent recursion""" def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: """Loads the given schema from the OSW instance and autogenerates python @@ -411,6 +445,7 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: if fetchSchemaParam is None: fetchSchemaParam = OSW._FetchSchemaParam() schema_title = fetchSchemaParam.schema_title + fetchSchemaParam.fetched_schema_titles.append(schema_title) root = fetchSchemaParam.root schema_name = schema_title.split(":")[-1] page = self.site.get_page(WtSite.GetPageParam(titles=[schema_title])).pages[0] @@ -433,6 +468,12 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: if (schema_str is None) or (schema_str == ""): print(f"Error: Schema {schema_title} does not exist") schema_str = "{}" # empty schema to make reference work + + generator = Generator() + schemas_for_preprocessing = [json.loads(schema_str)] + generator.preprocess(schemas_for_preprocessing) + schema_str = json.dumps(schemas_for_preprocessing[0]) + schema = json.loads( schema_str.replace("$ref", "dollarref").replace( # '$' is a special char for root object in jsonpath @@ -461,10 +502,12 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: # print(f"replace {match.value} with {value}") if ( ref_schema_title != schema_title + and ref_schema_title not in fetchSchemaParam.fetched_schema_titles ): # prevent recursion in case of self references - self._fetch_schema( - OSW._FetchSchemaParam(schema_title=ref_schema_title, root=False) - ) # resolve references recursive + _param = fetchSchemaParam.copy() + _param.root = False + _param.schema_title = ref_schema_title + self._fetch_schema(_param) # resolve references recursive model_dir_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "model" @@ -530,7 +573,11 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: # use_default=True, apply_default_values_for_required_fields=True, use_unique_items_as_set=True, - enum_field_as_literal=datamodel_code_generator.LiteralType.All, + # enum_field_as_literal=datamodel_code_generator.LiteralType.All, + enum_field_as_literal="all", + # will create MyEnum(str, Enum) instead of MyEnum(Enum) + use_subclass_enum=True, + set_default_enum_member=True, use_title_as_name=True, use_schema_description=True, use_field_description=True, @@ -538,6 +585,7 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: use_double_quotes=True, collapse_root_models=True, reuse_model=True, + field_include_all_keys=True, ) warnings.filterwarnings("default", category=PydanticDeprecatedSince20) diff --git a/src/osw/model/static.py b/src/osw/model/static.py index 8b18fd78..e8b09f57 100644 --- a/src/osw/model/static.py +++ b/src/osw/model/static.py @@ -6,6 +6,7 @@ from uuid import UUID, uuid4 from warnings import warn +from oold.model.v1 import LinkedBaseModel from pydantic.v1 import BaseModel, Field, constr from osw.custom_types import NoneType @@ -82,7 +83,7 @@ def custom_isinstance(obj: Union[type, T], class_name: str) -> bool: @_basemodel_decorator -class OswBaseModel(BaseModel): +class OswBaseModel(LinkedBaseModel): class Config: """Configuration for the OswBaseModel""" From 3d4c84549865b601fba1c2d4b7a7f5f341cb02a8 Mon Sep 17 00:00:00 2001 From: SimonTaurus Date: Fri, 25 Apr 2025 04:17:23 +0200 Subject: [PATCH 3/6] fix: use regular BaseModel for internal helper classes --- src/osw/defaults.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/osw/defaults.py b/src/osw/defaults.py index ee6b45bc..e76acd03 100644 --- a/src/osw/defaults.py +++ b/src/osw/defaults.py @@ -6,7 +6,7 @@ from pydantic.v1 import PrivateAttr, validator -from osw.model.static import OswBaseModel +from osw.model.static import BaseModel PACKAGE_ROOT_PATH = Path(__file__).parents[2] SRC_PATH = PACKAGE_ROOT_PATH / "src" @@ -18,7 +18,7 @@ WIKI_DOMAIN_DEFAULT = "wiki-dev.open-semantic-lab.org" -class FilePathDefault(OswBaseModel): +class FilePathDefault(BaseModel): """A class to store the default file path. This is a helper class to make the default file path, defined within this module, accessible from a calling script.""" @@ -53,7 +53,7 @@ def get(self): return self._default -class Defaults(OswBaseModel): +class Defaults(BaseModel): """Helper class to create an inheriting classes for storing default values.""" _changed: List[str] = PrivateAttr(default_factory=list) From 35dd834cfe0b5c567e0090317496e9c01a305144 Mon Sep 17 00:00:00 2001 From: SimonTaurus Date: Fri, 25 Apr 2025 04:31:18 +0200 Subject: [PATCH 4/6] feat: offline pages and custom target path for code gen --- src/osw/core.py | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/osw/core.py b/src/osw/core.py index 529688dd..c2ba44da 100644 --- a/src/osw/core.py +++ b/src/osw/core.py @@ -380,6 +380,14 @@ class FetchSchemaParam(BaseModel): ) legacy_generator: Optional[bool] = False """uses legacy command line for code generation if true""" + offline_pages: Optional[Dict[str, WtPage]] = None + """pages to be used offline instead of fetching them from the OSW instance""" + result_model_path: Optional[Union[str, pathlib.Path]] = None + """path to the generated model file, if None, + the default path ./model/entity.py is used""" + + class Config: + arbitrary_types_allowed = True def fetch_schema(self, fetchSchemaParam: FetchSchemaParam = None) -> None: """Loads the given schemas from the OSW instance and auto-generates python @@ -402,6 +410,8 @@ def fetch_schema(self, fetchSchemaParam: FetchSchemaParam = None) -> None: schema_title=schema_title, mode=mode, legacy_generator=fetchSchemaParam.legacy_generator, + offline_pages=fetchSchemaParam.offline_pages, + result_model_path=fetchSchemaParam.result_model_path, ) ) first = False @@ -428,9 +438,17 @@ class _FetchSchemaParam(BaseModel): ) legacy_generator: Optional[bool] = False """uses legacy command line for code generation if true""" + offline_pages: Optional[Dict[str, WtPage]] = None + """pages to be used offline instead of fetching them from the OSW instance""" + result_model_path: Optional[Union[str, pathlib.Path]] = None + """path to the generated model file, if None, + the default path ./model/entity.py is used""" fetched_schema_titles: Optional[List[str]] = [] """keep track of fetched schema titles to prevent recursion""" + class Config: + arbitrary_types_allowed = True + def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: """Loads the given schema from the OSW instance and autogenerates python datasclasses within osw.model.entity from it @@ -448,10 +466,20 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: fetchSchemaParam.fetched_schema_titles.append(schema_title) root = fetchSchemaParam.root schema_name = schema_title.split(":")[-1] - page = self.site.get_page(WtSite.GetPageParam(titles=[schema_title])).pages[0] - if not page.exists: - print(f"Error: Page {schema_title} does not exist") - return + if ( + fetchSchemaParam.offline_pages is not None + and schema_title in fetchSchemaParam.offline_pages + ): + print(f"Fetch {schema_title} from offline pages") + page = fetchSchemaParam.offline_pages[schema_title] + else: + print(f"Fetch {schema_title} from online pages") + page = self.site.get_page(WtSite.GetPageParam(titles=[schema_title])).pages[ + 0 + ] + if not page.exists: + print(f"Error: Page {schema_title} does not exist") + return # not only in the JsonSchema namespace the schema is located in the main sot # in all other namespaces, the json_schema slot is used if schema_title.startswith("JsonSchema:"): @@ -482,7 +510,6 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: ) # fix https://github.com/koxudaxi/datamodel-code-generator/issues/1910 ) - print(f"Fetch {schema_title}") jsonpath_expr = parse("$..dollarref") for match in jsonpath_expr.find(schema): @@ -523,6 +550,10 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: # result_model_path = schema_path.replace(".json", ".py") result_model_path = os.path.join(model_dir_path, "entity.py") + if fetchSchemaParam.result_model_path: + result_model_path = fetchSchemaParam.result_model_path + if not isinstance(result_model_path, str): + result_model_path = str(result_model_path) temp_model_path = os.path.join(model_dir_path, "temp.py") if root: if fetchSchemaParam.legacy_generator: From 954dddba3ad057927956232c0f74a7222fcad6fb Mon Sep 17 00:00:00 2001 From: SimonTaurus Date: Fri, 23 May 2025 17:00:51 +0200 Subject: [PATCH 5/6] refactor: switch to oold codegen, regenerate entity.py --- src/osw/core.py | 2 +- src/osw/model/entity.py | 518 ++++++++++++++++++++++++++++++++++----- src/osw/utils/codegen.py | 59 ----- 3 files changed, 454 insertions(+), 125 deletions(-) delete mode 100644 src/osw/utils/codegen.py diff --git a/src/osw/core.py b/src/osw/core.py index 9c73bd28..3b5f301f 100644 --- a/src/osw/core.py +++ b/src/osw/core.py @@ -26,6 +26,7 @@ SetResolverParam, set_resolver, ) +from oold.utils.codegen import OOLDJsonSchemaParser from pydantic import PydanticDeprecatedSince20 from pydantic.v1 import BaseModel, Field, PrivateAttr, create_model, validator from pyld import jsonld @@ -33,7 +34,6 @@ import osw.model.entity as model from osw.defaults import params as default_params from osw.model.static import OswBaseModel -from osw.utils.codegen import OOLDJsonSchemaParser from osw.utils.oold import ( AggregateGeneratedSchemasParam, AggregateGeneratedSchemasParamMode, diff --git a/src/osw/model/entity.py b/src/osw/model/entity.py index 2a6606b7..b8825159 100644 --- a/src/osw/model/entity.py +++ b/src/osw/model/entity.py @@ -1,23 +1,42 @@ # generated by datamodel-codegen: # filename: Item.json -# timestamp: 2024-12-10T05:09:51+00:00 +# timestamp: 2025-05-18T15:16:02+00:00 from __future__ import annotations -from typing import Any, List, Literal, Optional, Set, Union -from uuid import UUID, uuid4 +from enum import Enum +from typing import Any, List, Optional, Set, Union +from uuid import UUID from pydantic.v1 import Field, constr from osw.model.static import OswBaseModel +class Level(str, Enum): + public = "public" + internal = "internal" + restricted = "restricted" + + +from typing import Type, TypeVar +from uuid import uuid4 + +from osw.model.static import Ontology, OswBaseModel + + class ReadAccess(OswBaseModel): class Config: schema_extra = {"title": "Read access", "title*": {"de": "Lesezugriff"}} - level: Optional[Literal["public", "internal", "restricted"]] = Field( - None, title="Level" + level: Optional[Level] = Field( + None, + options={ + "enum_titles": ["Public", "For all users", "For some users"], + "enum_titles*": ["Öffentlich", "Für alle Nutzer", "Für bestimmte Nutzer"], + }, + title="Level", + title_={"de": "Level"}, ) @@ -34,7 +53,14 @@ class Config: }, } - read: Optional[ReadAccess] = Field(None, title="Read access") + read: Optional[ReadAccess] = Field( + None, title="Read access", title_={"de": "Lesezugriff"} + ) + + +class LangCode(str, Enum): + en = "en" + de = "de" class Label(OswBaseModel): @@ -48,8 +74,23 @@ class Config: "title": "Label", } - text: constr(min_length=1) = Field(..., title="Text") - lang: Optional[Literal["en", "de"]] = Field("en", title="Lang code") + text: constr(min_length=1) = Field( + ..., + options={ + "input_width": "800px", + "inputAttributes": {"placeholder": "Title of the entry"}, + "inputAttributes*": {"de": {"placeholder": "Titel dieses Eintrags"}}, + }, + title="Text", + title_={"de": "Text"}, + ) + lang: Optional[LangCode] = Field( + LangCode.en, + default_={"en": "en", "de": "de"}, + options={"input_width": "100px"}, + title="Lang code", + title_={"de": "Sprache"}, + ) class Description(OswBaseModel): @@ -63,8 +104,16 @@ class Config: "title": "Description", } - text: constr(min_length=1) = Field(..., title="Text") - lang: Optional[Literal["en", "de"]] = Field("en", title="Lang code") + text: constr(min_length=1) = Field( + ..., options={"input_width": "800px"}, title="Text", title_={"de": "Text"} + ) + lang: Optional[LangCode] = Field( + LangCode.en, + default_={"en": "en", "de": "de"}, + options={"input_width": "100px"}, + title="Lang code", + title_={"de": "Sprache"}, + ) class WikiPage(OswBaseModel): @@ -97,7 +146,7 @@ class Config: "title": "Meta", } - uuid: UUID = Field(default_factory=uuid4, title="UUID") + uuid: UUID = Field(default_factory=uuid4, options={"hidden": True}, title="UUID") wiki_page: Optional[WikiPage] = Field(None, title="Wiki page") """ The wiki page containing this entity @@ -112,13 +161,6 @@ class Entity(OswBaseModel): class Config: schema_extra = { "@context": { - "bvco": "https://bvco.ontology.link/", - "databatt": "http://www.databatt.org/", - "emmo": "https://w3id.org/emmo#", - "emmobattery": "https://w3id.org/emmo/domain/battery#", - "emmochameo": "https://w3id.org/emmo/domain/chameo#", - "emmochemicals": "https://w3id.org/emmo/domain/chemicalsubstance#", - "emmoelch": "https://w3id.org/emmo/domain/electrochemistry#", "schema": "https://schema.org/", "skos": "https://www.w3.org/TR/skos-reference/", "xsd": "http://www.w3.org/2001/XMLSchema#", @@ -133,13 +175,15 @@ class Config: "attachments*": {"@id": "Property:HasFileAttachment", "@type": "@id"}, "based_on": {"@id": "skos:isBasedOn", "@type": "@id"}, "based_on*": {"@id": "Property:IsBasedOn", "@type": "@id"}, - "description": {"@id": "skos:definition", "@type": "@id"}, - "description*": {"@id": "Property:HasDescription", "@type": "@id"}, + "description": {"@id": "skos:definition"}, + "description*": {"@id": "Property:HasDescription"}, "image": {"@id": "schema:image", "@type": "@id"}, "image*": {"@id": "Property:HasImage", "@type": "@id"}, - "label": {"@id": "skos:prefLabel", "@type": "@id"}, - "label*": {"@id": "Property:HasLabel", "@type": "@id"}, + "label": {"@id": "skos:prefLabel"}, + "label*": {"@id": "Property:HasLabel"}, "lang": {"@id": "@language"}, + "text": {"@id": "@value"}, + "uuid*": {"@id": "Property:HasUuid"}, "meta": { "@id": "Property:HasMeta", "@type": "@id", @@ -165,114 +209,344 @@ class Config: "keywords": {"@id": "schema:keywords", "@type": "@id"}, "keywords*": {"@id": "Property:HasKeyword", "@type": "@id"}, "statements": {"@id": "Property:HasStatement", "@type": "@id"}, - "text": {"@id": "@value"}, - "uuid*": {"@id": "Property:HasUuid"}, }, "uuid": "ce353767-c628-45bd-9d88-d6eb3009aec0", "title": "Entity", "defaultProperties": ["description"], } - rdf_type: Optional[Set[str]] = Field(None, title="Additional RDF type(s)") + rdf_type: Optional[Set[str]] = Field( + None, + description_={ + "de": "Gibt zusätzliche Typens für diese Entität an, z. B. um anzugeben, dass diese Entität dieselbe Bedeutung hat wie ein Begriff in einem kontrollierten Vokabular oder einer Ontologie. Diese Eigenschaft ist ein Synonym für schema:additionalType und owl:sameAs. Die Standardsyntax ist ontology:TermName. Das Ontologie-Präfix muss im @cotext der Entität, der Kategorie oder einer der beerbten Kategorien definiert werden. Der Termname muss ein gültiger Bezeichner in der Ontologie sein." + }, + title="Additional RDF type(s)", + title_={"de": "Zusätzliche(r) RDF-Typ(en)"}, + ) """ Declares additional type(s) for this entity, e.g., to state that this entity has the same meaning as a term in a controlled vocabulary or ontology. This property is synonymous to the schema:additionalType and owl:sameAs. The default syntax is ontology:TermName. The ontology prefix has to be defined in the @context of the Entity, the category or any of the parent categories. The term name has to be a valid identifier in the ontology. """ - uuid: UUID = Field(default_factory=uuid4, title="UUID") - iri: Optional[str] = Field(None, title="IRI") + uuid: UUID = Field(default_factory=uuid4, options={"hidden": True}, title="UUID") + iri: Optional[str] = Field( + None, + description_={ + "de": "Der internationalisierte Ressourcenbezeichner (IRI) dieser Entität" + }, + options={"hidden": True}, + title="IRI", + ) """ The Internationalized Resource Identifier (IRI) of this entity """ - name: Optional[str] = Field(None, title="Technical name") + name: Optional[str] = Field( + None, + description_={"de": "Technischer / Maschinenkompatibler Name"}, + options={"hidden": True}, + title="Technical name", + title_={"de": "Technischer Name"}, + ) """ Technical / Machine compatible name """ - label: List[Label] = Field(..., min_items=1, title="Label(s)") + label: List[Label] = Field( + ..., + description_={"de": "Mindestens eine Bezeichnung ist erforderlich."}, + eval_template=[ + { + "type": "mustache-wikitext", + "mode": "render", + "$comment": "Displays value according to user language with eng as fallback option. Note: {{=<% %>=}} changes mustache expression from {{..}} to <% %> for mixing with wikitext templates", + "value": "{{=<% %>=}} {{#switch:{{USERLANGUAGECODE}} <%#label%> | {{#ifeq: <%lang%>|en|#default|<%lang%>}} = <%text%> <%/label%> }}", + } + ], + min_items=1, + title="Label(s)", + title_={"de": "Bezeichnung(en)"}, + ) """ At least one label is required. """ - short_name: Optional[List[Label]] = Field(None, title="Short name(s)") + short_name: Optional[List[Label]] = Field( + None, + description_={"de": "Abkürzung, Akronym, etc."}, + eval_template={ + "type": "mustache-wikitext", + "mode": "render", + "$comment": "Displays value according to user language with eng as fallback option. Note: {{=<% %>=}} changes mustache expression from {{..}} to <% %> for mixing with wikitext templates", + "value": "{{=<% %>=}} {{#switch:{{USERLANGUAGECODE}} <%#short_name%> | {{#ifeq: <%lang%>|en|#default|<%lang%>}} = <%text%> <%/short_name%> }}", + }, + title="Short name(s)", + title_={"de": "Kurzname(n)"}, + ) """ Abbreviation, Acronym, etc. """ - query_label: Optional[str] = Field(None, title="Query label") - description: Optional[List[Description]] = Field(None, title="Description") - image: Optional[str] = Field(None, title="Image") + query_label: Optional[str] = Field( + None, + options={"hidden": True, "conditional_visible": {"modes": ["query"]}}, + title="Query label", + title_={"de": "Abfrage-Bezeichnung"}, + ) + description: Optional[List[Description]] = Field( + None, + eval_template={ + "type": "mustache-wikitext", + "mode": "render", + "$comment": "Displays value according to user language with eng as fallback option. Note: {{=<% %>=}} changes mustache expression from {{..}} to <% %> for mixing with wikitext templates", + "value": "{{=<% %>=}} {{#switch:{{USERLANGUAGECODE}} <%#description%> | {{#ifeq: <%lang%>|en|#default|<%lang%>}} = <%text%> <%/description%> }}", + }, + title="Description", + title_={"de": "Beschreibung"}, + ) + image: Optional[str] = Field( + None, + links=[ + { + "href": "{{#if self}}/w/index.php?title=Special:Redirect/file/{{self}}&width=200&height=200{{/if}}", + "mediaType": "image", + }, + { + "href": "{{#if self}}/w/index.php?title=Special:Redirect/file/{{self}}{{/if}}", + "rel": "{{#if self}}download{{/if}}", + "download": True, + }, + ], + options={"upload": {}}, + propertyOrder=1020, + title="Image", + title_={"de": "Bild"}, + ) ordering_categories: Optional[List[str]] = Field( - None, title="Classification categories" + None, + description_={ + "de": "Ordnungskategorien werden verwendet, um Instanzen zu kategorisieren, z. B. nach ihrer Verwendung, nicht aber nach ihren Eigenschaften. Bei der Abfrage nach Instanzen einer hier aufgeführten Ordnungskategorie wird diese Instanz zurückgegeben. Hinweis: Ordnungskategorien definieren keine Eigenschaften, während 'normale' Kategorien Eigenschaften definieren, denen eine Instanz Werte zuweist." + }, + eval_template={ + "type": "mustache-wikitext", + "mode": "render", + "value": "{{=<% %>=}}<%#ordering_categories%>[[:<%.%>]]
<%/ordering_categories%>", + }, + title="Classification categories", + title_={"de": "Ordnungskategorien"}, ) """ Classification categories are used to categorize instances, e.g., according to their use but not their properties. When querying for instances of a here listed classification category, this instance will be returned. Note: Classification categories define no properties, while 'regular' categories define properties, which an instance assigns values to. """ - keywords: Optional[List[str]] = Field(None, title="Keywords / Tags") + keywords: Optional[List[Keyword]] = Field( + None, + description_={"de": "Dient der nutzerdefinierten Kategorisierung des Elements"}, + eval_template={ + "type": "mustache-wikitext", + "mode": "render", + "value": "{{#keywords}}[[{{{.}}}]]
{{/keywords}}", + }, + range="Category:OSW09f6cdd54bc54de786eafced5f675cbe", + title="Keywords / Tags", + title_={"de": "Schlagworte / Tags"}, + ) """ Designated to the user defined categorization of this element """ - based_on: Optional[List[str]] = Field(None, title="Based on") + based_on: Optional[List[str]] = Field( + None, + description_={ + "de": "Andere Entitäten auf die diese basiert, z. B. wenn sie durch Kopieren entstanden ist" + }, + options={"hidden": "true"}, + title="Based on", + title_={"de": "Basierend auf"}, + ) """ Other entities on which this one is based, e.g. when it is created by copying """ statements: Optional[ List[Union[ObjectStatement, DataStatement, QuantityStatement]] - ] = Field(None, title="Statements") - attachments: Optional[List[str]] = Field(None, title="File attachments") - meta: Optional[Meta] = None + ] = Field( + None, + eval_template={ + "type": "mustache-wikitext", + "mode": "render", + "value": "{{#statements}}.. {{#predicate}}[[{{predicate}}]]{{/predicate}}{{#property}}[[{{property}}]]{{/property}}{{#quantity}}[[{{quantity}}]]{{/quantity}} {{#object}}[[{{object}}]]{{/object}}{{#value}}{{value}}{{/value}}
{{/statements}}", + }, + propertyOrder=1010, + title="Statements", + title_={"de": "Aussagen"}, + ) + attachments: Optional[List[str]] = Field( + None, + eval_template={ + "type": "mustache-wikitext", + "mode": "render", + "value": "{{=<% %>=}} <%={{ }}=%> {{#attachments}}{{{.}}};{{/attachments}} {{=<% %>=}} <%={{ }}=%>", + }, + options={"collapsed": True}, + propertyOrder=1030, + title="File attachments", + title_={"de": "Dateianhänge"}, + ) + meta: Optional[Meta] = Field(None, options={"hidden": True}) class ObjectStatement(OswBaseModel): class Config: schema_extra = {"title": "Object Statement"} - rdf_type: Optional[Any] = "rdf:Statement" - uuid: UUID = Field(default_factory=uuid4, title="UUID") - label: Optional[List[Label]] = Field(None, title="Label") + rdf_type: Optional[Any] = Field("rdf:Statement", options={"hidden": True}) + uuid: UUID = Field(default_factory=uuid4, options={"hidden": True}, title="UUID") + label: Optional[List[Label]] = Field( + None, options={"collapsed": True, "grid_columns": 12}, title="Label" + ) """ Human readable name """ - subject: Optional[str] = Field(None, title="Subject") + subject: Optional[str] = Field( + None, + options={ + "grid_columns": 4, + "autocomplete": {"query": "[[Category:Entity]]OR[[:Category:%2B]]"}, + }, + title="Subject", + title_={"de": "Subjekt"}, + ) substatements: Optional[ List[Union[ObjectStatement, DataStatement, QuantityStatement]] - ] = Field(None, title="Substatements") - predicate: str = Field(..., title="Predicate") - object: str = Field(..., title="Object") + ] = Field( + None, options={"grid_columns": 12}, propertyOrder=3010, title="Substatements" + ) + predicate: str = Field( + ..., + options={"grid_columns": 4, "autocomplete": {"category": "Category:Property"}}, + title="Predicate", + title_={"de": "Prädikat"}, + ) + object: str = Field( + ..., + options={ + "grid_columns": 4, + "autocomplete": {"query": "[[Category:Entity]]OR[[:Category:%2B]]"}, + }, + title="Object", + title_={"de": "Objekt"}, + ) class DataStatement(OswBaseModel): class Config: schema_extra = {"title": "Data Statement"} - rdf_type: Optional[Any] = "rdf:Statement" - uuid: UUID = Field(default_factory=uuid4, title="UUID") - label: Optional[List[Label]] = Field(None, title="Label") + rdf_type: Optional[Any] = Field("rdf:Statement", options={"hidden": True}) + uuid: UUID = Field(default_factory=uuid4, options={"hidden": True}, title="UUID") + label: Optional[List[Label]] = Field( + None, options={"collapsed": True, "grid_columns": 12}, title="Label" + ) """ Human readable name """ - subject: Optional[str] = Field(None, title="Subject") + subject: Optional[str] = Field( + None, + options={ + "grid_columns": 4, + "autocomplete": {"query": "[[Category:Entity]]OR[[:Category:%2B]]"}, + }, + title="Subject", + title_={"de": "Subjekt"}, + ) substatements: Optional[ List[Union[ObjectStatement, DataStatement, QuantityStatement]] - ] = Field(None, title="Substatements") - property: str = Field(..., title="Property") - value: str = Field(..., title="Value") + ] = Field( + None, options={"grid_columns": 12}, propertyOrder=3010, title="Substatements" + ) + property: str = Field( + ..., + options={ + "grid_columns": 4, + "autocomplete": {"category": "Category:DataProperty"}, + }, + title="Property", + title_={"de": "Attribut"}, + ) + value: str = Field( + ..., options={"grid_columns": 4}, title="Value", title_={"de": "Wert"} + ) class QuantityStatement(OswBaseModel): class Config: schema_extra = {"title": "Quantity Statement"} - rdf_type: Optional[Any] = "rdf:Statement" - uuid: UUID = Field(default_factory=uuid4, title="UUID") - label: Optional[List[Label]] = Field(None, title="Label") + rdf_type: Optional[Any] = Field("rdf:Statement", options={"hidden": True}) + uuid: UUID = Field(default_factory=uuid4, options={"hidden": True}, title="UUID") + label: Optional[List[Label]] = Field( + None, options={"collapsed": True, "grid_columns": 12}, title="Label" + ) """ Human readable name """ - subject: Optional[str] = Field(None, title="Subject") + subject: Optional[str] = Field( + None, + options={ + "grid_columns": 4, + "autocomplete": {"query": "[[Category:Entity]]OR[[:Category:%2B]]"}, + }, + title="Subject", + title_={"de": "Subjekt"}, + ) substatements: Optional[ List[Union[ObjectStatement, DataStatement, QuantityStatement]] - ] = Field(None, title="Substatements") - quantity: str = Field(..., title="Property") - numerical_value: str = Field(..., title="Value") - unit: str = Field(..., title="Unit") - unit_symbol: str - value: str = Field(..., title="Value") + ] = Field( + None, options={"grid_columns": 12}, propertyOrder=3010, title="Substatements" + ) + quantity: str = Field( + ..., + options={ + "grid_columns": 4, + "autocomplete": {"category": "Category:QuantityProperty"}, + }, + title="Property", + title_={"de": "Attribut"}, + ) + numerical_value: str = Field( + ..., options={"grid_columns": 2}, title="Value", title_={"de": "Wert"} + ) + unit: str = Field( + ..., + options={ + "grid_columns": 2, + "autocomplete": { + "query": "[[-HasInputUnit::{{$(quantity)}}]][[Display_title_of::like:*{{_user_input}}*]]|?Display_title_of=label", + "render_template": { + "type": ["handlebars"], + "value": "{{result.printouts.label.[0]}}", + }, + "field_maps": [ + { + "source_path": "$", + "template": "{{{result.printouts.label.[0]}}}", + "target_path": "$(unit_symbol)", + } + ], + }, + }, + propertyOrder=1010, + title="Unit", + title_={"de": "Einheit"}, + watch={ + "quantity": "statement.quantity", + "unit_symbol": "statement.unit_symbol", + }, + ) + unit_symbol: str = Field(..., options={"hidden": True}) + value: str = Field( + ..., + options={"hidden": True}, + template="{{{numerical_value}}} {{{unit_symbol}}}", + title="Value", + title_={"de": "Wert"}, + watch={ + "numerical_value": "statement.numerical_value", + "unit_symbol": "statement.unit_symbol", + }, + ) class Item(Entity): @@ -289,15 +563,129 @@ class Config: } type: Optional[List[str]] = Field( - ["Category:Item"], min_items=1, title="Types/Categories" + ["Category:Item"], + eval_template=[ + { + "type": "mustache-wikitext", + "mode": "render", + "value": "{{#type}} [[:{{{.}}}]]
{{/type}}", + } + ], + min_items=1, + options={ + "collapsed": True, + "conditional_visible": {"modes": ["default", "query"]}, + }, + propertyOrder=-1000, + title="Types/Categories", + title_={"de": "Typen/Kategorien"}, ) entry_access: Optional[AccessRestrictions] = Field( - None, title="Access restrictions" + None, + eval_template={ + "$comment": "See https://www.mediawiki.org/wiki/Extension:Semantic_ACL", + "type": "mustache-wikitext", + "mode": "render", + "value": "{{entry_access.read.level}} {{=<% %>=}} {{#set: |Visible to= {{#switch: <%={{ }}=%> {{{entry_access.read.level}}} {{=<% %>=}} |public=public |internal=users |restricted=whitelist |#default=}} }} <%={{ }}=%>", + }, + title="Access restrictions", + title_={"de": "Zugriffsbeschränkungen"}, ) +class IntangibleItem(Item): + """ + A utility class that serves as the umbrella for a number of 'intangible' things such as terms, quantities, structured values, etc. + """ + + class Config: + schema_extra = { + "@context": ["/wiki/Category:Item?action=raw&slot=jsonschema"], + "uuid": "cbb09a36-3367-40c6-a2cd-62db9bf647ec", + "title": "IntangibleItem", + "title*": {"en": "Intangible Item", "de": "Immaterieller Gegenstand"}, + "description": "A utility class that serves as the umbrella for a number of 'intangible' things such as terms, quantities, structured values, etc.", + "description*": { + "en": "A utility class that serves as the umbrella for a number of 'intangible' things such as terms, quantities, structured values, etc.", + "de": 'Eine Gebrauchsklasse, die als Dach für eine Reihe von "immateriellen" Dingen wie Begriffe, Mengen, strukturierte Werte usw. dient.', + }, + } + + type: Optional[Any] = ["Category:OSWcbb09a36336740c6a2cd62db9bf647ec"] + + +class DefinedTerm(IntangibleItem): + """ + Definition of a noun or compound word describing a specific concept + """ + + class Config: + schema_extra = { + "@context": [ + "/wiki/Category:OSWcbb09a36336740c6a2cd62db9bf647ec?action=raw&slot=jsonschema" + ], + "uuid": "a5812d3b-5119-416c-8da1-606cbe7054eb", + "title": "DefinedTerm", + "title*": {"en": "Defined Term", "de": "Definierter Begriff"}, + "description": "Definition of a noun or compound word describing a specific concept", + "description*": { + "en": "Definition of a noun or compound word describing a specific concept", + "de": "Definition eines Ausdruckes der ein spezifisches Konzept repräsentiert", + }, + } + + type: Optional[Any] = ["Category:OSWa5812d3b5119416c8da1606cbe7054eb"] + + +class Keyword(DefinedTerm): + """ + Used as a user defined tag to label entitites + """ + + class Config: + schema_extra = { + "@context": [ + "/wiki/Category:OSWa5812d3b5119416c8da1606cbe7054eb?action=raw&slot=jsonschema" + ], + "uuid": "09f6cdd5-4bc5-4de7-86ea-fced5f675cbe", + "title": "Keyword", + "title*": {"en": "Keyword", "de": "Schlüsselwort"}, + "description": "Used as a user defined tag to label entitites", + "description*": { + "en": "Used as a user defined tag to label entitites", + "de": "Wird als nutzerdefiniertes Etikett / Tag genutzt um Entitäten zu labeln", + }, + } + + type: Optional[Any] = ["Category:OSW09f6cdd54bc54de786eafced5f675cbe"] + + +Entity.update_forward_refs() +ObjectStatement.update_forward_refs() +DataStatement.update_forward_refs() +QuantityStatement.update_forward_refs() +Item.update_forward_refs() +IntangibleItem.update_forward_refs() +DefinedTerm.update_forward_refs() +Keyword.update_forward_refs() +# generated by datamodel-codegen: +# filename: Entity.json +# timestamp: 2025-05-18T15:16:03+00:00 + + +from enum import Enum +from typing import Any, List, Optional, Set, Union +from uuid import UUID + +from pydantic.v1 import Field, constr + +from osw.model.static import OswBaseModel + Entity.update_forward_refs() ObjectStatement.update_forward_refs() DataStatement.update_forward_refs() QuantityStatement.update_forward_refs() Item.update_forward_refs() +IntangibleItem.update_forward_refs() +DefinedTerm.update_forward_refs() +Keyword.update_forward_refs() diff --git a/src/osw/utils/codegen.py b/src/osw/utils/codegen.py deleted file mode 100644 index 6c467fcc..00000000 --- a/src/osw/utils/codegen.py +++ /dev/null @@ -1,59 +0,0 @@ -from pathlib import Path -from typing import Any, Dict - -from datamodel_code_generator import load_yaml_from_path -from datamodel_code_generator.model import pydantic as pydantic_v1_model -from datamodel_code_generator.model import pydantic_v2 as pydantic_v2_model -from datamodel_code_generator.parser.jsonschema import ( - JsonSchemaObject, - JsonSchemaParser, -) - -# https://docs.pydantic.dev/1.10/usage/schema/#schema-customization -# https://docs.pydantic.dev/latest/concepts/json_schema/#using-json_schema_extra-with-a-dict -# https://docs.pydantic.dev/latest/concepts/json_schema/#field-level-customization - - -class PydanticV1Config(pydantic_v1_model.Config): - # schema_extra: Optional[Dict[str, Any]] = None - schema_extra: str = None - - -class PydanticV2Config(pydantic_v2_model.ConfigDict): - # schema_extra: Optional[Dict[str, Any]] = None - json_schema_extra: str = None - - -class OOLDJsonSchemaParser(JsonSchemaParser): - """Custom parser for OO-LD schemas. - You can use this class directly or monkey-patch the datamodel_code_generator module: - `datamodel_code_generator.parser.jsonschema.JsonSchemaParser = OOLDJsonSchemaParser` - """ - - def set_additional_properties(self, name: str, obj: JsonSchemaObject) -> None: - schema_extras = repr(obj.extras) # keeps 'False' and 'True' boolean literals - if self.data_model_type == pydantic_v1_model.BaseModel: - self.extra_template_data[name]["config"] = PydanticV1Config( - schema_extra=schema_extras - ) - if self.data_model_type == pydantic_v2_model.BaseModel: - self.extra_template_data[name]["config"] = PydanticV2Config( - json_schema_extra=schema_extras - ) - return super().set_additional_properties(name, obj) - - -class OOLDJsonSchemaParserFixedRefs(OOLDJsonSchemaParser): - """Overwrite # overwrite the original `_get_ref_body_from_remote` function - to fix wrongly composed paths. This issue occurs only when using this parser class directy - and occurs not if used through mokey patching and `datamodel_code_generator.generate()` - """ - - def _get_ref_body_from_remote(self, resolved_ref: str) -> Dict[Any, Any]: - # full_path = self.base_path / resolved_ref - # fix: merge the paths correctly - full_path = self.base_path / Path(resolved_ref).parts[-1] - return self.remote_object_cache.get_or_put( - str(full_path), - default_factory=lambda _: load_yaml_from_path(full_path, self.encoding), - ) From 38fd0cfc11f9ba1da4d30bf3ee436e3653b4bbde Mon Sep 17 00:00:00 2001 From: SimonTaurus Date: Sat, 24 May 2025 11:30:27 +0200 Subject: [PATCH 6/6] refactor: move osw.model.static to opensemantic --- setup.cfg | 2 +- src/osw/auth.py | 2 +- src/osw/controller/database.py | 2 +- src/osw/controller/page_package.py | 2 +- src/osw/core.py | 12 +- src/osw/data/mining.py | 2 +- src/osw/defaults.py | 4 +- src/osw/express.py | 2 +- src/osw/model/entity.py | 8 +- src/osw/model/static.py | 391 ----------------------------- src/osw/ontology.py | 2 +- src/osw/utils/regex.py | 3 +- src/osw/utils/wiki.py | 2 +- src/osw/wiki_tools.py | 2 +- src/osw/wtsite.py | 2 +- 15 files changed, 21 insertions(+), 417 deletions(-) delete mode 100644 src/osw/model/static.py diff --git a/setup.cfg b/setup.cfg index 3b0b2345..cdb5aaf7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,7 @@ package_dir = # For more information, check out https://semver.org/. install_requires = oold + opensemantic pydantic>=1.10.17 datamodel-code-generator>=0.25 mwclient>=0.11.0 @@ -69,7 +70,6 @@ install_requires = asyncio tqdm pybars3-wheel - backports.strenum; python_version<"3.11" [options.packages.find] where = src diff --git a/src/osw/auth.py b/src/osw/auth.py index 93e57c0d..1df2f407 100644 --- a/src/osw/auth.py +++ b/src/osw/auth.py @@ -7,10 +7,10 @@ from warnings import warn import yaml +from opensemantic import OswBaseModel from pydantic.v1 import PrivateAttr from osw.defaults import paths as default_paths -from osw.model.static import OswBaseModel if TYPE_CHECKING: PossibleFilePath = Path diff --git a/src/osw/controller/database.py b/src/osw/controller/database.py index 8bc65ec6..16b7a0d2 100644 --- a/src/osw/controller/database.py +++ b/src/osw/controller/database.py @@ -1,5 +1,6 @@ from typing import Optional, Union +from opensemantic import OswBaseModel from sqlalchemy import URL, create_engine from sqlalchemy import text as sql_text from sqlalchemy.engine import Engine @@ -7,7 +8,6 @@ import osw.model.entity as model from osw.auth import CredentialManager from osw.core import OSW -from osw.model.static import OswBaseModel class DatabaseController(model.Database): diff --git a/src/osw/controller/page_package.py b/src/osw/controller/page_package.py index 26bc70ae..4c74b42d 100644 --- a/src/osw/controller/page_package.py +++ b/src/osw/controller/page_package.py @@ -6,6 +6,7 @@ from typing import Optional, Union from warnings import warn +from opensemantic import OswBaseModel from pydantic.v1 import FilePath from typing_extensions import Dict, List @@ -13,7 +14,6 @@ from osw.auth import CredentialManager 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.regex import RegExPatternExtended from osw.wtsite import WtPage, WtSite diff --git a/src/osw/core.py b/src/osw/core.py index 3b5f301f..23f073c3 100644 --- a/src/osw/core.py +++ b/src/osw/core.py @@ -27,13 +27,13 @@ set_resolver, ) from oold.utils.codegen import OOLDJsonSchemaParser +from opensemantic import OswBaseModel from pydantic import PydanticDeprecatedSince20 from pydantic.v1 import BaseModel, Field, PrivateAttr, create_model, validator from pyld import jsonld import osw.model.entity as model from osw.defaults import params as default_params -from osw.model.static import OswBaseModel from osw.utils.oold import ( AggregateGeneratedSchemasParam, AggregateGeneratedSchemasParamMode, @@ -587,7 +587,7 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: --input {schema_path} \ --input-file-type jsonschema \ --output {temp_model_path} \ - --base-class osw.model.static.OswBaseModel \ + --base-class opensemantic.OswBaseModel \ --use-default \ --use-unique-items-as-set \ --enum-field-as-literal all \ @@ -614,7 +614,7 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: input_=pathlib.Path(schema_path), input_file_type="jsonschema", output=pathlib.Path(temp_model_path), - base_class="osw.model.static.OswBaseModel", + base_class="opensemantic.OswBaseModel", # use_default=True, apply_default_values_for_required_fields=True, use_unique_items_as_set=True, @@ -647,7 +647,7 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: # parser = OOLDJsonSchemaParserFixedRefs( # source=pathlib.Path(schema_path), - # base_class="osw.model.static.OswBaseModel", + # base_class="opensemantic.OswBaseModel", # data_model_type=data_model_types.data_model, # data_model_root_type=data_model_types.root_model, # data_model_field_type=data_model_types.field_model, @@ -720,8 +720,8 @@ def _fetch_schema(self, fetchSchemaParam: _FetchSchemaParam = None) -> None: header = ( "from uuid import uuid4\n" "from typing import Type, TypeVar\n" - "from osw.model.static import OswBaseModel, Ontology\n" - # "from osw.model.static import *\n" + "from opensemantic import OswBaseModel\n" + # "from opensemantic import *\n" "\n" ) diff --git a/src/osw/data/mining.py b/src/osw/data/mining.py index 2ac86a73..579528c8 100644 --- a/src/osw/data/mining.py +++ b/src/osw/data/mining.py @@ -6,5 +6,5 @@ # # from pydantic import validator # -# from osw.model.static import OswBaseModel +# from opensemantic import OswBaseModel # from osw.utils.strings import * diff --git a/src/osw/defaults.py b/src/osw/defaults.py index e76acd03..56352bd1 100644 --- a/src/osw/defaults.py +++ b/src/osw/defaults.py @@ -4,9 +4,7 @@ from pathlib import Path from typing import List, Union -from pydantic.v1 import PrivateAttr, validator - -from osw.model.static import BaseModel +from pydantic.v1 import BaseModel, PrivateAttr, validator PACKAGE_ROOT_PATH = Path(__file__).parents[2] SRC_PATH = PACKAGE_ROOT_PATH / "src" diff --git a/src/osw/express.py b/src/osw/express.py index 7cb52a7c..0cf1e477 100644 --- a/src/osw/express.py +++ b/src/osw/express.py @@ -12,6 +12,7 @@ from warnings import warn import requests +from opensemantic import OswBaseModel from pydantic.v1 import validator from typing_extensions import ( IO, @@ -30,7 +31,6 @@ from osw.core import OSW, OVERWRITE_CLASS_OPTIONS, OverwriteOptions from osw.defaults import params as default_params from osw.defaults import paths as default_paths -from osw.model.static import OswBaseModel from osw.utils.wiki import namespace_from_full_title, title_from_full_title from osw.wtsite import WtSite diff --git a/src/osw/model/entity.py b/src/osw/model/entity.py index b8825159..d2b2666d 100644 --- a/src/osw/model/entity.py +++ b/src/osw/model/entity.py @@ -8,10 +8,9 @@ from typing import Any, List, Optional, Set, Union from uuid import UUID +from opensemantic import OswBaseModel from pydantic.v1 import Field, constr -from osw.model.static import OswBaseModel - class Level(str, Enum): public = "public" @@ -22,7 +21,7 @@ class Level(str, Enum): from typing import Type, TypeVar from uuid import uuid4 -from osw.model.static import Ontology, OswBaseModel +from opensemantic import OswBaseModel class ReadAccess(OswBaseModel): @@ -677,10 +676,9 @@ class Config: from typing import Any, List, Optional, Set, Union from uuid import UUID +from opensemantic import OswBaseModel from pydantic.v1 import Field, constr -from osw.model.static import OswBaseModel - Entity.update_forward_refs() ObjectStatement.update_forward_refs() DataStatement.update_forward_refs() diff --git a/src/osw/model/static.py b/src/osw/model/static.py deleted file mode 100644 index e8b09f57..00000000 --- a/src/osw/model/static.py +++ /dev/null @@ -1,391 +0,0 @@ -""" -This module is to be imported in the dynamically created and updated entity.py module. -""" - -from typing import TYPE_CHECKING, Literal, Optional, Type, TypeVar, Union -from uuid import UUID, uuid4 -from warnings import warn - -from oold.model.v1 import LinkedBaseModel -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) - -# This is dirty, but required for autocompletion: -# https://stackoverflow.com/questions/62884543/pydantic-autocompletion-in-vs-code -# Ideally, solved by custom templates in the future: -# https://github.com/koxudaxi/datamodel-code-generator/issues/860 -# ToDo: Still needed? - -if TYPE_CHECKING: - from dataclasses import dataclass as _basemodel_decorator -else: - _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(LinkedBaseModel): - - class Config: - """Configuration for the OswBaseModel""" - - # strict = False - # extra = "ignore" - # Additional fields are ignored - validate_assignment = True - # Ensures that the assignment of a value to a field is validated - smart_union = True - # To avoid unexpected coercing of types, the smart_union option is enabled - # See: https://docs.pydantic.dev/1.10/usage/model_config/#smart-union - # Not required in v2 as this will become the new default - - 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) - if "uuid" not in data: - # If no uuid is provided, generate a new one - data["uuid"] = OswBaseModel._init_uuid(**data) - super().__init__(**data) - - @classmethod - def _init_uuid(cls, **data) -> UUID: - """Generates a random UUID for the entity if not provided during initialization. - This method can be overridden to generate a UUID based on the data, e.g. - for using a UUIDv5 based on the name: - ```python - def _get_uuid(**data) -> UUID: - namespace_uuid = uuid.UUID("0dd6c54a-b162-4552-bab9-9942ccaf4f41") - return uuid.uuid5(namespace_uuid, data["name"]) - ``` - """ - - # default: random UUID - return uuid4() - - def full_dict(self, **kwargs): # extent BaseClass export function - d = super().dict(**kwargs) - for key in ("_osl_template", "_osl_footer"): - if hasattr(self, key): - d[key] = getattr(self, key) - # Include selected private properties. note: private properties are not - # considered as discriminator - return d - - def cast( - self, - cls: Union[Type[T], type], - none_to_default: bool = False, - remove_extra: bool = False, - silent: bool = True, - **kwargs, - ) -> T: - """Casting self into target class - - Parameters - ---------- - cls - target class - kwargs - additional attributes to be set - none_to_default - If True, attributes that are None will be set to their default value - remove_extra - If True, extra attributes that are passed to the constructor are removed - silent - If True, no warnings are printed - Returns - ------- - instance of target class - """ - - def empty_list_or_none( - obj: Union[ - NoneType, - list, - ] - ) -> bool: - if obj is None: - return True - elif isinstance(obj, list): - if len(obj) == 0: - return True - elif len([item for item in obj if item is not None]) == 0: - return True - return False - - combined_args = {**self.dict(), **kwargs} - none_args = [] - if none_to_default: - reduced = {} - for k, v in combined_args.items(): - if empty_list_or_none(v): - none_args.append(k) - else: - reduced[k] = v - combined_args = reduced - extra_args = [] - if remove_extra: - reduced = {} - for k, v in combined_args.items(): - if k not in cls.__fields__.keys(): - extra_args.append(k) - else: - reduced[k] = v - combined_args = reduced - if not silent: - if none_to_default and none_args: - warn(f"Removed attributes with None or empty list values: {none_args}") - if remove_extra and extra_args: - warn(f"Removed extra attributes: {extra_args}") - if "type" in combined_args: - del combined_args["type"] - return cls(**combined_args) - - def cast_none_to_default(self, cls: Union[Type[T], type], **kwargs) -> T: - """Casting self into target class. If the passed attribute is None or solely - includes None values, the attribute is not passed to the instance of the - target class, which will then fall back to the default.""" - - return self.cast(cls, none_to_default=True, **kwargs) - - 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, 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 - """ - return get_namespace(self) - - def get_title(self) -> Union[str, NoneType]: - """Determines the wiki page title based on the entity's data - - 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 - - Returns - ------- - The full title as a string or None if the title could not be determined. - """ - 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): - iri: str - prefix: str - 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/ontology.py b/src/osw/ontology.py index 749b1259..fa72dfac 100644 --- a/src/osw/ontology.py +++ b/src/osw/ontology.py @@ -4,13 +4,13 @@ import uuid from typing import Dict, List, Literal, Optional, Type +from opensemantic import OswBaseModel from pydantic.v1 import PrivateAttr from pyld import jsonld from rdflib import Graph from typing_extensions import deprecated from osw.core import OSW, model -from osw.model.static import OswBaseModel from osw.utils.strings import camel_case, pascal_case from osw.utils.wiki import get_namespace from osw.wtsite import WtSite diff --git a/src/osw/utils/regex.py b/src/osw/utils/regex.py index 3012d2d3..46c5a88c 100644 --- a/src/osw/utils/regex.py +++ b/src/osw/utils/regex.py @@ -1,10 +1,9 @@ import re from typing import Dict, List, Optional, Union +from opensemantic import OswBaseModel from pydantic.v1 import validator -from osw.model.static import OswBaseModel - # Classes class RegExPatternExtended(OswBaseModel): diff --git a/src/osw/utils/wiki.py b/src/osw/utils/wiki.py index 31496270..2d5bda85 100644 --- a/src/osw/utils/wiki.py +++ b/src/osw/utils/wiki.py @@ -2,7 +2,7 @@ from uuid import UUID # Legacy imports: -from osw.model.static import get_full_title, get_namespace, get_title # noqa: F401 +from opensemantic import get_full_title, get_namespace, get_title # noqa: F401 def get_osw_id(uuid: UUID) -> str: diff --git a/src/osw/wiki_tools.py b/src/osw/wiki_tools.py index 6ce979fb..c32294af 100644 --- a/src/osw/wiki_tools.py +++ b/src/osw/wiki_tools.py @@ -3,9 +3,9 @@ import mwclient import yaml +from opensemantic import OswBaseModel from pydantic.v1 import FilePath -from osw.model.static import OswBaseModel from osw.utils.util import parallelize # try import functions from wikitext.py (relies on the extra dependency osw[wikitext]) diff --git a/src/osw/wtsite.py b/src/osw/wtsite.py index d4e7a0e4..9b4db054 100644 --- a/src/osw/wtsite.py +++ b/src/osw/wtsite.py @@ -22,6 +22,7 @@ import requests from jsonpath_ng.ext import parse from mwclient.page import Page as MwPage +from opensemantic import OswBaseModel from pydantic.v1 import FilePath from typing_extensions import deprecated @@ -29,7 +30,6 @@ import osw.utils.util as ut import osw.wiki_tools as wt from osw.auth import CredentialManager -from osw.model.static import OswBaseModel from osw.utils.regex_pattern import REGEX_PATTERN_LIB from osw.utils.util import parallelize from osw.utils.wiki import get_osw_id