diff --git a/CHANGELOG.md b/CHANGELOG.md index 62b3772f..6e47d5cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,3 +66,17 @@ For more detail see #29. - addition of a `Link` class, to allow making reference to remote graph nodes defined by their `@id` that are not present locally. - improved CI testing: we now test v3 and v4, as well as "latest". + +## Release 0.4.0 (2025-11-18) + +- drop support for Python 3.8, add support for Python 3.14. +- more forgiving import of JSON-LD: + - an option to allow additional (non-openMINDS) keys in a JSON-LD document [#63](https://github.com/openMetadataInitiative/openMINDS_Python/pull/63) + - support fully-expanded IRIs as keys in JSON-LD documents [#64](https://github.com/openMetadataInitiative/openMINDS_Python/pull/64) + - accept `datetime` strings for properties with type `date` [#65](https://github.com/openMetadataInitiative/openMINDS_Python/pull/65) + - accept `"@type": []` as well as `"@type": ` [#66](https://github.com/openMetadataInitiative/openMINDS_Python/pull/66) +- make the class registry reusable by other packages [#70](https://github.com/openMetadataInitiative/openMINDS_Python/pull/70) +- bug fix: prevent infinite recursion in `validate()` where there are loops in the graph [#76](https://github.com/openMetadataInitiative/openMINDS_Python/pull/76) +- allow the user to specify which openMINDS version should be used by `Collection.load()` [#77](https://github.com/openMetadataInitiative/openMINDS_Python/pull/77) +- add the option to group files into subdirectories by schema when saving [#80](https://github.com/openMetadataInitiative/openMINDS_Python/pull/80) +- improvements to the `by_name()` method [#81](https://github.com/openMetadataInitiative/openMINDS_Python/pull/81) diff --git a/build.py b/build.py index 6c831bba..59a8c46d 100644 --- a/build.py +++ b/build.py @@ -120,7 +120,7 @@ env = Environment(loader=FileSystemLoader(os.path.dirname(os.path.realpath(__file__))), autoescape=select_autoescape()) context = { - "version": "0.3.1", + "version": "0.4.0", } if args.branch == "development": context["version"] += ".dev" diff --git a/pipeline/src/base.py b/pipeline/src/base.py index 58d7aa95..2068e5be 100644 --- a/pipeline/src/base.py +++ b/pipeline/src/base.py @@ -284,6 +284,7 @@ def to_jsonld(self): "@id": self.identifier } + class IRI: """ Representation of an International Resource Identifier diff --git a/pipeline/src/collection.py b/pipeline/src/collection.py index b3ea7eb9..78597a2e 100644 --- a/pipeline/src/collection.py +++ b/pipeline/src/collection.py @@ -5,7 +5,9 @@ The collection can be saved to and loaded from disk, in JSON-LD format. """ +from collections import Counter from glob import glob +from importlib import import_module import json import os from .registry import lookup_type @@ -70,6 +72,30 @@ def _sort_nodes_by_id(self): sorted_nodes = dict(sorted(self.nodes.items())) self.nodes = sorted_nodes + def generate_ids(self, id_generator): + """ + Generate an IRI id for all nodes in the graph that do not possess one. + + Args + ---- + + id_generator (function): + a function that takes the node as an argument, and returns a unique IRI + """ + for node_id in list(self.nodes.keys()): + if node_id.startswith("_:"): + node = self.nodes.pop(node_id) + node.id = id_generator(node) + self.nodes[node.id] = node + + @property + def complete(self): + """Do all nodes have an IRI?""" + for node_id in self.nodes: + if node_id.startswith("_:"): + return False + return True + def save(self, path, individual_files=False, include_empty_properties=False, group_by_schema=False): """ Save the node collection to disk in JSON-LD format. @@ -171,8 +197,18 @@ def load(self, *paths, version=DEFAULT_VERSION): 2) one or more JSON-LD files, which will all be loaded. By default, openMINDS v4 will be used. - If the JSON-LD files use a different openMINDS version, specify it with the `version` argument. + If the JSON-LD files use a different openMINDS version, specify it + with the `version` argument, e.g.:: + + import openminds.latest + + c = Collection() + c.load("/path/to/my/metadata.jsonld", version="latest") + """ + + import_module(f"openminds.{version}") + if len(paths) == 1 and os.path.isdir(paths[0]): data_dir = paths[0] json_paths = ( @@ -199,7 +235,7 @@ def load(self, *paths, version=DEFAULT_VERSION): self.add(node) else: if "@type" in data: - cls = lookup_type(data["@type"]) + cls = lookup_type(data["@type"], version=version) node = cls.from_jsonld(data) else: # allow links to metadata instances outside this collection @@ -260,3 +296,12 @@ def sort_nodes_for_upload(self): newly_sorted.append(node_id) unsorted -= set(newly_sorted) return [self.nodes[node_id] for node_id in sorted] + + def statistics(self): + """ + Return a counter containing the number of nodes of each type. + """ + stats = Counter( + node.__class__.__name__ for node in self.nodes.values() + ) + return stats