Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Changelog
All notable changes to this project will be documented in this file.

## [1.2.3] - 2025-01-12
### Added
- Added support for namespaced identifiers, where one or more prefixes can be registered
with the library and be used in constructing a document, with the library replacing the
prefixes with their associated URIs during document serialization.

## [1.2.2] - 2025-10-07
### Added
- Improved support for aliases, canonical property names and namespaced property names.
Expand Down
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ from semanticpy import Model
# argument making the class types available for use just by referencing their names:
Model.factory(profile="linked-art", globals=globals())

# Register one or more identifier prefixes that take the form "<prefix>:" when used in
# an identifier and are replaced with the specified URIs during document serialization:
Model.prefix("aat", "http://vocab.getty.edu/aat/")

# Create a HumanMadeObject (HMO) model instance
hmo = HumanMadeObject(
ident = "https://example.org/object/1",
Expand All @@ -26,14 +30,14 @@ hmo = HumanMadeObject(

# Assign a classification of "Works of Art" to the HMO as per the Linked.Art model
hmo.classified_as = Type(
ident = "http://vocab.getty.edu/aat/300133025",
ident = "aat:300133025",
label = "Works of Art",
)

# As this example HMO represents a painting, add a classification of "Paintings" as per
# the Linked.Art model to specify the type of artwork that this HMO represents:
hmo.classified_as = typed = Type(
ident = "http://vocab.getty.edu/aat/300033618",
ident = "aat:300033618",
label = "Paintings (Visual Works)",
)

Expand Down Expand Up @@ -124,11 +128,13 @@ The SemanticPy library is available from the PyPi repository, so may be added to

The primary interface to the SemanticPy library is its `Model` class which offers the following methods:

* `factory()` – the `factory()` method is used to initialise the model for use.
* `factory()` – the `factory()` class method is used to initialise the model for use.

* `teardown()` – the `teardown()` class method is used to de-initialise the model, reversing the setup performed by the `factory()` method.

* `teardown()` – the `teardown()` method is used to de-initialise the model, reversing the setup performed by the `factory()` method.
* `extend()` – the `extend()` class method is used to support extending the factory-generated model with additional model subclasses, and optionally, additional model-wide properties.

* `extend()` – the `extend()` method is used to support extending the factory-generated model with additional model subclasses, and optionally, additional model-wide properties.
* `prefix(prefix: str, uri: str)` – the `prefix()` class method can be used to register one or more identifier prefixes with the library that will be replaced with the specified URI during document serialization.

* `entity()` – the `entity()` method may be used to obtain the `type` reference for a named model entity, from which a new instance of that named model entity may be created.

Expand All @@ -148,26 +154,36 @@ The primary interface to the SemanticPy library is its `Model` class which offer
* `documents()` – the `documents()` method may be used to obtain a list of model entity
documents from the current model instance; the `documents()` method accepts the following
arguments, which control whether nodes of the following types will be including in the resulting list:

* `blank` (`bool`) – (optional) to return any blank nodes within the current document, leave the
`blank` argument set to its default value of `True` or to omit blank nodes, set to `False`;

* `embedded` (`bool`) – (optional) to return any embedded nodes within the current document, leave the
`embedded` argument set to its default value of `True` or to omit embedded nodes, set to `False`;

* `referenced` (`bool`) – (optional) to return any referenced documents within the current document,
leave the `referenced` argument to its default value of `True` or to omit any referenced nodes, set to `False`;

* `filter` (`callable`) – (optional) to achieve finer-grained control over whether nodes are include in the resulting list, a callback method can be provided to the method via the `filter` argument; the callback method must take a reference to the current document, and its containing entity, and must return a `bool` value each time it is called; to include a node in the returned list via custom filtering, the method must return `True` and to omit the node, the method must return `False`.

### Properties

The `Model` class offers the following named properties in addition to the methods defined above:

* `name` – the `name` (`str`) property provides access to the model instance's class name.

* `label` – the `label` (`str` | `None`) property provides access to the model instance's assigned label, if any.

* `ident` – the `ident` (`str` | `None`) property provides access to the model instance's assigned identifier, if any.

* `is_blank` – the `is_blank` (`bool`) property may be used to determine if the current model instance is considered a blank node or not – a blank node is a model node without an assigned identifier. The `is_blank` property will be `True` if the node is blank (lacks an identifier) or `False` otherwise.

* `is_cloned` – the `is_cloned` (`bool`) property may be used to determine if the current model instance is a clone of another node or not. The `is_cloned` property will be `True` if the current model instance is a clone of another or will be `False` otherwise.

* `is_reference` – the `is_reference` (`bool`) property may be used to determine if the current model instance is a reference to another node or not. The `is_reference` property will be `True` if the current model instance is a reference to another or will be `False` otherwise.

* `was_referenced` – the `was_referenced` (`bool`) property may be used to determine if one or more references have been created to the current model instance or not, via the `reference` method. The `was_referenced` property will be `True` if at least one reference has previously been generated for the current model instance via the `reference` method or will be `False` otherwise.

### License and Copyright Information

Copyright © 2022–2025 Daniel Sissman; licensed under the MIT License.
Copyright © 2022–2026 Daniel Sissman; licensed under the MIT License.
2 changes: 1 addition & 1 deletion requirements.development.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SemanticPy Dependencies: Development
black==24.3.0
black==26.1.0
pytest==8.3.0
pytest-cov==4.1.0
pytest-codeblocks==0.17.0
Expand Down
89 changes: 66 additions & 23 deletions source/semanticpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
readonlydict,
)


logger.debug("semanticpy library imported from: %s" % (__file__))


Expand All @@ -28,6 +27,7 @@ class Model(Node):
_properties: dict[str, dict] = {}
_hidden: list[str] = []
_globals: dict[str, object] = None
_prefixes: dict[str, str] = {}

@classmethod
def factory(
Expand Down Expand Up @@ -120,8 +120,6 @@ def factory(
)

def _class_factory(name: str) -> type:
nonlocal cls, glo, entities

# If the named class already exists, return immediately
if isinstance(class_type := cls._entities.get(name, default=None), type):
if issubclass(class_type, Model):
Expand Down Expand Up @@ -320,7 +318,7 @@ def open(cls, filepath: str) -> Model:
or filepath.startswith("//")
):
try:
if isinstance(response := requests.get(url), object):
if isinstance(response := requests.get(filepath), object):
if response.status_code == 200:
if not isinstance(data := response.json(), dict):
raise ValueError(
Expand Down Expand Up @@ -357,7 +355,14 @@ def open(cls, filepath: str) -> Model:
raise ValueError("The specified filepath (%s) is unsupported!" % (filepath))

if isinstance(data, dict):
if context := data.get("@context"):
if isinstance(context := data.get("@context"), (str, dict, list)):
logger.debug(
"%s.open(filepath: %s) context => %s",
cls.__name__,
filepath,
context,
)

if typed := data.get("type"):
if entity := cls.entity(typed):
if instance := entity(data=readonlydict(data)):
Expand Down Expand Up @@ -590,6 +595,30 @@ def extend(
# Add the class to global namespace so that it can be accessed elsewhere
glo[name] = subclass

@classmethod
def prefix(cls, prefix: str, uri: str) -> None:
if not isinstance(prefix, str):
raise TypeError("The 'prefix' argument must have a string value!")
elif ":" in prefix:
if prefix.endswith(":"):
prefix = prefix[0:-1]
else:
raise ValueError(
"The 'prefix' value may only contain at most one semi-colon at the end of the string!"
)

if not isinstance(uri, str):
raise TypeError("The 'uri' argument must have a string value!")
elif not (uri.startswith("http://") or uri.startswith("https://")):
raise ValueError("The 'uri' value must start with 'http://' or 'https://'!")

if prefix in cls._prefixes:
raise ValueError(
f"The '{prefix}' prefix has already been registered to '{cls._prefixes[prefix]}'!"
)

cls._prefixes[prefix] = uri

@classmethod
def entity(cls, name: str = None, property: str = None) -> Model | None:
"""Helper method to return the referenced entity type from the model"""
Expand Down Expand Up @@ -895,27 +924,17 @@ def __setattr__(self, name: str, value: object) -> None:
)
)

if domain := prop.get("domain"):
pass
if isinstance(domain := prop.get("domain"), str):
logger.debug(
"%s.__setattr__(name: %s, value: %s) domain => %s",
self.__class__.__name__,
name,
value,
domain,
)

return super().__setattr__(name, value)

def _serialize(
self,
source=None,
sorting: list[str] | dict[str, int] = None,
) -> str:
"""Support serializing the current model instance into JSON-LD."""

data: str = super()._serialize(source=source, sorting=sorting)

if self._hidden and isinstance(data, dict):
for prop in self._hidden:
if prop in data:
del data[prop]

return data

@property
def is_blank(self) -> bool:
"""Determine if a node is a blank node (i.e. that it does not have an id)."""
Expand Down Expand Up @@ -980,6 +999,30 @@ def was_referenced(self) -> bool:

return self._referenced is True

def _serialize(
self,
source: object = None,
sorting: list[str] | dict[str, int] = None,
) -> object:
"""Support serializing the current model instance into JSON-LD."""

if isinstance(source, dict):
source = dict(source)

if isinstance(identifier := source.get("id"), str):
for prefix, uri in self.__class__._prefixes.items():
if identifier.startswith(prefix + ":"):
source["id"] = identifier.replace(prefix + ":", uri)

data: object = super()._serialize(source=source, sorting=sorting)

if isinstance(self._hidden, list) and isinstance(data, dict):
for prop in self._hidden:
if prop in data:
del data[prop]

return data

def properties(
self,
sorting: list[str] | dict[str, int] = None,
Expand Down
2 changes: 2 additions & 0 deletions source/semanticpy/errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from semanticpy.logging import logger

logger = logger.getChild(__name__)


class SemanticPyError(RuntimeError):
def __init__(self, message: str = "SemanticPy Error"):
Expand Down
7 changes: 7 additions & 0 deletions source/semanticpy/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@
from semanticpy.types.dictionary import readonlydict
from semanticpy.types.namespace import Namespace
from semanticpy.types.node import Node

__all__ = [
"Attributed",
"readonlydict",
"Namespace",
"Node",
]
4 changes: 1 addition & 3 deletions source/semanticpy/types/attributed.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import copy
import json

from semanticpy.logging import logger


Expand Down Expand Up @@ -64,4 +61,5 @@ def get(self, key: object, default: object = None) -> object | None:
try:
return self.__getattr__(key)
except AttributeError as exception:
logger.debug(str(exception))
return default
5 changes: 2 additions & 3 deletions source/semanticpy/types/dictionary.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import copy
import json

from semanticpy.logging import logger

logger = logger.getChild(__name__)


class readonlydict(dict):
"""A subclass of the standard library dictionary that is read-only"""
Expand Down
5 changes: 2 additions & 3 deletions source/semanticpy/types/namespace.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import copy
import json

from semanticpy.logging import logger
from semanticpy.types.attributed import Attributed

logger = logger.getChild(__name__)


class Namespace(Attributed):
pass
Loading