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
4 changes: 2 additions & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ jobs:
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
python -m pip install black pytest
if [ -f ./requirements.txt ]; then pip install -r ./requirements.txt; fi
python -m pip install --requirement ./requirements.txt
python -m pip install --requirement ./requirements.development.txt
- name: Lint with Black
run: |
# Stop the build if there are any Python syntax or formatting errors
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# Changelog
All notable changes to this project will be documented in this file.

## [1.3.2] - 2026-05-19
### Added
- Improved assembly and filtering of existing JSON-LD documents.

## [1.3.1] - 2026-05-11
### Added
- Improved item comparison.

## [1.3.0] - 2026-05-08
### Added
- Item comparison support.

## [1.2.9] - 2026-04-08
### Added
- Improved documentation regarding model profiles and added an example profile.
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ elif [[ "${SERVICE}" == "flakes" ]]; then
elif [[ "${SERVICE}" == "tests" ]]; then
echo -e "pytest /tests ${ARGS[@]}";
pytest /tests ${ARGS[@]};
pytest --verbose --codeblocks /README.md;
pytest --verbose --codeblocks --rootdir=/tests /tests/README.md ${ARGS[@]};
elif [[ "${SERVICE}" == "all" ]]; then
if [[ "${ARGS[0]}" == "--reformat" ]]; then
echo -e "black --verbose ${ARGS[@]:1} /source /tests";
Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ Model.teardown()
The SemanticPy library supports the concept of model profiles, which are used to define
a metadata model, including its available entity type classes and properties. Profiles are stored as JSON documents, with sections to specify top-level properties, available model entity classes, as well as any class-level properties. Profiles support specifying the cardinality, domain and range of each of the properties, where this information is used to validate a model document as it is being assembled by ensuring that only values which are valid for a given property, can be set on that property.

#### Included Model Profiles
### Included Model Profiles

The [Linked.Art](https://linked.art) profile is included with the SemanticPy library. It also acts as an example of how to specify a SemanticPy model profile file. Profiles can be developed for any valid JSON-LD metadata model. Additional profiles may be added to the library over time.

Expand All @@ -486,7 +486,7 @@ path, including the `.json` file extension:
Model.factory(profile="/absolute/path/to/model/profile.json")
```

#### Model Profile Structure
### Model Profile Structure

Each model profile is described within a JSON document; the document contains a dictionary
with the following top-level keys: `properties` and `entities` – the `properties` key is
Expand Down Expand Up @@ -517,7 +517,7 @@ Metadata model profiles accept the following keywords:
| `scope_note` | Specifies a property's scope note for documentation | string |
| `sorting` | Optionally specifies a property's serialised sorting | integer |

#### Top-Level Properties
### Top-Level Properties

Properties, whether they are top-level or class-level, are referenced by name, and are defined through a key in a `properties` dictionary either at the top-level of the profile
for top-level properties, or through a `properties` key on a model class entry under the
Expand All @@ -526,15 +526,15 @@ top-level `entities` key.
Each model property dictionary entry then defines the property's attributes, including
its cardinality (can the property store one value or possibly multiple values?), its supported domain and range (what types of value does the property accept, including primitive types such as strings, integers, floating point numbers, dates, or class types defined in the model), and an optional sort ordering that is used to sort the properties used in a model document when it is serialised into JSON-LD. Sorting properties into a desired order is optional, but can help improve the readability of serialised JSON-LD, and navigation of the data within a document by outputting the JSON-LD dictionary keys in a consistent order.

#### Model Entity Classes and Class-Level Properties
### Model Entity Classes and Class-Level Properties

Model entity classes are referenced by the name that is used for the class in code, and
are defined through a key in the top-level `entities` dictionary of the profile.

Each model entity class dictionary entry then defines the class' attributes, including
its identifier, model type name, model names, and class code name, its superclass or superclasses, and any class-level properties (and the attributes of those properties).

#### Sample Model Profile
### Sample Model Profile

The below sample model profile shows the required structure along with sample properties
and classes to demonstrate defining a JSON-LD model for the SemanticPy library.
Expand Down Expand Up @@ -611,14 +611,15 @@ In order to expand the generated JSON-LD to its graph representation, the contex

The properties and classes that are referenced in the model profile should be present in the referenced JSON-LD context document in order for those classes and properties to be included when the graph expansion is performed. Additional properties can be specified in the model profile, or at runtime using the `Model.extend()` method documented above, but these additional properties will only be included in the JSON-LD representation of the data, which may be necessary or desirable for some use-cases. Graph expansion libraries like `pyld` or `rdflib` however need access to a definition of each class or property from a context document in order to understand how to translate each JSON-LD class and property to its graph representation.

#### Sample Model Document
### Sample Model Document

A sample model document using the sample model profile specified above can be generated using code similar to the following:
A sample model document using the sample model profile specified above can be generated
using code similar to the following:

```python
import semanticpy

model = semanticpy.Model.factory(profile="sample", globals=globals())
semanticpy.Model.factory(profile="sample", globals=globals())

entity = Entity(
ident="https://data.example.org/entities/123",
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# SemanticPy Dependencies
requests>=2.32.5
enumerific>=1.1.0
requests>=2.32.5,<3.0
enumerific>=1.1.0,<1.2
15 changes: 10 additions & 5 deletions source/semanticpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ def teardown(cls, globals: dict = None):
for key in removals:
del cls._entities[key]

if isinstance(glo, dict):
if isinstance(glo, dict) and key in glo:
del glo[key]

# Reset the configuration to the defaults
Expand Down Expand Up @@ -650,7 +650,7 @@ def prefix(cls, prefix: str, uri: str) -> None:

@classmethod
def entity(cls, name: str = None, property: str = None) -> Model | None:
"""Helper method to return the referenced entity type from the model"""
"""Helper method to return the referenced entity type from the model."""

if isinstance(name, str):
if name in cls._entities:
Expand All @@ -668,7 +668,7 @@ def entity(cls, name: str = None, property: str = None) -> Model | None:
# TODO: Should 'create' be a "private" method?
@classmethod
def create(cls, data: dict, property: str = None) -> Model:
"""Support creating a model entity from its data (dictionary) representation"""
"""Support creating a model entity from its data (dictionary) representation."""

if not isinstance(data, dict):
raise TypeError("The 'data' argument must have a dictionary value!")
Expand Down Expand Up @@ -705,6 +705,8 @@ def create(cls, data: dict, property: str = None) -> Model:

# TODO: Should 'load' be a "private" method?
def load(self, data: dict, model: Model) -> None:
"""Support loading data into the model entity from its dictionary representation."""

if not isinstance(data, dict):
raise ValueError("The 'data' argument must be provided as a dictionary!")

Expand Down Expand Up @@ -904,8 +906,11 @@ def __setattr__(self, name: str, value: object) -> None:
"The 'range' property can only contain valid type names or Model class types!"
)

if typed := self._find_type(range=range):
types += (typed,)
if not (typed := self._find_type(range=range)) is None:
if isinstance(typed, tuple):
types += tuple(typed)
else:
types += (typed,)
else:
raise ValueError(
"The '%s' range for the '%s' property cannot be reconciled to a known range type!"
Expand Down
6 changes: 5 additions & 1 deletion source/semanticpy/types/attributed.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from semanticpy.logging import logger
from typing import Iterator


class Attributed(object):
Expand Down Expand Up @@ -44,7 +45,7 @@ def __delattr__(self, key: str) -> None:
elif key in self._items:
del self._items[key]

def __iter__(self):
def __iter__(self) -> Iterator[tuple[str, object]]:
for key, value in self._items.items():
yield key, value

Expand All @@ -63,3 +64,6 @@ def get(self, key: object, default: object = None) -> object | None:
except AttributeError as exception:
logger.debug(str(exception))
return default

def clear(self) -> None:
self._items.clear()
Loading
Loading