From 60860ebbab1449b08db8bdcca24b32e61d597a4e Mon Sep 17 00:00:00 2001 From: Snowwpanda Date: Sun, 9 Nov 2025 23:30:51 +0100 Subject: [PATCH] Cleanup and adjustment to depreciated features Added Manifest for publishing --- .../FULL_EXAMPLE_EXPLANATION.md | 76 ---- .../python/lib-ro-crate-schema/MANIFEST.in | 31 ++ .../python/lib-ro-crate-schema/PUBLISHING.md | 2 +- .../PUBLISHING_CHECKLIST.md | 130 ------ .../python/lib-ro-crate-schema/QUICKSTART.md | 163 ------- .../lib/python/lib-ro-crate-schema/README.md | 31 +- .../python/lib-ro-crate-schema/TECHNICAL.md | 341 +++++++++++++++ .../lib-ro-crate-schema/architecture.puml | 110 ----- .../lib-ro-crate-schema/class_diagram.puml | 118 ------ .../examples/circular_import_test.py | 174 -------- .../examples/decorator_example.py | 185 -------- .../lib-ro-crate-schema/examples/examples.py | 135 ------ .../examples/export_import_pydantic_demo.py | 224 ---------- .../examples/full_example.py | 84 ++-- .../examples/minimal_import_example.py | 36 -- .../examples/minimal_pydantic_example.py | 53 +++ .../python/lib-ro-crate-schema/pyproject.toml | 10 +- .../lib-ro-crate-schema/run_all_tests.py | 77 ---- .../python/lib-ro-crate-schema/run_tests.py | 104 ----- .../src/lib_ro_crate_schema/__init__.py | 4 +- .../lib_ro_crate_schema/crate/decorators.py | 15 +- .../crate/schema_registry.py | 8 +- .../tests/test_context_detection.py | 138 ------ .../tests/test_decorator_id.py | 93 ---- .../tests/test_duplicate_detection.py | 0 .../tests/test_duplicate_integration.py | 0 .../lib-ro-crate-schema/tests/test_export.py | 138 ++++-- .../tests/test_get_crate.py | 76 ---- .../lib-ro-crate-schema/tests/test_import.py | 57 +++ .../tests/test_integration.py | 400 ------------------ .../tests/test_metadata_entry.py | 272 ------------ .../tests/test_published_package.py | 205 +++++++++ .../tests/test_pydantic_export.py | 209 --------- .../tests/test_restriction.py | 211 --------- .../tests/test_roundtrip.py | 397 ----------------- .../tests/test_schema_facade.py | 337 --------------- .../tests/test_standalone_elements.py | 129 ------ .../lib-ro-crate-schema/tests/test_type.py | 144 ------- .../tests/test_type_property.py | 187 -------- .../tests/test_unknown_namespaces.py | 247 ----------- 0.2.x/lib/python/lib-ro-crate-schema/uv.lock | 150 +++++-- 41 files changed, 993 insertions(+), 4508 deletions(-) delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/FULL_EXAMPLE_EXPLANATION.md create mode 100644 0.2.x/lib/python/lib-ro-crate-schema/MANIFEST.in delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/PUBLISHING_CHECKLIST.md delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/QUICKSTART.md create mode 100644 0.2.x/lib/python/lib-ro-crate-schema/TECHNICAL.md delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/architecture.puml delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/class_diagram.puml delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/examples/circular_import_test.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/examples/decorator_example.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/examples/examples.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/examples/export_import_pydantic_demo.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/examples/minimal_import_example.py create mode 100644 0.2.x/lib/python/lib-ro-crate-schema/examples/minimal_pydantic_example.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/run_all_tests.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/run_tests.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_context_detection.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_decorator_id.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_duplicate_detection.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_duplicate_integration.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_get_crate.py create mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_import.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_integration.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_metadata_entry.py create mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_published_package.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_pydantic_export.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_restriction.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_roundtrip.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_schema_facade.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_standalone_elements.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_type.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_type_property.py delete mode 100644 0.2.x/lib/python/lib-ro-crate-schema/tests/test_unknown_namespaces.py diff --git a/0.2.x/lib/python/lib-ro-crate-schema/FULL_EXAMPLE_EXPLANATION.md b/0.2.x/lib/python/lib-ro-crate-schema/FULL_EXAMPLE_EXPLANATION.md deleted file mode 100644 index 4486fdf..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/FULL_EXAMPLE_EXPLANATION.md +++ /dev/null @@ -1,76 +0,0 @@ -# 🧪 RO-Crate Full Example Guide - -**File:** `examples/full_example.py` - -Comprehensive example demonstrating advanced RO-Crate features: chemical synthesis workflow, circular relationships, SHACL validation, and dynamic updates. - -## 📊 **Data Model** - -#### **OpenBIS Entities** (`http://openbis.org/`) - -| Entity | Properties | Relationships | -|--------|------------|---------------| -| **Project** | code, name, description, created_date | → space | -| **Space** | name, description, created_date | → collections[] | -| **Collection** | name, sample_type, storage_conditions, created_date | _(leaf node)_ | -| **Equipment** | name, model, serial_number, created_date, configuration{} | → parent_equipment | - -#### **Schema.org Entities** (`https://schema.org/`) - -| Entity | Properties | Relationships | -|--------|------------|---------------| -| **Molecule** | name, **smiles**, molecular_weight, cas_number, created_date, experimental_notes | → contains_molecules[] | -| **Person** | name, orcid, email | → affiliation | -| **Organization** | name, country, website | _(referenced by Person)_ | -| **Publication** | title, doi, publication_date | → authors[], molecules[], equipment[], organization | - -## ⚡ **Workflow: Setup → Experiment → Export** - -**Created Entities:** -- 1 Project, 1 Space, 1 Collection, 2 Equipment (nested) -- 5 Molecules, 2 People, 1 Organization, 1 Publication - -**Key Features:** -- ✅ **Circular Relationships**: Person ↔ Person colleagues (auto-resolved) -- ✅ **Mixed Namespaces**: OpenBIS + schema.org with auto-context -- ✅ **SHACL Validation**: 100% compliance with 150+ rules -- ✅ **Dynamic Updates**: Experiment modifies molecules + adds new product - -## 🔧 **Key Technical Features** - -### **1. Circular Relationship Resolution** -```python -# Automatic resolution of Person ↔ Person colleagues -sarah = Person(colleagues=[marcus]) -marcus = Person(colleagues=[sarah]) -# → SchemaFacade.resolve_placeholders() merges duplicates -``` - -### **2. Chemical Data with SMILES** -- Benzene: `c1ccccc1` → Toluene: `Cc1ccccc1` → Product: `(c1ccccc1).(Cc1ccccc1)` - -### **3. Scale Metrics** -- **Entities**: 15 → 16 (after synthesis) -- **RDF Triples**: ~500 → ~530 -- **SHACL Validation**: 100% compliance - - -## � **Usage** - -```bash -PYTHONPATH=./src python examples/full_example.py -``` - -**Output:** -Initial Crate: `full_example_initial/` -Final Crate: `full_example_final/` including file [experimental_observations](examples/experimental_observations.csv) - -## ✅ **Testing** - -```bash -python -m pytest tests/ -v # Full suite (85 tests) -``` - ---- - -**Production-ready RO-Crate library with automatic relationship resolution, comprehensive validation, and modern architecture.** \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/MANIFEST.in b/0.2.x/lib/python/lib-ro-crate-schema/MANIFEST.in new file mode 100644 index 0000000..7dc5fb2 --- /dev/null +++ b/0.2.x/lib/python/lib-ro-crate-schema/MANIFEST.in @@ -0,0 +1,31 @@ +include README.md +include LICENSE +include PUBLISHING.md +include TECHNICAL.md +include pyproject.toml + +# Include examples (only the 4 main examples) +include examples/python_quickstart_write.py +include examples/python_quickstart_read.py +include examples/minimal_pydantic_example.py +include examples/full_example.py + +# Include tests +recursive-include tests *.py +recursive-include tests *.shacl + +# Exclude compiled Python files +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__ + +# Exclude build artifacts +global-exclude *.so +global-exclude *.dylib +global-exclude .DS_Store + +# Exclude output directories +prune output_crates +prune .venv +prune build +prune dist diff --git a/0.2.x/lib/python/lib-ro-crate-schema/PUBLISHING.md b/0.2.x/lib/python/lib-ro-crate-schema/PUBLISHING.md index e3a70cc..7212542 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/PUBLISHING.md +++ b/0.2.x/lib/python/lib-ro-crate-schema/PUBLISHING.md @@ -234,6 +234,6 @@ python -m twine upload --repository testpypi dist/* python -m twine upload dist/* # Test installation -pip install --index-url https://test.pypi.org/simple/ lib-ro-crate-schema # Test PyPI +pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ lib-ro-crate-schema # Test PyPI pip install lib-ro-crate-schema # Production PyPI ``` diff --git a/0.2.x/lib/python/lib-ro-crate-schema/PUBLISHING_CHECKLIST.md b/0.2.x/lib/python/lib-ro-crate-schema/PUBLISHING_CHECKLIST.md deleted file mode 100644 index 225ea1c..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/PUBLISHING_CHECKLIST.md +++ /dev/null @@ -1,130 +0,0 @@ -# PyPI Publishing Checklist - -Use this checklist when you're ready to publish to PyPI. - -## Pre-Publishing ✅ - -- [ ] All tests pass: `python run_all_tests.py` -- [ ] Examples run successfully: `python examples/decorator_example.py` -- [ ] Version updated in: - - [ ] `pyproject.toml` (line 3: `version = "X.Y.Z"`) - - [ ] `src/lib_ro_crate_schema/__init__.py` (line 25: `__version__ = "X.Y.Z"`) - - [ ] `src/lib_ro_crate_schema/crate/__init__.py` (line 4: `__version__ = "X.Y.Z"`) -- [ ] Changes committed to git -- [ ] Git tag created: `git tag -a vX.Y.Z -m "Release vX.Y.Z"` - -## Build Package ✅ - -```bash -cd "c:\git\eln_interoperability\ro-crate-interoperability-profile\0.2.x\lib\python\lib-ro-crate-schema" - -# Clean old builds -Remove-Item -Recurse -Force dist, build -ErrorAction SilentlyContinue - -# Build -python -m build - -# Verify -python -m twine check dist/* -``` - -- [ ] Build completed without errors -- [ ] Twine check passed - -## Test on Test PyPI ✅ - -```bash -# Upload to Test PyPI -python -m twine upload --repository testpypi dist/* -``` - -When prompted: -- Username: `__token__` -- Password: `[Your Test PyPI API token]` - -- [ ] Uploaded successfully -- [ ] Check the page: https://test.pypi.org/project/lib-ro-crate-schema/ - -### Test Installation - -```bash -# Create test environment -python -m venv test_env -test_env\Scripts\activate - -# Install from Test PyPI -pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ lib-ro-crate-schema - -# Test import -python -c "from lib_ro_crate_schema import SchemaFacade, ro_crate_schema; print('✅ Import successful!')" -``` - -- [ ] Installed without errors -- [ ] Import works correctly -- [ ] Basic functionality works - -## Publish to PyPI ✅ - -**⚠️ This cannot be undone! Once published, you cannot upload the same version again.** - -```bash -# Upload to production PyPI -python -m twine upload dist/* -``` - -When prompted: -- Username: `__token__` -- Password: `[Your PyPI API token]` - -- [ ] Uploaded successfully -- [ ] Check the page: https://pypi.org/project/lib-ro-crate-schema/ -- [ ] README renders correctly on PyPI - -### Verify Installation - -```bash -# Fresh environment -python -m venv verify_env -verify_env\Scripts\activate - -# Install from PyPI -pip install lib-ro-crate-schema - -# Test -python -c "from lib_ro_crate_schema import SchemaFacade; print('✅ Success!')" -``` - -- [ ] Installed from PyPI successfully -- [ ] All imports work - -## Post-Publishing ✅ - -- [ ] Push git tag: `git push origin vX.Y.Z` -- [ ] Create GitHub release from tag -- [ ] Add release notes to GitHub release -- [ ] Update CHANGELOG (if you have one) -- [ ] Announce release (if applicable) - -## Quick Commands - -```bash -# All in one - Test PyPI -python -m build && python -m twine check dist/* && python -m twine upload --repository testpypi dist/* - -# All in one - Production PyPI -python -m build && python -m twine check dist/* && python -m twine upload dist/* -``` - -## Need Help? - -- **Test PyPI**: https://test.pypi.org/account/register/ -- **PyPI**: https://pypi.org/account/register/ -- **Full Guide**: See [PUBLISHING.md](PUBLISHING.md) -- **Package Docs**: See [README.md](README.md) - ---- - -**Remember**: -- Always test on Test PyPI first! -- You cannot reupload the same version to PyPI -- Keep your API tokens secure diff --git a/0.2.x/lib/python/lib-ro-crate-schema/QUICKSTART.md b/0.2.x/lib/python/lib-ro-crate-schema/QUICKSTART.md deleted file mode 100644 index f808b4f..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/QUICKSTART.md +++ /dev/null @@ -1,163 +0,0 @@ -# Quick Start Guide - -Get started with `lib-ro-crate-schema` in 5 minutes! - -## Installation - -```bash -pip install lib-ro-crate-schema -``` - -## Your First RO-Crate - -Create a file called `my_first_crate.py`: - -```python -from lib_ro_crate_schema import SchemaFacade, ro_crate_schema, Field -from pydantic import BaseModel - -# 1. Define your data model with decorators -@ro_crate_schema(ontology="https://schema.org/Person") -class Person(BaseModel): - name: str = Field(ontology="https://schema.org/name") - email: str = Field(ontology="https://schema.org/email") - -# 2. Create some data -alice = Person(name="Alice Smith", email="alice@example.com") - -# 3. Create and export an RO-Crate -facade = SchemaFacade() -facade.add_all_registered_models() # Register your models -facade.add_model_instance(alice, "person_001") # Add data -facade.write("my_first_crate") # Export! - -print("✅ RO-Crate created in ./my_first_crate/") -``` - -Run it: -```bash -python my_first_crate.py -``` - -This creates a folder `my_first_crate/` containing: -- `ro-crate-metadata.json` - Your data and schema in JSON-LD format -- Proper RDF/OWL type definitions -- Schema.org vocabulary mappings - -## Next Steps - -### Add Files to Your Crate - -```python -# Add a data file before writing -facade.add_file("data.csv", - name="Experimental Data", - description="Raw measurements") -facade.write("my_crate") -``` - -### Define Related Objects - -```python -@ro_crate_schema(ontology="https://schema.org/Organization") -class Organization(BaseModel): - name: str = Field(ontology="https://schema.org/name") - -@ro_crate_schema(ontology="https://schema.org/Person") -class Person(BaseModel): - name: str = Field(ontology="https://schema.org/name") - affiliation: Organization = Field(ontology="https://schema.org/affiliation") - -# Create related objects -mit = Organization(name="MIT") -alice = Person(name="Alice", affiliation=mit) - -# Export both -facade = SchemaFacade() -facade.add_all_registered_models() -facade.add_model_instance(mit, "org_001") -facade.add_model_instance(alice, "person_001") -facade.write("my_crate") -``` - -### Import and Modify Existing Crates - -```python -from lib_ro_crate_schema import SchemaFacade - -# Load existing crate -facade = SchemaFacade.from_ro_crate("existing_crate") - -# Modify it -# (add more instances, files, etc.) - -# Export modified version -facade.write("modified_crate") -``` - -## What Just Happened? - -When you use `@ro_crate_schema`: -1. Your Pydantic model is registered as an RO-Crate type -2. Field annotations map to ontology properties (like Schema.org) -3. The library generates proper RDF/OWL definitions -4. Your data is packaged following the RO-Crate specification - -## More Examples - -Check out the `examples/` directory: -- `decorator_example.py` - More complex schemas -- `full_example.py` - Scientific workflow with files -- `minimal_import_example.py` - Working with existing crates - -## Common Patterns - -### Optional Fields -```python -from typing import Optional - -@ro_crate_schema(ontology="https://schema.org/Person") -class Person(BaseModel): - name: str = Field(ontology="https://schema.org/name") - email: Optional[str] = Field(default=None, ontology="https://schema.org/email") -``` - -### Lists -```python -from typing import List - -@ro_crate_schema(ontology="https://schema.org/Dataset") -class Dataset(BaseModel): - name: str = Field(ontology="https://schema.org/name") - authors: List[Person] = Field(ontology="https://schema.org/author") -``` - -### Dates and Times -```python -from datetime import datetime - -@ro_crate_schema(ontology="https://schema.org/Event") -class Event(BaseModel): - name: str = Field(ontology="https://schema.org/name") - date: datetime = Field(ontology="https://schema.org/startDate") -``` - -## Need Help? - -- **Full Documentation**: See [README.md](README.md) -- **API Reference**: Browse the [src/lib_ro_crate_schema/](src/lib_ro_crate_schema/) directory -- **Examples**: Check [examples/](examples/) for real-world usage -- **Issues**: Report bugs at [GitHub Issues](https://github.com/Snowwpanda/ro-crate-interoperability-profile/issues) - -## Understanding RO-Crate - -RO-Crate (Research Object Crate) is a standard for packaging research data with metadata. It uses: -- **JSON-LD**: Linked data format -- **Schema.org**: Standard vocabulary for describing things -- **RDF/OWL**: Semantic web technologies - -This library makes it easy to create RO-Crates from Python without needing to understand all these technologies! - ---- - -**Ready for more?** Check out the full [README.md](README.md) for advanced usage and API details. diff --git a/0.2.x/lib/python/lib-ro-crate-schema/README.md b/0.2.x/lib/python/lib-ro-crate-schema/README.md index df0bd54..e43a63f 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/README.md +++ b/0.2.x/lib/python/lib-ro-crate-schema/README.md @@ -22,14 +22,20 @@ pip install lib-ro-crate-schema ### From Source ```bash -git clone https://github.com/Snowwpanda/ro-crate-interoperability-profile.git +git clone https://github.com/researchobjectschema/ro-crate-interoperability-profile.git cd ro-crate-interoperability-profile/0.2.x/lib/python/lib-ro-crate-schema pip install -e . ``` ## Quick Start -Here's a minimal example to get you started: +For complete working examples, see the `examples/` directory: +- **[python_quickstart_write.py](examples/python_quickstart_write.py)** - Create and export an RO-Crate +- **[python_quickstart_read.py](examples/python_quickstart_read.py)** - Import and read an RO-Crate +- **[minimal_pydantic_example.py](examples/minimal_pydantic_example.py)** - Minimal Pydantic usage +- **[full_example.py](examples/full_example.py)** - Complete export/import/modify workflow + +Here's a minimal example of creating an RO-Crate with schema definitions: ```python from lib_ro_crate_schema import SchemaFacade, ro_crate_schema, Field @@ -38,8 +44,8 @@ from pydantic import BaseModel # Define your schema using decorators @ro_crate_schema(ontology="https://schema.org/Person") class Person(BaseModel): - name: str = Field(ontology="https://schema.org/name") - email: str = Field(ontology="https://schema.org/email") + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + email: str = Field(json_schema_extra={"ontology": "https://schema.org/email"}) # Create an instance person = Person(name="Dr. Alice Smith", email="alice@example.com") @@ -80,12 +86,12 @@ facade.write("my_crate") ```python @ro_crate_schema(ontology="https://schema.org/Organization") class Organization(BaseModel): - name: str = Field(ontology="https://schema.org/name") + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) @ro_crate_schema(ontology="https://schema.org/Person") class Person(BaseModel): - name: str = Field(ontology="https://schema.org/name") - affiliation: Organization = Field(ontology="https://schema.org/affiliation") + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + affiliation: Organization = Field(json_schema_extra={"ontology": "https://schema.org/affiliation"}) org = Organization(name="MIT") person = Person(name="Alice", affiliation=org) @@ -234,13 +240,20 @@ If you use this library in your research, please cite: title = {RO-Crate Schema Library}, author = {Baffelli, Simone and Su, Pascal}, year = {2025}, - url = {https://github.com/Snowwpanda/ro-crate-interoperability-profile} + url = {https://github.com/researchobjectschema/ro-crate-interoperability-profile} } ``` +## Documentation + +- **[README.md](README.md)** - Installation and quick start +- **[TECHNICAL.md](TECHNICAL.md)** - Architecture, components, and API reference +- **[PUBLISHING.md](PUBLISHING.md)** - Guide for publishing to PyPI +- **[examples/](examples/)** - Working code examples + ## Links -- **Repository**: https://github.com/Snowwpanda/ro-crate-interoperability-profile +- **Repository**: https://github.com/researchobjectschema/ro-crate-interoperability-profile - **RO-Crate Specification**: https://www.researchobject.org/ro-crate/ - **Pydantic Documentation**: https://docs.pydantic.dev/ diff --git a/0.2.x/lib/python/lib-ro-crate-schema/TECHNICAL.md b/0.2.x/lib/python/lib-ro-crate-schema/TECHNICAL.md new file mode 100644 index 0000000..df2477c --- /dev/null +++ b/0.2.x/lib/python/lib-ro-crate-schema/TECHNICAL.md @@ -0,0 +1,341 @@ +# Technical Documentation + +## Architecture Overview + +`lib-ro-crate-schema` is a Python library that bridges Pydantic models with RO-Crate metadata standards. It provides a decorator-based approach to create semantically annotated research objects. + +## Package Structure + +``` +lib_ro_crate_schema/ +├── crate/ +│ ├── decorators.py # Schema decorators for Pydantic models +│ ├── schema_facade.py # Main API for crate management +│ ├── schema_registry.py # Global registry for type templates +│ ├── type.py # Type definition and management +│ ├── type_property.py # Property type definitions +│ ├── property_type.py # Property metadata +│ ├── metadata_entry.py # Metadata entry handling +│ ├── jsonld_utils.py # JSON-LD conversion utilities +│ ├── rdf.py # RDF graph generation +│ ├── restriction.py # OWL restriction handling +│ ├── forward_ref_resolver.py # Forward reference resolution +│ └── literal_type.py # Literal type handling +└── check.py # Validation utilities +``` + +## Core Components + +### 1. Decorators (`decorators.py`) + +**Purpose**: Provide a clean decorator-based API for annotating Pydantic models with ontology information. + +**Key Functions**: +- `@ro_crate_schema(ontology: str)`: Class decorator that registers a Pydantic model as an RO-Crate schema type +- `Field(json_schema_extra: dict, ...)`: Pydantic Field with RO-Crate metadata in json_schema_extra + +**Flow**: +1. Decorator captures class definition +2. Extracts field annotations and ontology mappings from json_schema_extra +3. Creates `TypeTemplate` and registers in `SchemaRegistry` +4. Returns original class unchanged + +**Example** (Pydantic 2.x compatible): +```python +@ro_crate_schema(ontology="https://schema.org/Person") +class Person(BaseModel): + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + email: str = Field(json_schema_extra={"ontology": "https://schema.org/email"}) +``` + + +### 2. Schema Registry (`schema_registry.py`) + +**Purpose**: Global singleton that maintains a registry of all decorated types and their ontological mappings. + +**Key Classes**: +- `SchemaRegistry`: Singleton registry pattern +- `TypeTemplate`: Template containing type information and property mappings + +**Key Methods**: +- `register_type(model, ontology)`: Register a new type template +- `get_type_template(model_name)`: Retrieve registered type +- `get_all_type_templates()`: Get all registered types + +**Flow**: +``` +Decorator → TypeTemplate Creation → Registry Storage → Facade Retrieval +``` + +### 3. Schema Facade (`schema_facade.py`) + +**Purpose**: Main API for creating and managing RO-Crates. Provides high-level operations for schema and metadata management. + +**Key Classes**: +- `SchemaFacade`: Primary interface for RO-Crate operations + +**Key Methods**: +- `add_all_registered_models()`: Add all decorator-registered models to schema +- `add_model_instance(instance, id)`: Add a Pydantic instance as metadata +- `to_graph()`: Convert schema + metadata to RDF graph +- `from_ro_crate(path)`: Import existing RO-Crate +- `export(path)`: Export to directory (deprecated, use jsonld_utils) + +**Internal State**: +- `types`: List of `Type` objects (schema definitions) +- `type_properties`: List of `TypeProperty` objects (property definitions) +- `metadata_entries`: List of `MetadataEntry` objects (actual data) + +**Flow**: +``` +SchemaFacade.add_all_registered_models() + ↓ +Retrieve from SchemaRegistry + ↓ +Convert TypeTemplate → Type + TypeProperty + ↓ +SchemaFacade.add_model_instance(person) + ↓ +Convert Pydantic instance → MetadataEntry + ↓ +SchemaFacade.to_graph() + ↓ +Generate RDF triples +``` + +### 4. Type System (`type.py`, `type_property.py`, `property_type.py`) + +**Type (`type.py`)**: +- Represents an RDFS Class (e.g., `schema:Person`) +- Contains type properties and restrictions +- Generates RDF class definitions + +**TypeProperty (`type_property.py`)**: +- Represents an RDF Property definition +- Links a property to its domain (type) and range (value type) +- Handles cardinality restrictions (min/max) + +**PropertyType (`property_type.py`)**: +- Simple data class for property metadata +- Stores property name and RDF type + +**Relationships**: +``` +Type (Person) + └── has TypeProperty (name) + └── has PropertyType (string) + └── has Restriction (minCardinality: 1) +``` + +### 5. Metadata Entry (`metadata_entry.py`) + +**Purpose**: Represents an actual instance of data (not the schema). + +**Key Attributes**: +- `id`: Unique identifier +- `class_id`: Reference to Type +- `properties`: Dict of property values +- `references`: List of referenced entities + +**Flow**: +``` +Pydantic Instance (person = Person(...)) + ↓ +extract_from_pydantic() + ↓ +MetadataEntry + ↓ +to_triples() + ↓ +RDF Graph +``` + +### 6. JSON-LD Utilities (`jsonld_utils.py`) + +**Purpose**: Convert between RO-Crate (JSON-LD) and internal representations. + +**Key Functions**: +- `add_schema_to_crate(facade, crate)`: Merge schema/metadata into ROCrate object +- Handles context management +- Converts RDF graph to JSON-LD @graph array + +**Flow**: +``` +SchemaFacade → RDF Graph → JSON-LD @graph → ROCrate.write() +``` + +### 7. RDF Generation (`rdf.py`) + +**Purpose**: Convert schema and metadata to RDF triples. + +**Key Functions**: +- `to_graph()`: Main conversion function +- Creates RDF graph with proper namespaces +- Handles OWL restrictions, class hierarchies, property definitions + +**Output**: RDFLib Graph object with schema + data triples + +### 8. Forward Reference Resolution (`forward_ref_resolver.py`) + +**Purpose**: Handle Pydantic forward references (e.g., `creator: "Person"` when Person is defined later). + +**Key Classes**: +- `ForwardRefResolver`: Resolves string type hints to actual classes + +**When Used**: During TypeTemplate creation when processing nested models + +### 9. Restriction (`restriction.py`) + +**Purpose**: Model OWL restrictions (cardinality constraints). + +**Key Classes**: +- `Restriction`: Represents OWL constraints on properties +- `RestrictionType`: Enum of restriction types (min/max cardinality) + +**Flow**: +``` +Pydantic Field (required vs Optional) + ↓ +Determine cardinality + ↓ +Create Restriction object + ↓ +Generate OWL:Restriction triples +``` + +## Data Flow + +### Complete Export Flow + +``` +1. Model Definition + @ro_crate_schema(...) + class Person(BaseModel): ... + ↓ +2. Registration + TypeTemplate → SchemaRegistry + ↓ +3. Facade Creation + facade = SchemaFacade() + facade.add_all_registered_models() + ↓ +4. Add Instances + person = Person(...) + facade.add_model_instance(person, "alice") + ↓ +5. RDF Generation + graph = facade.to_graph() + ↓ +6. JSON-LD Conversion + crate = ROCrate() + final_crate = add_schema_to_crate(facade, crate) + ↓ +7. Export + final_crate.write(path) +``` + +### Complete Import Flow + +``` +1. Read RO-Crate + path = "crate_directory/" + ↓ +2. Parse JSON-LD + ROCrate.read(path) + ↓ +3. Import to Facade + facade = SchemaFacade.from_ro_crate(path) + ↓ +4. Extract Schema + facade.types → Type objects + facade.type_properties → TypeProperty objects + ↓ +5. Extract Metadata + facade.metadata_entries → MetadataEntry objects + ↓ +6. Query/Modify + Access entities via facade +``` + +## Key Design Patterns + +### 1. Decorator Pattern +- Non-intrusive model annotation +- Preserves Pydantic functionality +- Automatic registration + +### 2. Facade Pattern +- `SchemaFacade` provides simplified interface +- Hides internal complexity of RDF/OWL generation +- Single entry point for operations + +### 3. Registry Pattern +- `SchemaRegistry` maintains global type catalog +- Singleton ensures consistency +- Enables decoupled type lookup + +### 4. Builder Pattern +- Incremental construction of RO-Crates +- Add types, properties, metadata step-by-step +- Flexible composition + +## Extension Points + +### Adding Custom Types + +```python +@ro_crate_schema(ontology="https://example.org/CustomType") +class CustomType(BaseModel): + custom_field: str = Field(json_schema_extra={ontology="https://example.org/customField"}) +``` + +### Custom Property Types + +Extend `PropertyType` for specialized property handling. + +### Custom Restrictions + +Extend `Restriction` class for additional OWL constraints. + +## Performance Considerations + +- **Registry Lookups**: O(1) hash-based lookups +- **RDF Generation**: Linear in number of entities + properties +- **Memory**: Stores full graph in memory (consider streaming for large crates) + +## Dependencies + +- **pydantic**: Model definition and validation +- **rdflib**: RDF graph manipulation +- **rocrate**: RO-Crate standard implementation +- **pyld**: JSON-LD processing +- **pyshacl**: SHACL validation + +## Testing Strategy + +- **Unit Tests**: Test individual components (Type, TypeProperty, etc.) +- **Integration Tests**: Test full export/import cycles +- **Round-trip Tests**: Ensure export → import → export produces identical results +- **Published Package Tests**: Verify installability from PyPI + +## Common Pitfalls + +1. **Forgetting to call `add_all_registered_models()`**: Models won't appear in schema +2. **Circular references**: Use forward references carefully +3. **ID conflicts**: Ensure unique IDs when adding instances +4. **Context mixing**: RO-Crate context vs custom contexts + +## Debugging Tips + +- Use `facade.to_graph()` to inspect RDF triples +- Check `SchemaRegistry.get_all_type_templates()` to see registered types +- Validate JSON-LD output with online validators +- Use `pyshacl` for SHACL validation + +## Future Enhancements + +- Streaming support for large datasets +- SHACL shape generation from Pydantic models +- Query API for metadata +- Incremental updates to existing crates +- Better circular reference handling diff --git a/0.2.x/lib/python/lib-ro-crate-schema/architecture.puml b/0.2.x/lib/python/lib-ro-crate-schema/architecture.puml deleted file mode 100644 index f56e8d5..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/architecture.puml +++ /dev/null @@ -1,110 +0,0 @@ -@startuml RO-Crate Architecture - -!theme plain -skinparam backgroundColor white -skinparam componentStyle rectangle - -package "Input Sources" as inputs { - [SHACL Schema\nConstraints] as shacl - [Pydantic Models\n@ro_crate_schema] as pymod - [Manual Schema\nDefinition] as manual - [Existing RO-Crate\nMetadata] as rocin -} - -package "External Dependencies" as external { - [RDFLib\nRDF Graph Processing] as rdflib - [RO-Crate\nPython Library] as rocrate - [Pydantic\nData Validation] as pydantic - [JSON-LD\nLinked Data] as jsonld -} - -package "Core Library Components" as core { - - package "Schema Facade (Orchestrator)" as orchestrator { - [SchemaFacade\nMain API Controller] as sf - } - - package "Schema Components" as components { - [Type\nRDFS Classes] as type - [TypeProperty\nRDFS Properties] as prop - [MetadataEntry\nRDF Instances] as meta - [Restriction\nConstraints] as rest - } - - package "Registry & Discovery" as registry { - [SchemaRegistry\nDecorator System] as reg - [ForwardRefResolver\nReference Linking] as frr - } - - package "JSON-LD Processing" as jsonld_proc { - [JSONLDUtils\nContext Generation] as jsonldutils - [Dynamic Context\nNamespace Detection] as ctx - } - - package "RDF Processing" as rdf_proc { - [RDF Module\nTriple Generation] as rdfp - [RDF Graph\nConversion] as graph - } -} - -package "API Interfaces" as apis { - [Python API\nadd_type(), get_entries()] as pyapi - [Java API Compatibility\naddType(), getEntries()] as japi - [Decorator API\n@ro_crate_schema] as decapi -} - -package "Output Formats" as outputs { - [RO-Crate\nJSON-LD Files] as rocout - [RDF/Turtle\nSerialization] as ttlout - [Pure JSON-LD\nSchema Export] as jsonout - [Data Files\nAttachment] as fileout -} - -package "Examples & Usage" as usage { - [Examples\nfull_example.py\nquickstart.py] as examples - [Test Suite\npytest Framework\n83 Tests] as tests -} - -' Data Flow Connections -shacl --> sf -pymod --> reg -manual --> sf -rocin --> sf - -reg --> sf -sf --> type -sf --> prop -sf --> meta -sf --> rest - -type --> rdfp -prop --> rdfp -meta --> rdfp -rest --> rdfp - -rdfp --> graph -graph --> jsonldutils -jsonldutils --> ctx - -frr --> sf -sf --> pyapi -sf --> japi -reg --> decapi - -sf --> rocout -graph --> ttlout -jsonldutils --> jsonout -sf --> fileout - -pyapi --> examples -japi --> examples -decapi --> examples -sf --> tests - -' External Dependencies -rdflib --> graph -rocrate --> sf -pydantic --> reg -jsonld --> jsonldutils - -@enduml \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/class_diagram.puml b/0.2.x/lib/python/lib-ro-crate-schema/class_diagram.puml deleted file mode 100644 index 7cc7f45..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/class_diagram.puml +++ /dev/null @@ -1,118 +0,0 @@ -@startuml RO-Crate Core Classes - -!theme plain -skinparam class { - BackgroundColor White - BorderColor Black - ArrowColor Black -} - -package "Core Schema Objects" { - - class SchemaFacade { - +types: List[Type] - +metadata_entries: List[MetadataEntry] - +standalone_properties: List[TypeProperty] - +standalone_restrictions: List[Restriction] - +prefix: str - -- - +addType(type: Type) - +addEntry(entry: MetadataEntry) - +add_property_type(prop: TypeProperty) - +get_crate(): ROCrate - +from_ro_crate(path): SchemaFacade - +write(destination: str) - +to_json(): dict - } - - class Type { - +id: str - +rdfs_property: List[TypeProperty] - +restrictions: List[Restriction] - +label: str - +comment: str - +sub_class_of: List[ForwardRef] - -- - +to_triples(): Generator[Triple] - } - - class TypeProperty { - +id: str - +range_includes: List[LiteralType] - +domain_includes: List[str] - +required: bool - +label: str - +comment: str - -- - +to_triples(): Generator[Triple] - } - - class MetadataEntry { - +id: str - +class_id: str - +properties: Dict[str, Any] - +label: str - +comment: str - -- - +to_triples(): Generator[Triple] - } - - class Restriction { - +id: str - +target_class: str - +target_property: str - +restriction_type: RestrictionType - +value: Any - -- - +to_triples(): Generator[Triple] - } -} - -package "Registry System" { - class SchemaRegistry { - +registered_models: Dict[str, TypeTemplate] - -- - +register_model(name: str, template: TypeTemplate) - +get_model(name: str): TypeTemplate - +list_models(): List[str] - } - - class TypeTemplate { - +name: str - +properties: List[TypePropertyTemplate] - +base_classes: List[str] - -- - +to_type(): Type - } -} - -package "Processing Utilities" { - class JSONLDUtils { - -- - +get_context(graph: Graph): List - +add_schema_to_crate(facade: SchemaFacade, crate: ROCrate): ROCrate - } - - class ForwardRefResolver { - -- - +resolve_ref(ref: Union[ForwardRef, str]): Any - } -} - -' Relationships -SchemaFacade ||--o{ Type : contains -SchemaFacade ||--o{ MetadataEntry : contains -SchemaFacade ||--o{ TypeProperty : "standalone properties" -SchemaFacade ||--o{ Restriction : "standalone restrictions" - -Type ||--o{ TypeProperty : defines -Type ||--o{ Restriction : constraints - -SchemaRegistry ||--o{ TypeTemplate : manages -TypeTemplate --> Type : generates - -SchemaFacade --> JSONLDUtils : uses -SchemaFacade --> ForwardRefResolver : uses -SchemaFacade --> SchemaRegistry : accesses - -@enduml \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/examples/circular_import_test.py b/0.2.x/lib/python/lib-ro-crate-schema/examples/circular_import_test.py deleted file mode 100644 index f468ec0..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/examples/circular_import_test.py +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env python3 -""" -Focused test for circular import handling in RO-Crate schema. - -This test specifically creates two people who are each other's colleagues -to verify how the system handles circular references during: -1. Schema creation -2. RDF serialization -3. JSON-LD export -4. Round-trip import/export -""" - -import sys -import json -from pathlib import Path -from typing import List, Optional - -# Add src to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from pydantic import BaseModel -from lib_ro_crate_schema.crate.decorators import ro_crate_schema, Field -from lib_ro_crate_schema.crate.schema_facade import SchemaFacade - -@ro_crate_schema(ontology="https://schema.org/Organization") -class SimpleOrganization(BaseModel): - """Simple organization for testing""" - name: str = Field(ontology="https://schema.org/name") - country: str = Field(ontology="https://schema.org/addressCountry") - -@ro_crate_schema(ontology="https://schema.org/Person") -class SimplePerson(BaseModel): - """Person with circular colleague relationship""" - name: str = Field(ontology="https://schema.org/name") - email: str = Field(ontology="https://schema.org/email") - affiliation: SimpleOrganization = Field(ontology="https://schema.org/affiliation") - colleagues: List['SimplePerson'] = Field(default=[], ontology="https://schema.org/colleague") - -def test_circular_imports(): - """Test circular colleague relationships""" - - print("🧪 CIRCULAR IMPORT TEST") - print("=" * 50) - - # Create organization - org = SimpleOrganization( - name="Test University", - country="Switzerland" - ) - - # Create two people without colleagues initially - alice = SimplePerson( - name="Dr. Alice Johnson", - email="alice@test.edu", - affiliation=org, - colleagues=[] - ) - - bob = SimplePerson( - name="Prof. Bob Smith", - email="bob@test.edu", - affiliation=org, - colleagues=[] - ) - - print(f"✅ Created Alice (colleagues: {len(alice.colleagues)})") - print(f"✅ Created Bob (colleagues: {len(bob.colleagues)})") - - # Establish circular colleague relationship - alice = alice.model_copy(update={'colleagues': [bob]}) - bob = bob.model_copy(update={'colleagues': [alice]}) - - print(f"\n🔄 Circular relationships established:") - print(f" Alice colleagues: {[c.name for c in alice.colleagues]}") - print(f" Bob colleagues: {[c.name for c in bob.colleagues]}") - - # Test schema creation with circular refs - print(f"\n📊 Testing schema creation...") - facade = SchemaFacade() - facade.add_all_registered_models() - - print(f" ✅ Schema created with {len(facade.types)} types") - - # Add instances to facade - facade.add_model_instance(org, "test_org") - facade.add_model_instance(alice, "alice") - facade.add_model_instance(bob, "bob") - - print(f" ✅ Added {len(facade.metadata_entries)} instances to facade") - - # Test RDF generation - print(f"\n🕸️ Testing RDF generation...") - try: - graph = facade.to_graph() - print(f" ✅ Generated {len(graph)} RDF triples successfully") - except Exception as e: - print(f" ❌ RDF generation failed: {e}") - return False - - # Test JSON-LD export - print(f"\n📄 Testing RO-Crate export...") - try: - import os - output_dir = "output_crates" - os.makedirs(output_dir, exist_ok=True) - output_path = os.path.join(output_dir, "circular_test") - - facade.write(output_path, name="Circular Import Test", - description="Testing circular colleague relationships") - print(f" ✅ Exported to {output_path}") - except Exception as e: - print(f" ❌ Export failed: {e}") - return False - - # Test round-trip import - print(f"\n🔄 Testing round-trip import...") - try: - imported_facade = SchemaFacade.from_ro_crate(output_path) - print(f" ✅ Imported {len(imported_facade.types)} types, {len(imported_facade.metadata_entries)} entries") - - # Check if circular references are preserved - alice_entry = None - bob_entry = None - - for entry in imported_facade.metadata_entries: - if entry.id == "alice": - alice_entry = entry - elif entry.id == "bob": - bob_entry = entry - - if alice_entry and bob_entry: - print(f" ✅ Found Alice and Bob entries after import") - - # Check if colleague relationships survived - alice_colleagues = alice_entry.properties.get('colleagues', []) - bob_colleagues = bob_entry.properties.get('colleagues', []) - - print(f" Alice colleagues in imported data: {alice_colleagues}") - print(f" Bob colleagues in imported data: {bob_colleagues}") - else: - print(f" ⚠️ Could not find Alice/Bob entries after import") - - except Exception as e: - print(f" ❌ Import failed: {e}") - return False - - # Examine the actual JSON-LD structure - print(f"\n🔍 Examining generated JSON-LD structure...") - try: - with open(f"{output_path}/ro-crate-metadata.json", 'r') as f: - crate_data = json.load(f) - - # Find Person entities - person_entities = [] - for entity in crate_data.get("@graph", []): - if entity.get("@type") == "SimplePerson": - person_entities.append(entity) - - print(f" Found {len(person_entities)} Person entities:") - for person in person_entities: - person_id = person.get("@id", "unknown") - person_name = person.get("base:name", "unknown") - colleagues = person.get("base:colleagues", "none") - print(f" - {person_id}: {person_name}") - print(f" Colleagues: {colleagues}") - - except Exception as e: - print(f" ⚠️ Could not examine JSON-LD: {e}") - - print(f"\n🎉 Circular import test completed!") - return True - -if __name__ == "__main__": - test_circular_imports() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/examples/decorator_example.py b/0.2.x/lib/python/lib-ro-crate-schema/examples/decorator_example.py deleted file mode 100644 index 837f0d2..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/examples/decorator_example.py +++ /dev/null @@ -1,185 +0,0 @@ -""" -Example demonstrating the decorator-based model registration system. -""" -from datetime import datetime -from typing import List, Optional -from pydantic import BaseModel - -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from lib_ro_crate_schema.crate.decorators import ro_crate_schema, Field -from lib_ro_crate_schema.crate.schema_facade import SchemaFacade -from lib_ro_crate_schema.crate.schema_registry import get_schema_registry - - -# Example 1: Basic model with ontology annotations (required and optional fields) -@ro_crate_schema(ontology="https://schema.org/Person") -class Person(BaseModel): - """A person in the research project""" - # Required fields (minCardinality: 1) - name: str = Field(ontology="https://schema.org/name", comment="Person's full name") - email: str = Field(ontology="https://schema.org/email", comment="Contact email address") - - # Optional fields (minCardinality: 0) - orcid: Optional[str] = Field(default=None, ontology="https://orcid.org/", comment="ORCID identifier") - phone: Optional[str] = Field(default=None, ontology="https://schema.org/telephone", comment="Phone number") - affiliation: Optional[str] = Field(default=None, ontology="https://schema.org/affiliation", comment="Institution affiliation") - - -# Example 2: Model with relationships and mixed required/optional fields -@ro_crate_schema(ontology="https://schema.org/Dataset") -class Dataset(BaseModel): - """A research dataset""" - # Required fields (minCardinality: 1) - title: str = Field(ontology="https://schema.org/name", comment="Dataset title") - description: str = Field(ontology="https://schema.org/description", comment="Dataset description") - authors: List[Person] = Field(ontology="https://schema.org/author", comment="Dataset authors") - created_date: datetime = Field(ontology="https://schema.org/dateCreated", comment="Creation date") - - # Optional fields (minCardinality: 0) - keywords: Optional[List[str]] = Field(default=None, ontology="https://schema.org/keywords", comment="Research keywords") - version: Optional[str] = Field(default=None, ontology="https://schema.org/version", comment="Dataset version") - license: Optional[str] = Field(default=None, ontology="https://schema.org/license", comment="License information") - - -# Example 3: Model with institutional information -@ro_crate_schema(ontology="https://schema.org/Organization") -class Institution(BaseModel): - """Research institution or organization""" - name: str = Field(ontology="https://schema.org/name", comment="Institution name") - country: str = Field(comment="Country where institution is located") - website: Optional[str] = Field(default=None, comment="Institution website") - - -def example_usage(): - """Demonstrate the complete workflow""" - - print("=== Decorator-based RO-Crate Schema Generation ===") - print() - - # 1. Show registered models (automatically registered by decorators) - registry = get_schema_registry() - - print("Registered models:") - for model_name, type_template in registry.get_all_type_templates().items(): - print(f" - {model_name}: {type_template.ontology}") - for prop_info in type_template.type_properties: - print(f" * {prop_info.name}: {prop_info.rdf_type} (ontology: {prop_info.ontology})") - print() - - # 2. Create schema facade and add all registered models - facade = SchemaFacade() - facade.add_all_registered_models() - - print(f"Schema contains {len(facade.types)} types:") - for type_obj in facade.types: - print(f" - {type_obj.id}: {type_obj.ontological_annotations}") - print() - - # 3. Create model instances and add them as metadata - person1 = Person( - name="Dr. Jane Smith", - email="jane.smith@university.edu", - orcid="0000-0000-0000-0001" - ) - - person2 = Person( - name="Prof. John Doe", - email="john.doe@institute.org" - ) - - dataset = Dataset( - title="Climate Change Impact Study", - description="Analysis of climate data from 2000-2023", - authors=[person1, person2], - created_date=datetime(2024, 1, 15), - keywords=["climate", "environment", "data analysis"] - ) - - # Add instances as metadata entries - facade.add_model_instance(person1, "jane_smith") - facade.add_model_instance(person2, "john_doe") - facade.add_model_instance(dataset, "climate_study_2024") - - print(f"Metadata contains {len(facade.metadata_entries)} entries:") - for entry in facade.metadata_entries: - print(f" - {entry.id} ({entry.class_id})") - print(f" Properties: {entry.properties}") - print(f" References: {entry.references}") - print() - - # 4. Generate RDF graph - graph = facade.to_graph() - print(f"Generated RDF graph with {len(graph)} triples") - print() - print("Sample triples:") - for i, (s, p, o) in enumerate(graph): - if i < 10: # Show first 10 triples - print(f" {s} {p} {o}") - print() - - # 5. Convert to RO-Crate - from lib_ro_crate_schema.crate.jsonld_utils import add_schema_to_crate - from rocrate.rocrate import ROCrate - import json - from pathlib import Path - - print("🔄 Adding schema and metadata to RO-Crate...") - crate = ROCrate() - crate.name = "Decorator Example RO-Crate" - crate.description = "Generated using decorator-based schema registration" - - final_crate = add_schema_to_crate(facade, crate) - - # Get JSON representation by writing to temp directory - import tempfile - with tempfile.TemporaryDirectory() as temp_dir: - final_crate.write(temp_dir) - metadata_file = Path(temp_dir) / "ro-crate-metadata.json" - with open(metadata_file, 'r') as f: - final_crate_json = json.load(f) - - # Save to file - output_path = Path("ro-crate-metadata.json") - with open(output_path, 'w', encoding='utf-8') as f: - json.dump(final_crate_json, f, indent=2) - - print(f"✅ RO-Crate saved to: {output_path.absolute()}") - print(f"📊 Total entities in @graph: {len(final_crate_json['@graph'])}") - print() - - # Show entity types summary - entity_types = {} - for entity in final_crate_json["@graph"]: - entity_type = entity.get("@type", "Unknown") - if isinstance(entity_type, list): - for t in entity_type: - entity_types[t] = entity_types.get(t, 0) + 1 - else: - entity_types[entity_type] = entity_types.get(entity_type, 0) + 1 - - print("📋 Entity types in RO-Crate:") - for entity_type, count in entity_types.items(): - print(f" - {entity_type}: {count}") - print() - - # Show context - context = final_crate_json["@context"] - print(f"🔗 RO-Crate @context: {context}") - print() - - print("🎯 Key Features Demonstrated:") - print(" ✓ Pydantic models → RDFS schema") - print(" ✓ Ontology annotations (schema.org, ORCID)") - print(" ✓ Model instances → RDF metadata") - print(" ✓ Proper RO-Crate integration") - print(" ✓ JSON-LD context management") - print(" ✓ Schema embedding in ro-crate-metadata.json") - - return facade, final_crate_json - - -if __name__ == "__main__": - example_usage() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/examples/examples.py b/0.2.x/lib/python/lib-ro-crate-schema/examples/examples.py deleted file mode 100644 index a1051bc..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/examples/examples.py +++ /dev/null @@ -1,135 +0,0 @@ -# Utility functions for reconstruction - -import json -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from lib_ro_crate_schema.crate.type import Type -from lib_ro_crate_schema.crate.type_property import TypeProperty -from lib_ro_crate_schema.crate.literal_type import LiteralType -from lib_ro_crate_schema.crate.metadata_entry import MetadataEntry -from lib_ro_crate_schema.crate.schema_facade import SchemaFacade -from rocrate.rocrate import ROCrate - -from rdflib import Graph -from lib_ro_crate_schema.crate.jsonld_utils import add_schema_to_crate -# from lib_ro_crate_schema.crate import reconstruction # Not available - - -def main(): - """ - Example demonstrating manual RO-Crate construction with automatic OWL restrictions. - - When manually creating TypeProperty objects, you can specify required=True/False - to automatically generate OWL restrictions with appropriate cardinality constraints: - - required=True -> generates minCardinality: 1 (field is mandatory) - - required=False -> generates minCardinality: 0 (field is optional) - - This ensures Java compatibility where OWL restrictions define field requirements. - """ - - # Define properties with cardinality information - name = TypeProperty( - id="name", - range_includes=[LiteralType.STRING], - required=True, # This will generate minCardinality: 1 - label="Full Name", - comment="The full name of the entity" - ) - identifier = TypeProperty( - id="identifier", - range_includes=[LiteralType.STRING], - required=True, # This will generate minCardinality: 1 - label="Identifier", - comment="Unique identifier for the entity" - ) - - colleague = TypeProperty( - id="colleague", - range_includes=["Participant"], - required=False, # This will generate minCardinality: 0 (optional) - label="Colleague", - comment="Optional colleague relationship" - ) - - participant_type = Type( - id="Participant", - type="Type", - subclass_of=["https://schema.org/Thing"], - ontological_annotations=["http://purl.org/dc/terms/creator"], - rdfs_property=[name, identifier], - comment="A participant in the research", - label="Participant", - ) - - creator_type = Type( - id="Creator", - type="Type", - subclass_of=["https://schema.org/Thing"], - ontological_annotations=["http://purl.org/dc/terms/creator"], - rdfs_property=[name, identifier, colleague], - comment="A creator of the research work", - label="Creator", - ) - - # Example MetadataEntry using new format with class_id and values - creator_entry = MetadataEntry( - id="creator1", - class_id="Creator", - values={ - "name": "John Author", - "identifier": "https://orcid.org/0000-0000-0000-0000", - }, - references={}, - ) - - participant_entry = MetadataEntry( - id="participant", - class_id="Participant", - values={ - "name": "Karl Participant", - "identifier": "https://orcid.org/0000-0000-0000-0001", - }, - references={ - "colleague": ["creator1"] - }, - ) - - schema = SchemaFacade( - types=[creator_type, participant_type], - # properties=[has_name, has_identifier], - metadata_entries=[creator_entry, participant_entry], - ) - #Resolve refs - schema.resolve_forward_refs() - #Add it to a crate - crate = ROCrate() - crate.license = "a" - crate.name = "mtcrate" - crate.description = "test crate" - crate = add_schema_to_crate(schema, crate) - #Serialise - write to temp dir and read back for JSON output - import tempfile - with tempfile.TemporaryDirectory() as temp_dir: - crate.write(temp_dir) - metadata_file = Path(temp_dir) / "ro-crate-metadata.json" - with open(metadata_file, 'r') as f: - res = json.load(f) - print(json.dumps(res)) - # Write to file - import os - output_dir = "output_crates" - os.makedirs(output_dir, exist_ok=True) - crate_path = os.path.join(output_dir, "example_crate") - crate.write(crate_path) - - -# Use the reconstruction module's main entry point -def reconstruct(graph: Graph): - # return reconstruction.reconstruct(graph) # Not available - raise NotImplementedError("Reconstruction module not available") - - -if __name__ == "__main__": - main() diff --git a/0.2.x/lib/python/lib-ro-crate-schema/examples/export_import_pydantic_demo.py b/0.2.x/lib/python/lib-ro-crate-schema/examples/export_import_pydantic_demo.py deleted file mode 100644 index 4bb23ca..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/examples/export_import_pydantic_demo.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env python3 -""" -Demonstration of exporting Pydantic models from SchemaFacade. - -This example shows how to: -1. Create a schema with Type definitions -2. Export those Types as Pydantic model classes -3. Use the generated classes to create and validate instances -""" - -import sys -from pathlib import Path - -# Add src to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from lib_ro_crate_schema.crate.schema_facade import SchemaFacade -from lib_ro_crate_schema.crate.type import Type -from lib_ro_crate_schema.crate.type_property import TypeProperty -from lib_ro_crate_schema.crate.restriction import Restriction -from lib_ro_crate_schema.crate.literal_type import LiteralType -from pydantic import BaseModel -from typing import List, Optional - - -def main(): - print("🔧 RO-Crate Pydantic Export Demo") - print("=" * 50) - - # Create SchemaFacade and add some types - # For this demo, we'll define two types: Person and Organization - # The ro-crate-schema will not be exported as crate, just used here for model generation - facade = SchemaFacade() - - # Define Person type, starting with the properties and restrictions - person_name_prop = TypeProperty( - id="name", - label="Full Name", - comment="The complete name of the person", - range_includes=["http://www.w3.org/2001/XMLSchema#string"], - required=True - ) - - person_age_prop = TypeProperty( - id="age", - label="Age", - comment="Age in years", - range_includes=["http://www.w3.org/2001/XMLSchema#integer"], - required=False - ) - - person_emails_prop = TypeProperty( - id="emails", - label="Email Addresses", - comment="List of email addresses", - range_includes=["http://www.w3.org/2001/XMLSchema#string"], - required=False - ) - - # Create restrictions - person_name_restriction = Restriction( - property_type="name", - min_cardinality=1, - max_cardinality=1 - ) - - person_age_restriction = Restriction( - property_type="age", - min_cardinality=0, - max_cardinality=1 - ) - - person_emails_restriction = Restriction( - property_type="emails", - min_cardinality=0, - max_cardinality=None # Unbounded list - ) - - person_type = Type( - id="Person", - label="Person", - comment="Represents a person with personal information", - subclass_of=["https://schema.org/Person"], - rdfs_property=[person_name_prop, person_age_prop, person_emails_prop], - restrictions=[person_name_restriction, person_age_restriction, person_emails_restriction] - ) - - # Define Organization type, starting with properties and restrictions - org_name_prop = TypeProperty( - id="name", - label="Organization Name", - comment="The official name of the organization", - range_includes=["http://www.w3.org/2001/XMLSchema#string"], - required=True - ) - - org_employees_prop = TypeProperty( - id="employees", - label="Employees", - comment="People working for this organization", - range_includes=["Person"], # Reference to Person type - required=False - ) - - org_name_restriction = Restriction( - property_type="name", - min_cardinality=1, - max_cardinality=1 - ) - - org_employees_restriction = Restriction( - property_type="employees", - min_cardinality=0, - max_cardinality=None # Unbounded list - ) - - organization_type = Type( - id="Organization", - label="Organization", - comment="Represents an organization or company", - subclass_of=["https://schema.org/Organization"], - rdfs_property=[org_name_prop, org_employees_prop], - restrictions=[org_name_restriction, org_employees_restriction] - ) - - # Add types to facade - facade.addType(person_type) - facade.addType(organization_type) - - print("📋 Schema created with types:") - for type_def in facade.get_types(): - print(f" - {type_def.id}: {type_def.comment}") - - print("\n🏗️ Exporting Pydantic models...") - - # Export individual model - print("\n1️⃣ Export single model:") - PersonModel = facade.export_pydantic_model("Person") - print(f"Generated class: {PersonModel.__name__}") - print(f"Fields: {list(PersonModel.__annotations__.keys())}") - - # Export all models - print("\n2️⃣ Export all models:") - models = facade.export_all_pydantic_models() - print("Generated models:") - for name, model_class in models.items(): - print(f" - {name}: {model_class.__name__}") - print(f" Fields: {list(model_class.__annotations__.keys())}") - - print("\n✨ Testing generated models...") - - # Test Person model - print("\n👤 Creating Person instances:") - try: - # Valid person with required field - person1 = PersonModel(name="Alice Johnson", age=30, emails=["alice@example.com", "alice.j@work.com"]) - print(f"✅ Created person: {person1.name}, age {person1.age}") - print(f" Emails: {person1.emails}") - - # Person with only required fields - person2 = PersonModel(name="Bob Smith") - print(f"✅ Created person: {person2.name} (minimal)") - - # Test validation - missing required field - print("\n🔍 Testing validation:") - try: - invalid_person = PersonModel(age=25) # Missing required 'name' - print("❌ This should have failed!") - except Exception as e: - print(f"✅ Validation caught error: {e}") - - except Exception as e: - print(f"❌ Error creating person: {e}") - - # Test Organization model - print("\n🏢 Creating Organization instances:") - try: - OrganizationModel = models["Organization"] - - # Note: For now, forward references to other models need to be handled carefully - # In a real implementation, you'd want to resolve these properly - person_as_dict = {"name": "Charlie Brown", "age": 28} - org = OrganizationModel(name="Acme Corporation", employees=[person1, person_as_dict]) - print(f"✅ Created organization: {org.name} with employees {[emp.name for emp in org.employees]}") - - # Test validation - employees must be person instances or dicts with the right fields - try: - invalid_org = OrganizationModel(name="Invalid Org", employees=["Not a person"]) - print("❌ This should have failed!") - except Exception as e: - print(f"✅ Validation caught error: {e}") - - - - # Test validation - employees missing name (required field) will fail - fake_person = {"firstname": "Fake", "lastname": "Person"} - try: - invalid_org = OrganizationModel(name="Invalid Org", employees=[fake_person]) - print("❌ This should have failed!") - except Exception as e: - print(f"✅ Validation caught error: {e}") - - except Exception as e: - print(f"❌ Error creating organization: {e}") - - print("\n🎯 Model schemas:") - print("\nPerson model schema:") - try: - print(PersonModel.model_json_schema()) - except Exception as e: - print(f"Schema generation error: {e}") - - print("\n🎉 Pydantic export demo completed!") - print("\n💡 Key features demonstrated:") - print(" ✓ Export Type definitions as Pydantic model classes") - print(" ✓ Handle required vs optional fields from OWL restrictions") - print(" ✓ Support list fields (unbounded cardinality)") - print(" ✓ Map RDF types to Python types") - print(" ✓ Generate proper Pydantic validation") - print(" ✓ Preserve field metadata (descriptions)") - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/examples/full_example.py b/0.2.x/lib/python/lib-ro-crate-schema/examples/full_example.py index 7c40e7d..fab47da 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/examples/full_example.py +++ b/0.2.x/lib/python/lib-ro-crate-schema/examples/full_example.py @@ -45,82 +45,82 @@ from lib_ro_crate_schema.crate.schema_facade import SchemaFacade @ro_crate_schema(ontology="http://openbis.org/Project") class Project(BaseModel): """OpenBIS research project""" - code: str = Field(comment="Unique project identifier") - name: str = Field(ontology="https://schema.org/name") - description: str = Field(ontology="https://schema.org/description") - created_date: datetime = Field(ontology="https://schema.org/dateCreated") - space: Optional['Space'] = Field(default=None, ontology="http://openbis.org/hasSpace") + code: str = Field(json_schema_extra={"comment": "Unique project identifier"}) + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + description: str = Field(json_schema_extra={"ontology": "https://schema.org/description"}) + created_date: datetime = Field(json_schema_extra={"ontology": "https://schema.org/dateCreated"}) + space: Optional['Space'] = Field(default=None, json_schema_extra={"ontology": "http://openbis.org/hasSpace"}) @ro_crate_schema(ontology="http://openbis.org/Space") class Space(BaseModel): """OpenBIS laboratory space""" - name: str = Field(ontology="https://schema.org/name") - description: str = Field(ontology="https://schema.org/description") - created_date: datetime = Field(ontology="https://schema.org/dateCreated") - collections: List['Collection'] = Field(default=[], ontology="http://openbis.org/hasCollection") + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + description: str = Field(json_schema_extra={"ontology": "https://schema.org/description"}) + created_date: datetime = Field(json_schema_extra={"ontology": "https://schema.org/dateCreated"}) + collections: List['Collection'] = Field(default=[], json_schema_extra={"ontology": "http://openbis.org/hasCollection"}) @ro_crate_schema(ontology="http://openbis.org/Collection") class Collection(BaseModel): """OpenBIS sample/data collection""" - name: str = Field(ontology="https://schema.org/name") - sample_type: str = Field(comment="Type of samples stored") - storage_conditions: str = Field(comment="Storage requirements") - created_date: datetime = Field(ontology="https://schema.org/dateCreated") - contains: List[Any] = Field(default=[], comment="Entities contained in the collection") + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + sample_type: str = Field(json_schema_extra={"comment": "Type of samples stored"}) + storage_conditions: str = Field(json_schema_extra={"comment": "Storage requirements"}) + created_date: datetime = Field(json_schema_extra={"ontology": "https://schema.org/dateCreated"}) + contains: List[Any] = Field(default=[], json_schema_extra={"comment": "Entities contained in the collection"}) @ro_crate_schema(ontology="http://openbis.org/Equipment") class Equipment(BaseModel): """Laboratory equipment with optional nesting""" - name: str = Field(ontology="https://schema.org/name") - model: str = Field(comment="Equipment model/version") - serial_number: str = Field(ontology="https://schema.org/serialNumber") - created_date: datetime = Field(ontology="https://schema.org/dateCreated") - parent_equipment: Optional['Equipment'] = Field(default=None, ontology="https://schema.org/isPartOf") - configuration: Dict[str, Any] = Field(default={}, comment="Equipment configuration parameters") + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + model: str = Field(json_schema_extra={"comment": "Equipment model/version"}) + serial_number: str = Field(json_schema_extra={"ontology": "https://schema.org/serialNumber"}) + created_date: datetime = Field(json_schema_extra={"ontology": "https://schema.org/dateCreated"}) + parent_equipment: Optional['Equipment'] = Field(default=None, json_schema_extra={"ontology": "https://schema.org/isPartOf"}) + configuration: Dict[str, Any] = Field(default={}, json_schema_extra={"comment": "Equipment configuration parameters"}) @ro_crate_schema(ontology="https://schema.org/ChemicalSubstance") class Molecule(BaseModel): """Chemical compound with SMILES notation""" - name: str = Field(ontology="https://schema.org/name") - smiles: str = Field(comment="SMILES notation for chemical structure") - molecular_weight: float = Field(comment="Molecular weight in g/mol") - contains_molecules: List['Molecule'] = Field(default=[], ontology="https://schema.org/hasPart") - cas_number: Optional[str] = Field(default=None, comment="CAS Registry Number") - created_date: datetime = Field(ontology="https://schema.org/dateCreated") - experimental_notes: Optional[str] = Field(default=None, comment="Lab notes or modifications") + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + smiles: str = Field(json_schema_extra={"comment": "SMILES notation for chemical structure"}) + molecular_weight: float = Field(json_schema_extra={"comment": "Molecular weight in g/mol"}) + contains_molecules: List['Molecule'] = Field(default=[], json_schema_extra={"ontology": "https://schema.org/hasPart"}) + cas_number: Optional[str] = Field(default=None, json_schema_extra={"comment": "CAS Registry Number"}) + created_date: datetime = Field(json_schema_extra={"ontology": "https://schema.org/dateCreated"}) + experimental_notes: Optional[str] = Field(default=None, json_schema_extra={"comment": "Lab notes or modifications"}) @ro_crate_schema(ontology="https://schema.org/Person") class Person(BaseModel): """Research author/scientist""" - name: str = Field(ontology="https://schema.org/name") - orcid: str = Field(ontology="https://schema.org/identifier") - email: str = Field(ontology="https://schema.org/email") - affiliation: 'Organization' = Field(ontology="https://schema.org/affiliation") - colleagues: List['Person'] = Field(default=[], ontology="https://schema.org/colleague") + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + orcid: str = Field(json_schema_extra={"ontology": "https://schema.org/identifier"}) + email: str = Field(json_schema_extra={"ontology": "https://schema.org/email"}) + affiliation: 'Organization' = Field(json_schema_extra={"ontology": "https://schema.org/affiliation"}) + colleagues: List['Person'] = Field(default=[], json_schema_extra={"ontology": "https://schema.org/colleague"}) @ro_crate_schema(ontology="https://schema.org/Organization") class Organization(BaseModel): """Research institution""" - name: str = Field(ontology="https://schema.org/name") - country: str = Field(ontology="https://schema.org/addressCountry") - website: str = Field(ontology="https://schema.org/url") + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + country: str = Field(json_schema_extra={"ontology": "https://schema.org/addressCountry"}) + website: str = Field(json_schema_extra={"ontology": "https://schema.org/url"}) @ro_crate_schema(ontology="https://schema.org/ScholarlyArticle") class Publication(BaseModel): """Scientific publication""" - title: str = Field(ontology="https://schema.org/name") - authors: List[Person] = Field(ontology="https://schema.org/author") - molecules: List[Molecule] = Field(ontology="https://schema.org/mentions") - equipment: List[Equipment] = Field(ontology="https://schema.org/instrument") - organization: Organization = Field(ontology="https://schema.org/publisher") - doi: str = Field(ontology="https://schema.org/identifier") - publication_date: datetime = Field(ontology="https://schema.org/datePublished") + title: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + authors: List[Person] = Field(json_schema_extra={"ontology": "https://schema.org/author"}) + molecules: List[Molecule] = Field(json_schema_extra={"ontology": "https://schema.org/mentions"}) + equipment: List[Equipment] = Field(json_schema_extra={"ontology": "https://schema.org/instrument"}) + organization: Organization = Field(json_schema_extra={"ontology": "https://schema.org/publisher"}) + doi: str = Field(json_schema_extra={"ontology": "https://schema.org/identifier"}) + publication_date: datetime = Field(json_schema_extra={"ontology": "https://schema.org/datePublished"}) def create_initial_data(): diff --git a/0.2.x/lib/python/lib-ro-crate-schema/examples/minimal_import_example.py b/0.2.x/lib/python/lib-ro-crate-schema/examples/minimal_import_example.py deleted file mode 100644 index edb2246..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/examples/minimal_import_example.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -""" -Minimal import example: Load external openBIS RO-Crate and print summary. -""" - -import sys -from pathlib import Path - -# Add src to path -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from lib_ro_crate_schema.crate.schema_facade import SchemaFacade - -# Import openBIS RO-Crate from external lib/example (kept outside for now) -crate_path = Path(__file__).parent.parent.parent.parent / "example" / "obenbis-one-publication" / "ro-crate-metadata.json" -facade = SchemaFacade.from_ro_crate(str(crate_path)) - -# Print summary -print(f"📁 Imported SchemaFacade with:") -print(f" - {len(facade.types)} RDFS Classes (types)") -print(f" - {len(facade.metadata_entries)} metadata entries") - -print(f"\n📋 Types imported:") -for t in facade.types: - props = len(t.rdfs_property or []) - restrictions = len(t.get_restrictions()) - print(f" - {t.id}: {props} properties, {restrictions} restrictions") - -print(f"\n📦 Metadata entries:") -for entry in facade.metadata_entries[:5]: # Show first 5 - print(f" - {entry.id} (type: {entry.class_id})") - -print(f"\n🎯 Ready to use! You can now:") -print(f" - Export: facade.write('output-directory')") -print(f" - Add data: facade.addEntry(...)") -print(f" - Add types: facade.addType(...)") \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/examples/minimal_pydantic_example.py b/0.2.x/lib/python/lib-ro-crate-schema/examples/minimal_pydantic_example.py new file mode 100644 index 0000000..4817b60 --- /dev/null +++ b/0.2.x/lib/python/lib-ro-crate-schema/examples/minimal_pydantic_example.py @@ -0,0 +1,53 @@ +""" +Minimal example showing basic Pydantic model usage with RO-Crate schema decorators. +This demonstrates the simplest way to create an RO-Crate with typed entities. +""" + +from pydantic import BaseModel +from lib_ro_crate_schema.crate.decorators import ro_crate_schema, Field +from lib_ro_crate_schema.crate.schema_facade import SchemaFacade +from lib_ro_crate_schema.crate.jsonld_utils import add_schema_to_crate +from rocrate.rocrate import ROCrate +from pathlib import Path + + +# Define a simple Person schema +@ro_crate_schema(ontology="https://schema.org/Person") +class Person(BaseModel): + """Person entity in the RO-Crate""" + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + email: str = Field(json_schema_extra={"ontology": "https://schema.org/email"}) + + +def main(): + """Minimal example: Create a Person and export to RO-Crate""" + + print("Creating a Person instance...") + person = Person(name="Alice Researcher", email="alice@example.org") + + print("Building RO-Crate...") + # Create schema facade and add registered models + facade = SchemaFacade() + facade.add_all_registered_models() + + # Add the person instance + facade.add_model_instance(person, "alice") + + # Create RO-Crate + crate = ROCrate() + crate.name = "Minimal Example" + crate.description = "A minimal RO-Crate with one Person" + + final_crate = add_schema_to_crate(facade, crate) + + # Export + output_path = Path("output_crates/minimal_example") + output_path.mkdir(parents=True, exist_ok=True) + final_crate.write(output_path) + + print(f"✓ RO-Crate exported to: {output_path}") + print(f"✓ Entities: {len(final_crate.get_entities())}") + + +if __name__ == "__main__": + main() diff --git a/0.2.x/lib/python/lib-ro-crate-schema/pyproject.toml b/0.2.x/lib/python/lib-ro-crate-schema/pyproject.toml index f9f88f5..1f1c21f 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/pyproject.toml +++ b/0.2.x/lib/python/lib-ro-crate-schema/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lib-ro-crate-schema" -version = "0.2.0" +version = "0.2.1" description = "A Pythonic library for creating and managing RO-Crates with schema definitions using Pydantic models" readme = "README.md" license = { text = "Apache-2.0" } @@ -42,10 +42,10 @@ dependencies = [ ] [project.urls] -Homepage = "https://github.com/Snowwpanda/ro-crate-interoperability-profile" -Documentation = "https://github.com/Snowwpanda/ro-crate-interoperability-profile/tree/main/0.2.x/lib/python/lib-ro-crate-schema" -Repository = "https://github.com/Snowwpanda/ro-crate-interoperability-profile" -Issues = "https://github.com/Snowwpanda/ro-crate-interoperability-profile/issues" +Homepage = "https://github.com/researchobjectschema/ro-crate-interoperability-profile" +Documentation = "https://github.com/researchobjectschema/ro-crate-interoperability-profile/tree/main/0.2.x/lib/python/lib-ro-crate-schema" +Repository = "https://github.com/researchobjectschema/ro-crate-interoperability-profile" +Issues = "https://github.com/researchobjectschema/ro-crate-interoperability-profile/issues" [project.optional-dependencies] dev = [ diff --git a/0.2.x/lib/python/lib-ro-crate-schema/run_all_tests.py b/0.2.x/lib/python/lib-ro-crate-schema/run_all_tests.py deleted file mode 100644 index a70f69d..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/run_all_tests.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 -""" -Test runner for RO-Crate Schema Library -""" -import sys -import subprocess -from pathlib import Path - -def run_test(test_file): - """Run a single test file and return success status""" - print(f"\n🧪 Running {test_file.name}") - print("=" * 60) - - try: - result = subprocess.run([sys.executable, str(test_file)], - capture_output=False, - check=True, - cwd=test_file.parent) - print(f"✅ {test_file.name} PASSED") - return True - except subprocess.CalledProcessError as e: - print(f"❌ {test_file.name} FAILED (exit code: {e.returncode})") - return False - except Exception as e: - print(f"❌ {test_file.name} ERROR: {e}") - return False - -def main(): - """Run all tests""" - print("🚀 RO-Crate Schema Library Test Suite") - print("=" * 60) - - # Find test directory - test_dir = Path(__file__).parent / "tests" - if not test_dir.exists(): - print(f"❌ Test directory not found: {test_dir}") - return False - - # Find all test files - test_files = list(test_dir.glob("test_*.py")) - if not test_files: - print(f"❌ No test files found in {test_dir}") - return False - - print(f"📋 Found {len(test_files)} test files:") - for test_file in test_files: - print(f" - {test_file.name}") - - # Run tests - results = [] - for test_file in test_files: - success = run_test(test_file) - results.append((test_file.name, success)) - - # Summary - print("\n🎯 Test Results Summary") - print("=" * 60) - - passed = sum(1 for _, success in results if success) - total = len(results) - - for test_name, success in results: - status = "✅ PASS" if success else "❌ FAIL" - print(f" {test_name}: {status}") - - print(f"\n📊 Overall: {passed}/{total} tests passed") - - if passed == total: - print("🏆 ALL TESTS PASSED!") - return True - else: - print("💥 SOME TESTS FAILED!") - return False - -if __name__ == "__main__": - success = main() - sys.exit(0 if success else 1) \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/run_tests.py b/0.2.x/lib/python/lib-ro-crate-schema/run_tests.py deleted file mode 100644 index ef4d556..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/run_tests.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python3 -""" -Interactive test runner for RO-Crate bidirectional system -""" - -import sys -import subprocess -from pathlib import Path - -def run_test(test_file, working_dir=None): - """Run a test file with proper environment setup""" - import os - - original_dir = Path.cwd() - - try: - if working_dir: - Path(working_dir).mkdir(parents=True, exist_ok=True) - os.chdir(working_dir) - - # Make test_file relative to the working directory if it's absolute - if working_dir and test_file.is_absolute(): - try: - test_file = test_file.relative_to(working_dir) - except ValueError: - # If we can't make it relative, use the absolute path - pass - - # Try to use uv if available, otherwise use regular python - try: - result = subprocess.run([ - "uv", "run", "python", str(test_file) - ], check=True, capture_output=False) - except (subprocess.CalledProcessError, FileNotFoundError): - # Fallback to regular python - result = subprocess.run([ - "python", str(test_file) - ], check=True, capture_output=False) - - return result.returncode == 0 - except Exception as e: - print(f"❌ Error running {test_file}: {e}") - return False - finally: - os.chdir(original_dir) - -def main(): - print("🔬 RO-Crate Bidirectional Test Runner") - print("=====================================") - - # Get the path to test folder - test_folder = Path(__file__).parent / "tests" - # Read in the tests dictionary - if not test_folder.exists(): - print(f"❌ Test folder not found: {test_folder}") - sys.exit(1) - tests = {} - test_counter = 1 - for test in test_folder.glob("test_*.py"): - test_name = test.stem.replace("test_", "").replace("_", " ").title() - tests[str(test_counter)] = (test_name, test, None) - test_counter += 1 - - - - print("\nAvailable tests:") - for key, (name, _, _) in tests.items(): - print(f"{key}. {name}") - print() - - choice = input("Select test (number) or press Enter for complete test: ").strip() - - if not choice: - # Run script run_all_tests.py - script_path = Path(__file__).parent / "run_all_tests.py" - if script_path.exists(): - print("\n🔄 Running all tests via run_all_tests.py...") - success = run_test(script_path) - if success: - print("\n✅ All tests completed successfully!") - else: - print("\n❌ Some tests failed!") - sys.exit(1) - print("\n🏁 Test execution completed!") - return - - if choice in tests: - name, test_file, working_dir = tests[choice] - print(f"\n🔄 Running {name}...") - success = run_test(test_file, working_dir) - - if success: - print(f"\n✅ {name} completed successfully!") - else: - print(f"\n❌ {name} failed!") - sys.exit(1) - else: - print("❌ Invalid choice. Running default complete test...") - run_test("test_complete_round_trip.py") - - print("\n🏁 Test execution completed!") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/__init__.py b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/__init__.py index 3e28230..7451b46 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/__init__.py +++ b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/__init__.py @@ -14,8 +14,8 @@ Quick Start @ro_crate_schema(ontology="https://schema.org/Person") class Person(BaseModel): - name: str = Field(ontology="https://schema.org/name") - email: str = Field(ontology="https://schema.org/email") + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + email: str = Field(json_schema_extra={"ontology": "https://schema.org/email"}) # Create and export facade = SchemaFacade() diff --git a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/decorators.py b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/decorators.py index 5d325e9..ecac664 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/decorators.py +++ b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/decorators.py @@ -12,14 +12,17 @@ def Field(ontology: Optional[str] = None, comment: Optional[str] = None, **kwarg """Enhanced Pydantic Field that supports ontology annotations for RO-Crate schema generation. Args: - ontology: URI of the ontological concept this field represents - comment: Human-readable description of this field - **kwargs: Standard Pydantic Field arguments + ontology: URI of the ontological concept this field represents (deprecated: use json_schema_extra) + comment: Human-readable description of this field (deprecated: use json_schema_extra) + **kwargs: Standard Pydantic Field arguments, including json_schema_extra Returns: Pydantic FieldInfo with RO-Crate metadata - Example: + Example (recommended): + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name", "comment": "Person's full name"}) + + Example (deprecated but still supported): name: str = Field(ontology="https://schema.org/name", comment="Person's full name") """ # Store RO-Crate specific metadata in json_schema_extra @@ -67,8 +70,8 @@ def ro_crate_schema( Example: @ro_crate_schema(id="Person", ontology="https://schema.org/Person") class PersonModel(BaseModel): - name: str = Field(ontology="https://schema.org/name") - email: str = Field(ontology="https://schema.org/email") + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + email: str = Field(json_schema_extra={"ontology": "https://schema.org/email"}) """ def decorator(cls: Type[BaseModel]) -> Type[BaseModel]: # Ensure it's a Pydantic model diff --git a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/schema_registry.py b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/schema_registry.py index 57afa18..33471b2 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/schema_registry.py +++ b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/schema_registry.py @@ -104,9 +104,13 @@ class SchemaRegistry: # Convert to RDF type rdf_type = self._type_converter.python_to_rdf(field_type) - # Extract ontology annotation from field metadata + # Extract ontology and comment annotations from field metadata json_extra = getattr(field_info, 'json_schema_extra', None) if hasattr(field_info, 'json_schema_extra') else None ontology = json_extra.get('ontology') if json_extra else None + # Prefer comment from json_schema_extra, fallback to description + comment = json_extra.get('comment') if json_extra else None + if comment is None: + comment = field_info.description type_property_template = TypePropertyTemplate( name=field_name, @@ -115,7 +119,7 @@ class SchemaRegistry: required=field_info.is_required(), is_list=is_list, ontology=ontology, - comment=field_info.description, + comment=comment, default_value=field_info.default if field_info.default is not ... else None ) diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_context_detection.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_context_detection.py deleted file mode 100644 index ae5e54b..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_context_detection.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple test to see how unknown namespaces are handled by get_context function. -""" - -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent / "src")) - -from lib_ro_crate_schema.crate.jsonld_utils import get_context -from rdflib import Graph, URIRef, Literal -from rdflib.namespace import RDF, RDFS - - -def create_graph_with_unknown_namespaces(): - """Create an RDF graph with unknown namespaces.""" - g = Graph() - - # Add triples with unknown pokemon.org namespace - pokemon_ns = "http://pokemon.org/" - pikachu = URIRef(pokemon_ns + "pikachu") - pokemon_name = URIRef(pokemon_ns + "pokemonName") - electric_type = URIRef(pokemon_ns + "ElectricPokemon") - - # Add some triples - g.add((pikachu, RDF.type, electric_type)) - g.add((pikachu, pokemon_name, Literal("Pikachu"))) - g.add((pokemon_name, RDF.type, RDF.Property)) - g.add((pokemon_name, RDFS.label, Literal("Pokemon Name"))) - - # Add triples with another unknown namespace - villains_ns = "http://villains.org/" - team_rocket = URIRef(villains_ns + "team_rocket") - criminal_org = URIRef(villains_ns + "CriminalOrganization") - motto = URIRef(villains_ns + "motto") - - g.add((team_rocket, RDF.type, criminal_org)) - g.add((team_rocket, motto, Literal("Prepare for trouble!"))) - - # Also add some known namespaces for comparison - schema_name = URIRef("https://schema.org/name") - g.add((pikachu, schema_name, Literal("Pikachu the Electric Mouse"))) - - # Add example.com namespace (base namespace in predefined list) - example_person = URIRef("http://example.com/trainer") - example_name = URIRef("http://example.com/trainerName") - g.add((example_person, example_name, Literal("Ash Ketchum"))) - g.add((example_name, RDF.type, RDF.Property)) - - return g - - -def main(): - print("🔍 TESTING get_context() WITH UNKNOWN NAMESPACES") - print("=" * 55) - - # Create graph with unknown namespaces - g = create_graph_with_unknown_namespaces() - - print("📊 Graph Statistics:") - print(f" Total triples: {len(g)}") - - print("\n🔍 URIs in the graph:") - all_uris = set() - for s, p, o in g: - for uri in [str(s), str(p), str(o)]: - if uri.startswith('http'): - all_uris.add(uri) - - # Group by namespace - namespaces = {} - for uri in sorted(all_uris): - if 'pokemon.org' in uri: - namespaces.setdefault('pokemon.org', []).append(uri) - elif 'villains.org' in uri: - namespaces.setdefault('villains.org', []).append(uri) - elif 'schema.org' in uri: - namespaces.setdefault('schema.org', []).append(uri) - elif 'example.com' in uri: - namespaces.setdefault('example.com', []).append(uri) - else: - namespaces.setdefault('other', []).append(uri) - - for ns, uris in namespaces.items(): - print(f"\n {ns}:") - for uri in uris[:3]: # Show first 3 - print(f" {uri}") - if len(uris) > 3: - print(f" ... and {len(uris) - 3} more") - - # Test get_context function - print(f"\n🎯 Testing get_context() function:") - context = get_context(g) - - print("📋 Generated Context:") - if isinstance(context, list): - for i, ctx_layer in enumerate(context): - if isinstance(ctx_layer, str): - print(f" Layer {i}: \"{ctx_layer}\"") - else: - print(f" Layer {i}:") - for prefix, uri in sorted(ctx_layer.items()): - print(f" \"{prefix}\": \"{uri}\"") - else: - print(f" Single context: {context}") - - # Analyze what happened - print(f"\n🧪 Analysis:") - detected_namespaces = set() - if isinstance(context, list) and len(context) > 1: - for ctx in context[1:]: - if isinstance(ctx, dict): - detected_namespaces.update(ctx.values()) - - test_namespaces = [ - ('pokemon.org', 'http://pokemon.org/'), - ('villains.org', 'http://villains.org/'), - ('schema.org', 'https://schema.org/'), - ('example.com', 'http://example.com/') - ] - - for ns_name, ns_uri in test_namespaces: - if ns_uri in detected_namespaces: - print(f" ✅ {ns_name}: DETECTED") - else: - print(f" ❌ {ns_name}: NOT DETECTED") - - print(f"\n🎮 Conclusion:") - unknown_detected = any(ns in detected_namespaces for _, ns in test_namespaces[:2]) - if unknown_detected: - print(f" 🎉 Unknown namespaces are automatically detected!") - else: - print(f" ❌ Unknown namespaces are NOT automatically detected") - print(f" ➡️ Only predefined namespaces in namespace_prefixes are recognized") - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_decorator_id.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_decorator_id.py deleted file mode 100644 index 8f56145..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_decorator_id.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -""" -Test the enhanced @ro_crate_schema decorator with explicit id parameter. -""" - -import sys -sys.path.append('src') - -from lib_ro_crate_schema.crate.schema_facade import SchemaFacade -from lib_ro_crate_schema.crate.decorators import ro_crate_schema, Field -from pydantic import BaseModel - -# Test the new 'id' parameter in the decorator -@ro_crate_schema( - id="CustomPerson", - ontology="https://schema.org/Person" -) -class PersonModel(BaseModel): - """A person model with explicit ID different from class name""" - name: str = Field(ontology="https://schema.org/name") - email: str = Field(ontology="https://schema.org/email") - -# Test without explicit ID (should default to class name) -@ro_crate_schema(ontology="https://schema.org/Dataset") -class DatasetModel(BaseModel): - """A dataset model without explicit ID""" - title: str = Field(ontology="https://schema.org/name") - description: str = Field(ontology="https://schema.org/description") - -def test_decorator_with_id(): - print("🧪 Testing @ro_crate_schema decorator with explicit id parameter...") - - # Create facade and add models - facade = SchemaFacade() - facade.add_all_registered_models() - - print("\n📊 Registered types:") - for type_obj in facade.get_types(): - print(f" - Type ID: '{type_obj.id}' (from class: {type_obj.__class__.__name__})") - - # Verify that PersonModel got the custom ID "CustomPerson" - person_type = facade.get_type("CustomPerson") - dataset_type = facade.get_type("DatasetModel") # Should use class name - - if person_type: - print(f"✅ Found PersonModel with custom ID: '{person_type.id}'") - else: - print("❌ PersonModel with custom ID not found") - - if dataset_type: - print(f"✅ Found DatasetModel with default ID: '{dataset_type.id}'") - else: - print("❌ DatasetModel with default ID not found") - - # Create instances and add them - person = PersonModel(name="Alice Johnson", email="alice@example.com") - dataset = DatasetModel(title="Test Dataset", description="A test dataset") - - facade.add_model_instance(person, "alice") - facade.add_model_instance(dataset, "test_dataset") - - print("\n📦 Metadata entries:") - for entry in facade.get_entries(): - print(f" - {entry.id} (class_id: {entry.class_id})") - - # Verify the entries use the correct type IDs - alice_entry = facade.get_entry("alice") - dataset_entry = facade.get_entry("test_dataset") - - if alice_entry and alice_entry.class_id == "CustomPerson": - print("✅ Alice entry correctly references 'CustomPerson' type") - else: - print(f"❌ Alice entry has wrong class_id: {alice_entry.class_id if alice_entry else 'None'}") - - if dataset_entry and dataset_entry.class_id == "DatasetModel": - print("✅ Dataset entry correctly references 'DatasetModel' type") - else: - print(f"❌ Dataset entry has wrong class_id: {dataset_entry.class_id if dataset_entry else 'None'}") - - # Export and verify - print("\n💾 Testing RO-Crate export...") - import os - output_dir = "output_crates" - os.makedirs(output_dir, exist_ok=True) - - test_output_path = os.path.join(output_dir, "test_decorator_id_output") - facade.write(test_output_path, name="Test ID Parameter") - print("✅ Export successful!") - - print("\n🎉 Test completed successfully!") - -if __name__ == "__main__": - test_decorator_with_id() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_duplicate_detection.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_duplicate_detection.py deleted file mode 100644 index e69de29..0000000 diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_duplicate_integration.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_duplicate_integration.py deleted file mode 100644 index e69de29..0000000 diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_export.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_export.py index 0569330..bf3eb11 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_export.py +++ b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_export.py @@ -1,57 +1,107 @@ -#!/usr/bin/env python3 +""" +Test RO-Crate export functionality. +Verifies that Pydantic models can be converted to RO-Crate format. +""" -import sys +import json +import tempfile from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent / "src")) -from datetime import datetime -from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel, Field +from lib_ro_crate_schema.crate.decorators import ro_crate_schema from lib_ro_crate_schema.crate.schema_facade import SchemaFacade -from lib_ro_crate_schema.crate.decorators import ro_crate_schema, Field +from lib_ro_crate_schema.crate.jsonld_utils import add_schema_to_crate +from rocrate.rocrate import ROCrate -@ro_crate_schema(ontology="http://openbis.org/Equipment") -class Equipment(BaseModel): - """Laboratory equipment with optional nesting""" - name: str = Field(ontology="https://schema.org/name") - model: str = Field(comment="Equipment model/version") - serial_number: str = Field(ontology="https://schema.org/serialNumber") - created_date: datetime = Field(ontology="https://schema.org/dateCreated") - parent_equipment: Optional['Equipment'] = Field(default=None, ontology="https://schema.org/isPartOf") -def test_export(): +@ro_crate_schema(ontology="https://schema.org/Person") +class Person(BaseModel): + """Person entity""" + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + email: str = Field(json_schema_extra={"ontology": "https://schema.org/email"}) + + +@ro_crate_schema(ontology="https://schema.org/Dataset") +class Dataset(BaseModel): + """Dataset entity""" + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + description: str = Field(json_schema_extra={"ontology": "https://schema.org/description"}) + + +def test_basic_export(): + """Test that we can export Pydantic models to RO-Crate""" + + # Create instances + person = Person(name="Test Person", email="test@example.org") + dataset = Dataset(name="Test Dataset", description="Test description") + + # Create facade facade = SchemaFacade() + facade.add_all_registered_models() + facade.add_model_instance(person, "test_person") + facade.add_model_instance(dataset, "test_dataset") - # Create parent equipment - parent = Equipment( - name="Parent Equipment", - model="P1", - serial_number="P001", - created_date=datetime(2023, 1, 1), - parent_equipment=None - ) + # Export to RO-Crate + with tempfile.TemporaryDirectory() as tmpdir: + output_path = Path(tmpdir) / "test_crate" + + crate = ROCrate() + crate.name = "Test Crate" + final_crate = add_schema_to_crate(facade, crate) + final_crate.write(output_path) + + # Verify output + metadata_file = output_path / "ro-crate-metadata.json" + assert metadata_file.exists(), "Metadata file not created" + + with open(metadata_file) as f: + metadata = json.load(f) + + assert "@context" in metadata + assert "@graph" in metadata + assert len(metadata["@graph"]) > 0 + + print("✓ Export test passed") + + +def test_json_ld_structure(): + """Test that exported RO-Crate has valid JSON-LD structure""" - # Create child equipment with parent reference - child = Equipment( - name="Child Equipment", - model="C1", - serial_number="C001", - created_date=datetime(2023, 2, 1), - parent_equipment=parent - ) + person = Person(name="Alice", email="alice@example.org") - # Add to facade - facade.add_model_instance(parent, "base:parent") - facade.add_model_instance(child, "base:child") + facade = SchemaFacade() + facade.add_all_registered_models() + facade.add_model_instance(person, "alice") - # Export - import os - output_dir = "output_crates" - os.makedirs(output_dir, exist_ok=True) - test_output_path = os.path.join(output_dir, "test_simple") - - facade.write(test_output_path, "Simple Test", "Testing reference export") - print(f"Export completed - check {test_output_path}/ro-crate-metadata.json") + with tempfile.TemporaryDirectory() as tmpdir: + output_path = Path(tmpdir) / "test_crate" + + crate = ROCrate() + final_crate = add_schema_to_crate(facade, crate) + final_crate.write(output_path) + + with open(output_path / "ro-crate-metadata.json") as f: + metadata = json.load(f) + + # Check JSON-LD structure + assert isinstance(metadata["@context"], (str, list, dict)) + assert isinstance(metadata["@graph"], list) + + # Check for entity types + types = set() + for entity in metadata["@graph"]: + entity_type = entity.get("@type") + if isinstance(entity_type, list): + types.update(entity_type) + elif entity_type: + types.add(entity_type) + + assert "Person" in types or any("Person" in t for t in types) + + print("✓ JSON-LD structure test passed") + if __name__ == "__main__": - test_export() \ No newline at end of file + test_basic_export() + test_json_ld_structure() + print("\n✅ All export tests passed!") diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_get_crate.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_get_crate.py deleted file mode 100644 index 8ced6aa..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_get_crate.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 - -""" -Test the refactored get_crate method to ensure it works independently. -""" - -import sys -sys.path.append('src') - -from lib_ro_crate_schema.crate.schema_facade import SchemaFacade -from lib_ro_crate_schema.crate.metadata_entry import MetadataEntry -from lib_ro_crate_schema.crate.type import Type -from lib_ro_crate_schema.crate.type_property import TypeProperty -from lib_ro_crate_schema.crate.restriction import Restriction - -def test_get_crate_method(): - print("🧪 Testing get_crate method...") - - # Create a simple schema - facade = SchemaFacade() - - # Add a simple type with a property - name_prop = TypeProperty( - id="name", - range_includes=["http://www.w3.org/2001/XMLSchema#string"], - required=True - ) - - person_type = Type( - id="Person", - rdfs_property=[name_prop], - comment="A person entity" - ) - - facade.addType(person_type) - - # Add a metadata entry - person_entry = MetadataEntry( - id="john_doe", - class_id="Person", - properties={"name": "John Doe"} - ) - - facade.addEntry(person_entry) - - # Test get_crate method - print("📦 Testing get_crate method...") - crate = facade.get_crate( - name="Test RO-Crate", - description="A test crate created using get_crate method" - ) - - print(f"✅ Created crate: {crate}") - print(f"✅ Crate name: {getattr(crate, 'name', 'Not set')}") - print(f"✅ Crate description: {getattr(crate, 'description', 'Not set')}") - - # Test that the crate can be written - print("💾 Testing crate writing...") - import os - output_dir = "output_crates" - os.makedirs(output_dir, exist_ok=True) - - test_get_crate_path = os.path.join(output_dir, "test_get_crate_output") - crate.write(test_get_crate_path) - print(f"✅ Crate written successfully to '{test_get_crate_path}'") - - # Test that write method still works (using get_crate internally) - print("💾 Testing write method (should use get_crate internally)...") - test_write_path = os.path.join(output_dir, "test_write_output") - facade.write(test_write_path, name="Test via Write", description="Using write method") - print("✅ Write method works correctly") - - print("🎉 All tests passed!") - -if __name__ == "__main__": - test_get_crate_method() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_import.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_import.py new file mode 100644 index 0000000..bc1352f --- /dev/null +++ b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_import.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent / "src")) + +from datetime import datetime +from typing import Optional +from pydantic import BaseModel +from lib_ro_crate_schema.crate.schema_facade import SchemaFacade +from lib_ro_crate_schema.crate.decorators import ro_crate_schema, Field + +@ro_crate_schema(ontology="http://openbis.org/Equipment") +class Equipment(BaseModel): + """Laboratory equipment with optional nesting""" + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + model: str = Field(json_schema_extra={"comment": "Equipment model/version"}) + serial_number: str = Field(json_schema_extra={"ontology": "https://schema.org/serialNumber"}) + created_date: datetime = Field(json_schema_extra={"ontology": "https://schema.org/dateCreated"}) + parent_equipment: Optional['Equipment'] = Field(default=None, json_schema_extra={"ontology": "https://schema.org/isPartOf"}) + +def test_export(): + facade = SchemaFacade() + + # Create parent equipment + parent = Equipment( + name="Parent Equipment", + model="P1", + serial_number="P001", + created_date=datetime(2023, 1, 1), + parent_equipment=None + ) + + # Create child equipment with parent reference + child = Equipment( + name="Child Equipment", + model="C1", + serial_number="C001", + created_date=datetime(2023, 2, 1), + parent_equipment=parent + ) + + # Add to facade + facade.add_model_instance(parent, "base:parent") + facade.add_model_instance(child, "base:child") + + # Export + import os + output_dir = "output_crates" + os.makedirs(output_dir, exist_ok=True) + test_output_path = os.path.join(output_dir, "test_simple") + + facade.write(test_output_path, "Simple Test", "Testing reference export") + print(f"Export completed - check {test_output_path}/ro-crate-metadata.json") + +if __name__ == "__main__": + test_export() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_integration.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_integration.py deleted file mode 100644 index 6157752..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_integration.py +++ /dev/null @@ -1,400 +0,0 @@ -import unittest -import sys -import json -import tempfile -from pathlib import Path - -from lib_ro_crate_schema.crate.schema_facade import SchemaFacade -from lib_ro_crate_schema.crate.type import Type -from lib_ro_crate_schema.crate.type_property import TypeProperty -from lib_ro_crate_schema.crate.literal_type import LiteralType -from lib_ro_crate_schema.crate.metadata_entry import MetadataEntry - - -class TestIntegrationExamples(unittest.TestCase): - """Integration tests using real examples from the codebase""" - - def setUp(self): - """Set up paths to example files""" - self.test_dir = Path(__file__).parent - self.examples_dir = self.test_dir.parent.parent / "examples" - self.lib_dir = self.test_dir.parent - self.obenbis_crate = self.lib_dir.parent.parent / "example" / "obenbis-one-publication" / "ro-crate-metadata.json" - - def test_examples_py_recreation(self): - """Test recreating the example from examples.py""" - - # Recreate the example schema from examples.py - name = TypeProperty( - id="name", - range_includes=[LiteralType.STRING], - required=True, - label="Full Name", - comment="The full name of the entity" - ) - - identifier = TypeProperty( - id="identifier", - range_includes=[LiteralType.STRING], - required=True, - label="Identifier", - comment="Unique identifier for the entity" - ) - - colleague = TypeProperty( - id="colleague", - range_includes=["Participant"], - required=False, - label="Colleague", - comment="Optional colleague relationship" - ) - - participant_type = Type( - id="Participant", - subclass_of=["https://schema.org/Thing"], - ontological_annotations=["http://purl.org/dc/terms/creator"], - rdfs_property=[name, identifier], - comment="A participant in the research", - label="Participant", - ) - - creator_type = Type( - id="Creator", - subclass_of=["https://schema.org/Thing"], - ontological_annotations=["http://purl.org/dc/terms/creator"], - rdfs_property=[name, identifier, colleague], - comment="A creator of the research work", - label="Creator", - ) - - creator_entry = MetadataEntry( - id="creator1", - class_id="Creator", - properties={ - "name": "John Author", - "identifier": "https://orcid.org/0000-0000-0000-0000", - }, - references={}, - ) - - participant_entry = MetadataEntry( - id="participant", - class_id="Participant", - properties={ - "name": "Karl Participant", - "identifier": "https://orcid.org/0000-0000-0000-0001", - }, - references={ - "colleague": ["creator1"] - }, - ) - - schema = SchemaFacade( - types=[creator_type, participant_type], - metadata_entries=[creator_entry, participant_entry], - ) - - # Test the schema - self.assertEqual(len(schema.types), 2) - self.assertEqual(len(schema.metadata_entries), 2) - - # Test types - creator = schema.get_type("Creator") - self.assertIsNotNone(creator) - self.assertEqual(creator.label, "Creator") - self.assertEqual(len(creator.rdfs_property), 3) # name, identifier, colleague - - participant = schema.get_type("Participant") - self.assertIsNotNone(participant) - self.assertEqual(participant.label, "Participant") - self.assertEqual(len(participant.rdfs_property), 2) # name, identifier - - # Test metadata entries - creator_md = schema.get_entry("creator1") - self.assertIsNotNone(creator_md) - self.assertEqual(creator_md.properties["name"], "John Author") - - participant_md = schema.get_entry("participant") - self.assertIsNotNone(participant_md) - self.assertEqual(participant_md.references["colleague"], ["creator1"]) - - # Test triple generation - triples = list(schema.to_triples()) - self.assertGreater(len(triples), 0) - - # Test JSON generation - json_data = schema.to_json() - self.assertIn("@context", json_data) - self.assertIn("@graph", json_data) - - def test_obenbis_import(self): - """Test importing the OpenBIS one-publication RO-Crate""" - - if not self.obenbis_crate.exists(): - self.skipTest(f"OpenBIS example file not found at {self.obenbis_crate}") - - # Import the OpenBIS RO-Crate - facade = SchemaFacade.from_ro_crate(self.obenbis_crate) - - # Test that import was successful - self.assertIsNotNone(facade) - - # Should have imported some types and/or metadata entries - total_items = len(facade.types) + len(facade.metadata_entries) - self.assertGreater(total_items, 0, "Should have imported some schema elements") - - # Test that we can generate JSON-LD from imported data - json_data = facade.to_json() - self.assertIn("@context", json_data) - self.assertIn("@graph", json_data) - - # Test that we can generate triples - triples = list(facade.to_triples()) - self.assertGreater(len(triples), 0, "Should generate RDF triples") - - print(f"Imported facade with {len(facade.types)} types and {len(facade.metadata_entries)} metadata entries") - - # If we have types, test they have proper structure - if facade.types: - first_type = facade.types[0] - self.assertIsNotNone(first_type.id) - print(f"First imported type: {first_type.id}") - - # If we have metadata entries, test they have proper structure - if facade.metadata_entries: - first_entry = facade.metadata_entries[0] - self.assertIsNotNone(first_entry.id) - self.assertIsNotNone(first_entry.class_id) - print(f"First imported entry: {first_entry.id} of type {first_entry.class_id}") - - def test_obenbis_structure_analysis(self): - """Test analyzing the structure of the OpenBIS RO-Crate""" - - if not self.obenbis_crate.exists(): - self.skipTest(f"OpenBIS example file not found at {self.obenbis_crate}") - - # Read raw JSON to analyze structure - with open(self.obenbis_crate, 'r') as f: - crate_data = json.load(f) - - self.assertIn("@graph", crate_data) - graph = crate_data["@graph"] - - # Analyze what types of entities are in the crate - entity_types = {} - rdfs_classes = [] - rdf_properties = [] - owl_restrictions = [] - metadata_entities = [] - - for item in graph: - item_type = item.get("@type", "Unknown") - item_id = item.get("@id", "") - - if item_type == "rdfs:Class": - rdfs_classes.append(item_id) - elif item_type in ["rdf:Property", "rdfs:Property"]: - rdf_properties.append(item_id) - elif item_type == "owl:Restriction": - owl_restrictions.append(item_id) - elif item_id not in ["./", "ro-crate-metadata.json"]: - metadata_entities.append((item_id, item_type)) - - # Count entity types - if item_type in entity_types: - entity_types[item_type] += 1 - else: - entity_types[item_type] = 1 - - print("\nOpenBIS RO-Crate structure analysis:") - print(f"Total entities: {len(graph)}") - print(f"RDFS Classes: {len(rdfs_classes)}") - print(f"RDF Properties: {len(rdf_properties)}") - print(f"OWL Restrictions: {len(owl_restrictions)}") - print(f"Metadata entities: {len(metadata_entities)}") - - print("\nEntity type distribution:") - for entity_type, count in sorted(entity_types.items()): - print(f" {entity_type}: {count}") - - # Test that the structure makes sense - self.assertGreater(len(graph), 0, "Should have entities in the graph") - - if rdfs_classes: - print(f"\nSample RDFS Classes: {rdfs_classes[:5]}") - if rdf_properties: - print(f"Sample RDF Properties: {rdf_properties[:5]}") - if metadata_entities: - print(f"Sample Metadata Entities: {[f'{id} ({type})' for id, type in metadata_entities[:5]]}") - - def test_create_minimal_example(self): - """Test creating a minimal working example similar to examples.py""" - - # Create a minimal Person schema - name_prop = TypeProperty( - id="name", - range_includes=[LiteralType.STRING], - required=True, - label="Name" - ) - - email_prop = TypeProperty( - id="email", - range_includes=[LiteralType.STRING], - required=False, - label="Email" - ) - - person_type = Type( - id="Person", - rdfs_property=[name_prop, email_prop], - label="Person", - comment="A person entity" - ) - - # Create a person instance - person_instance = MetadataEntry( - id="john_doe", - class_id="Person", - properties={ - "name": "John Doe", - "email": "john@example.com" - } - ) - - # Create facade - facade = SchemaFacade( - types=[person_type], - metadata_entries=[person_instance] - ) - - # Test basic functionality - self.assertEqual(len(facade.types), 1) - self.assertEqual(len(facade.metadata_entries), 1) - - # Test export to temporary directory - with tempfile.TemporaryDirectory() as temp_dir: - facade.write( - temp_dir, - name="Minimal Example", - description="A minimal RO-Crate example", - license="CC0" - ) - - # Verify files were created - metadata_file = Path(temp_dir) / "ro-crate-metadata.json" - self.assertTrue(metadata_file.exists()) - - # Verify the JSON structure - with open(metadata_file, 'r') as f: - exported_data = json.load(f) - - self.assertIn("@context", exported_data) - self.assertIn("@graph", exported_data) - - # Check that our Person type and instance are included - graph = exported_data["@graph"] - - person_class_found = any( - (item.get("@id") in ["Person", "base:Person", "http://example.com/Person"]) and item.get("@type") == "rdfs:Class" - for item in graph - ) - self.assertTrue(person_class_found, "Should export Person class") - - person_instance_found = any( - (item.get("@id") in ["john_doe", "base:john_doe", "http://example.com/john_doe"]) and - item.get("@type") in ["Person", "base:Person", "http://example.com/Person"] - for item in graph - ) - self.assertTrue(person_instance_found, "Should export person instance") - - print(f"\nMinimal example exported with {len(graph)} entities") - - def test_complex_relationship_example(self): - """Test creating example with complex relationships between entities""" - - # Define properties - name_prop = TypeProperty(id="name", range_includes=[LiteralType.STRING], required=True) - title_prop = TypeProperty(id="title", range_includes=[LiteralType.STRING], required=True) - author_prop = TypeProperty(id="author", range_includes=["Person"], required=True) - publisher_prop = TypeProperty(id="publisher", range_includes=["Organization"], required=False) - - # Define types - person_type = Type( - id="Person", - rdfs_property=[name_prop], - label="Person" - ) - - organization_type = Type( - id="Organization", - rdfs_property=[name_prop], - label="Organization" - ) - - article_type = Type( - id="Article", - rdfs_property=[title_prop, author_prop, publisher_prop], - label="Article" - ) - - # Create instances - author = MetadataEntry( - id="author1", - class_id="Person", - properties={"name": "Dr. Jane Smith"} - ) - - publisher = MetadataEntry( - id="pub1", - class_id="Organization", - properties={"name": "Academic Press"} - ) - - article = MetadataEntry( - id="article1", - class_id="Article", - properties={"title": "Advanced RO-Crate Techniques"}, - references={ - "author": ["author1"], - "publisher": ["pub1"] - } - ) - - # Create facade - facade = SchemaFacade( - types=[person_type, organization_type, article_type], - metadata_entries=[author, publisher, article] - ) - - # Test relationships - self.assertEqual(len(facade.types), 3) - self.assertEqual(len(facade.metadata_entries), 3) - - # Test that references work correctly - article_entry = facade.get_entry("article1") - self.assertIn("author1", article_entry.references["author"]) - self.assertIn("pub1", article_entry.references["publisher"]) - - # Test triple generation includes relationships - triples = list(facade.to_triples()) - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Should have triples linking article to author and publisher - author_ref_found = any( - "article1" in triple[0] and "author" in triple[1] and "author1" in triple[2] - for triple in triple_strs - ) - self.assertTrue(author_ref_found, "Should generate author reference triple") - - publisher_ref_found = any( - "article1" in triple[0] and "publisher" in triple[1] and "pub1" in triple[2] - for triple in triple_strs - ) - self.assertTrue(publisher_ref_found, "Should generate publisher reference triple") - - print(f"\nComplex relationship example generated {len(triples)} triples") - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_metadata_entry.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_metadata_entry.py deleted file mode 100644 index c3ced30..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_metadata_entry.py +++ /dev/null @@ -1,272 +0,0 @@ -import unittest -import sys -from pathlib import Path -from datetime import datetime - -# Add source to path -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from lib_ro_crate_schema.crate.metadata_entry import MetadataEntry -from rdflib import URIRef, RDF, Literal - - -class TestMetadataEntry(unittest.TestCase): - """Test cases for the MetadataEntry class""" - - def setUp(self): - """Set up test fixtures""" - self.basic_entry = MetadataEntry( - id="basic_entry", - class_id="BasicClass" - ) - - self.complete_entry = MetadataEntry( - id="person1", - class_id="Person", - properties={ - "name": "John Doe", - "age": 30, - "active": True - }, - references={ - "knows": ["person2", "person3"], - "worksFor": ["organization1"] - } - ) - - self.datetime_entry = MetadataEntry( - id="event1", - class_id="Event", - properties={ - "title": "Important Meeting", - "startTime": datetime(2023, 12, 25, 14, 30, 0) - } - ) - - def test_metadata_entry_creation(self): - """Test basic MetadataEntry object creation""" - self.assertEqual(self.basic_entry.id, "basic_entry") - self.assertEqual(self.basic_entry.class_id, "BasicClass") - self.assertEqual(self.basic_entry.properties, {}) - self.assertEqual(self.basic_entry.references, {}) - - def test_complete_entry_properties(self): - """Test entry with complete properties and references""" - self.assertEqual(self.complete_entry.id, "person1") - self.assertEqual(self.complete_entry.class_id, "Person") - - # Check properties - self.assertEqual(self.complete_entry.properties["name"], "John Doe") - self.assertEqual(self.complete_entry.properties["age"], 30) - self.assertEqual(self.complete_entry.properties["active"], True) - - # Check references - self.assertEqual(self.complete_entry.references["knows"], ["person2", "person3"]) - self.assertEqual(self.complete_entry.references["worksFor"], ["organization1"]) - - def test_java_api_compatibility(self): - """Test Java API compatibility methods""" - self.assertEqual(self.complete_entry.getId(), "person1") - self.assertEqual(self.complete_entry.getClassId(), "Person") - - values = self.complete_entry.getValues() - self.assertEqual(values["name"], "John Doe") - self.assertEqual(values["age"], 30) - - references = self.complete_entry.getReferences() - self.assertEqual(references["knows"], ["person2", "person3"]) - - # Test alias method - self.assertEqual(self.complete_entry.get_values(), self.complete_entry.properties) - - def test_to_triples(self): - """Test RDF triple generation""" - triples = list(self.complete_entry.to_triples()) - - # Should generate multiple triples - self.assertGreater(len(triples), 0) - - # Convert to string representation for easier testing - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Check for type declaration - type_triple_found = any("Person" in triple[2] for triple in triple_strs) - self.assertTrue(type_triple_found, "Should generate class type triple") - - # Check for properties - name_triple_found = any("name" in triple[1] and "John Doe" in triple[2] for triple in triple_strs) - self.assertTrue(name_triple_found, "Should generate property triples") - - age_triple_found = any("age" in triple[1] and "30" in triple[2] for triple in triple_strs) - self.assertTrue(age_triple_found, "Should generate age property triple") - - def test_datetime_handling(self): - """Test handling of datetime objects in properties""" - triples = list(self.datetime_entry.to_triples()) - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Datetime should be converted to ISO format string - datetime_found = any("startTime" in triple[1] and "2023-12-25T14:30:00" in triple[2] for triple in triple_strs) - self.assertTrue(datetime_found, "Should convert datetime to ISO string") - - def test_reference_triples(self): - """Test reference generation in triples""" - triples = list(self.complete_entry.to_triples()) - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Check for reference triples (no Literal wrapper for references) - knows_ref_found = any("knows" in triple[1] and "person2" in triple[2] for triple in triple_strs) - self.assertTrue(knows_ref_found, "Should generate reference triples") - - works_for_ref_found = any("worksFor" in triple[1] and "organization1" in triple[2] for triple in triple_strs) - self.assertTrue(works_for_ref_found, "Should generate worksFor reference") - - def test_empty_entry_triples(self): - """Test triple generation for entry with no properties or references""" - empty_entry = MetadataEntry(id="empty", class_id="EmptyClass") - triples = list(empty_entry.to_triples()) - - # Should at least generate the type declaration - self.assertGreater(len(triples), 0) - - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - type_found = any("EmptyClass" in triple[2] for triple in triple_strs) - self.assertTrue(type_found, "Should generate type declaration even for empty entry") - - def test_mixed_property_types(self): - """Test entry with various property value types""" - mixed_entry = MetadataEntry( - id="mixed", - class_id="MixedType", - properties={ - "string_prop": "text value", - "int_prop": 42, - "float_prop": 3.14, - "bool_prop": False, - "none_prop": None # Should be filtered out - } - ) - - triples = list(mixed_entry.to_triples()) - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Check each type is properly handled - string_found = any("string_prop" in triple[1] and "text value" in triple[2] for triple in triple_strs) - int_found = any("int_prop" in triple[1] and "42" in triple[2] for triple in triple_strs) - float_found = any("float_prop" in triple[1] and "3.14" in triple[2] for triple in triple_strs) - bool_found = any("bool_prop" in triple[1] and "false" in triple[2] for triple in triple_strs) - - self.assertTrue(string_found, "Should handle string properties") - self.assertTrue(int_found, "Should handle integer properties") - self.assertTrue(float_found, "Should handle float properties") - self.assertTrue(bool_found, "Should handle boolean properties") - - # None properties should not generate triples (filtered out in actual implementation) - none_found = any("none_prop" in triple[1] for triple in triple_strs) - # Note: The current implementation might include None values, - # but ideally they should be filtered out - - def test_multiple_references_same_property(self): - """Test property with multiple reference values""" - multi_ref_entry = MetadataEntry( - id="multi_ref", - class_id="MultiRef", - references={ - "collaborator": ["person1", "person2", "person3"] - } - ) - - triples = list(multi_ref_entry.to_triples()) - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Should generate separate triples for each reference - collab1_found = any("collaborator" in triple[1] and "person1" in triple[2] for triple in triple_strs) - collab2_found = any("collaborator" in triple[1] and "person2" in triple[2] for triple in triple_strs) - collab3_found = any("collaborator" in triple[1] and "person3" in triple[2] for triple in triple_strs) - - self.assertTrue(collab1_found, "Should generate triple for person1") - self.assertTrue(collab2_found, "Should generate triple for person2") - self.assertTrue(collab3_found, "Should generate triple for person3") - - - def test_id_and_class_id_validation(self): - """Test that id and class_id are properly set and accessible""" - entry = MetadataEntry(id="test_id", class_id="TestClass") - - # Direct access - self.assertEqual(entry.id, "test_id") - self.assertEqual(entry.class_id, "TestClass") - - # Java API access - self.assertEqual(entry.getId(), "test_id") - self.assertEqual(entry.getClassId(), "TestClass") - - - def test_get_entry_as_compatibility(self): - """Test the get_entry_as method for SchemaFacade compatibility""" - # This test verifies that MetadataEntry objects work with the new get_entry_as method - from lib_ro_crate_schema.crate.schema_facade import SchemaFacade - from pydantic import BaseModel - from typing import Optional - - # Create a simple test model - class TestPerson(BaseModel): - name: str - age: Optional[int] = None - active: Optional[bool] = None - - # Create a facade and add our test entry - facade = SchemaFacade() - facade.addEntry(self.complete_entry) - - # Test conversion to our test model - person_instance = facade.get_entry_as("person1", TestPerson) - - self.assertIsNotNone(person_instance) - self.assertIsInstance(person_instance, TestPerson) - self.assertEqual(person_instance.name, "John Doe") - self.assertEqual(person_instance.age, 30) - self.assertEqual(person_instance.active, True) - - # Test with non-existent entry - none_result = facade.get_entry_as("nonexistent", TestPerson) - self.assertIsNone(none_result) - - def test_get_entry_as_with_references(self): - """Test get_entry_as handling of references""" - from lib_ro_crate_schema.crate.schema_facade import SchemaFacade - from pydantic import BaseModel - from typing import Optional, List - - class TestOrganization(BaseModel): - name: str - - class TestPersonWithRefs(BaseModel): - name: str - age: Optional[int] = None - knows: Optional[List[str]] = None # Keep as strings for this test - worksFor: Optional[str] = None # Single reference as string - - # Create facade and add entries - facade = SchemaFacade() - facade.addEntry(self.complete_entry) - - # Add a referenced organization entry - org_entry = MetadataEntry( - id="organization1", - class_id="Organization", - properties={"name": "Tech Corp"} - ) - facade.addEntry(org_entry) - - # Test conversion - person = facade.get_entry_as("person1", TestPersonWithRefs) - - self.assertIsNotNone(person) - self.assertEqual(person.name, "John Doe") - self.assertEqual(person.age, 30) - self.assertEqual(person.knows, ["person2", "person3"]) # References as IDs - self.assertEqual(person.worksFor, "organization1") # Single reference as ID - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_published_package.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_published_package.py new file mode 100644 index 0000000..0d4229d --- /dev/null +++ b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_published_package.py @@ -0,0 +1,205 @@ +""" +Test that verifies the published package can be installed and used from TestPyPI. + +This test creates an isolated environment, installs the package from TestPyPI, +and runs a quickstart-style example to ensure everything works as expected. +""" + +import subprocess +import sys +import tempfile +import shutil +from pathlib import Path + + +def test_install_from_testpypi(): + """ + Test that the package can be installed from TestPyPI and basic functionality works. + + This test: + 1. Creates a temporary virtual environment + 2. Installs the package from TestPyPI + 3. Runs a simple example similar to quickstart + 4. Verifies the output is correct + """ + + # Create a temporary directory for our test environment + with tempfile.TemporaryDirectory() as tmpdir: + venv_path = Path(tmpdir) / "test_venv" + + print(f"\n{'='*60}") + print("Testing Published Package from TestPyPI") + print(f"{'='*60}") + + # Step 1: Create virtual environment + print("\n1. Creating virtual environment...") + result = subprocess.run( + [sys.executable, "-m", "venv", str(venv_path)], + capture_output=True, + text=True + ) + assert result.returncode == 0, f"Failed to create venv: {result.stderr}" + print(" ✓ Virtual environment created") + + # Determine pip executable path + if sys.platform == "win32": + pip_path = venv_path / "Scripts" / "pip" + python_path = venv_path / "Scripts" / "python" + else: + pip_path = venv_path / "bin" / "pip" + python_path = venv_path / "bin" / "python" + + # Step 2: Install from TestPyPI + print("\n2. Installing lib-ro-crate-schema from TestPyPI...") + print(" Note: Package may take a few minutes to be indexed on TestPyPI") + + # Try with the specific version (0.2.0 is available on TestPyPI) + result = subprocess.run( + [ + str(pip_path), + "install", + "--index-url", "https://test.pypi.org/simple/", + "--extra-index-url", "https://pypi.org/simple/", + "lib-ro-crate-schema==0.2.0" + ], + capture_output=True, + text=True, + timeout=120 + ) + + if result.returncode != 0: + print(f" ✗ Installation failed!") + print(f" stdout: {result.stdout}") + print(f" stderr: {result.stderr}") + assert False, "Failed to install package from TestPyPI" + + print(" ✓ Package installed successfully") + + # Step 3: Create a test script similar to quickstart + print("\n3. Creating test script...") + test_script = venv_path / "test_quickstart.py" + test_script.write_text(""" +# Test script that mimics the quickstart example +from pydantic import BaseModel, Field +from lib_ro_crate_schema.crate.decorators import ro_crate_schema +from lib_ro_crate_schema.crate.schema_facade import SchemaFacade +import json +from pathlib import Path +import tempfile + +# Define a schema using decorators +@ro_crate_schema(ontology="https://schema.org/Person") +class Person(BaseModel): + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + email: str = Field(json_schema_extra={"ontology": "https://schema.org/email"}) + +@ro_crate_schema(ontology="https://schema.org/Organization") +class Organization(BaseModel): + name: str = Field(json_schema_extra={"ontology": "https://schema.org/name"}) + url: str = Field(json_schema_extra={"ontology": "https://schema.org/url"}) + +# Create instances +person = Person(name="Alice Researcher", email="alice@example.org") +org = Organization(name="Research Institute", url="https://example.org") + +# Create RO-Crate using the actual API +with tempfile.TemporaryDirectory() as tmpdir: + output_path = Path(tmpdir) / "test_crate" + + # Create facade and add registered models + facade = SchemaFacade() + facade.add_all_registered_models() + + # Add model instances + facade.add_model_instance(person, "alice_researcher") + facade.add_model_instance(org, "research_institute") + + # Export using add_schema_to_crate + from lib_ro_crate_schema.crate.jsonld_utils import add_schema_to_crate + from rocrate.rocrate import ROCrate + + crate = ROCrate() + crate.name = "Test RO-Crate" + crate.description = "Testing published package" + final_crate = add_schema_to_crate(facade, crate) + final_crate.write(output_path) + + # Verify the crate was created + metadata_file = output_path / "ro-crate-metadata.json" + assert metadata_file.exists(), "ro-crate-metadata.json not created" + + # Load and verify content + with open(metadata_file) as f: + metadata = json.load(f) + + # Verify basic structure + assert "@context" in metadata, "Missing @context" + assert "@graph" in metadata, "Missing @graph" + + # Count entities + graph = metadata["@graph"] + + # Look for our entity types + type_counts = {} + for item in graph: + entity_type = item.get("@type", "Unknown") + if isinstance(entity_type, list): + for t in entity_type: + type_counts[t] = type_counts.get(t, 0) + 1 + else: + type_counts[entity_type] = type_counts.get(entity_type, 0) + 1 + + # Verify we have our custom types + has_person = "Person" in type_counts + has_org = "Organization" in type_counts + + assert has_person or has_org, "No custom entity types found" + + print("SUCCESS: All checks passed!") + print(f"- Created RO-Crate with {len(graph)} entities") + print(f"- Entity types: {', '.join(f'{k}: {v}' for k, v in sorted(type_counts.items()))}") + if has_person: + print(f"- ✓ Person entities found") + if has_org: + print(f"- ✓ Organization entities found") +""") + print(" ✓ Test script created") + + # Step 4: Run the test script + print("\n4. Running test script...") + result = subprocess.run( + [str(python_path), str(test_script)], + capture_output=True, + text=True, + timeout=60 + ) + + print("\n--- Test Script Output ---") + print(result.stdout) + if result.stderr: + print("--- Stderr ---") + print(result.stderr) + print("-------------------------\n") + + if result.returncode != 0: + print(" ✗ Test script failed!") + assert False, f"Test script execution failed with code {result.returncode}" + + # Verify success message + assert "SUCCESS: All checks passed!" in result.stdout, "Test did not complete successfully" + print(" ✓ Test script passed all checks") + + print(f"\n{'='*60}") + print("✓ All Tests Passed!") + print(f"{'='*60}") + print("\nThe published package on TestPyPI works correctly:") + print("- Installation successful") + print("- Import successful") + print("- Decorator pattern works") + print("- RO-Crate creation works") + print("- Export functionality works") + print("- Metadata validation passed") + + +if __name__ == "__main__": + test_install_from_testpypi() diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_pydantic_export.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_pydantic_export.py deleted file mode 100644 index 6a29650..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_pydantic_export.py +++ /dev/null @@ -1,209 +0,0 @@ -""" -Test suite for Pydantic model export functionality in SchemaFacade. -""" - -import unittest -import sys -from pathlib import Path -from typing import List, Optional - -# Add src to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from lib_ro_crate_schema.crate.schema_facade import SchemaFacade -from lib_ro_crate_schema.crate.type import Type -from lib_ro_crate_schema.crate.type_property import TypeProperty -from lib_ro_crate_schema.crate.restriction import Restriction -from pydantic import BaseModel, ValidationError - - -class TestPydanticExport(unittest.TestCase): - """Test Pydantic model export functionality""" - - def setUp(self): - """Set up test fixtures""" - self.facade = SchemaFacade() - - # Create a simple Person type - person_name_prop = TypeProperty( - id="name", - label="Name", - comment="Person's name", - range_includes=["http://www.w3.org/2001/XMLSchema#string"], - required=True - ) - - person_age_prop = TypeProperty( - id="age", - label="Age", - comment="Age in years", - range_includes=["http://www.w3.org/2001/XMLSchema#integer"], - required=False - ) - - person_type = Type( - id="Person", - label="Person", - comment="A person", - rdfs_property=[person_name_prop, person_age_prop], - restrictions=[ - Restriction(property_type="name", min_cardinality=1, max_cardinality=1), - Restriction(property_type="age", min_cardinality=0, max_cardinality=1) - ] - ) - - self.facade.addType(person_type) - - def test_export_single_model(self): - """Test exporting a single model""" - PersonModel = self.facade.export_pydantic_model("Person") - - # Check class properties - self.assertEqual(PersonModel.__name__, "Person") - self.assertIn("name", PersonModel.__annotations__) - self.assertIn("age", PersonModel.__annotations__) - - # Test instance creation - person = PersonModel(name="Alice") - self.assertEqual(person.name, "Alice") - self.assertIsNone(person.age) - - # Test validation - with self.assertRaises(ValidationError): - PersonModel() # Missing required 'name' - - def test_export_all_models(self): - """Test exporting all models""" - models = self.facade.export_all_pydantic_models() - - self.assertIn("Person", models) - PersonModel = models["Person"] - - # Test functionality - person = PersonModel(name="Bob", age=30) - self.assertEqual(person.name, "Bob") - self.assertEqual(person.age, 30) - - def test_type_mapping(self): - """Test RDF type to Python type mapping""" - # Test different data types - string_type = self.facade._rdf_type_to_python_type(["http://www.w3.org/2001/XMLSchema#string"]) - self.assertEqual(string_type, str) - - int_type = self.facade._rdf_type_to_python_type(["http://www.w3.org/2001/XMLSchema#integer"]) - self.assertEqual(int_type, int) - - bool_type = self.facade._rdf_type_to_python_type(["http://www.w3.org/2001/XMLSchema#boolean"]) - self.assertEqual(bool_type, bool) - - # Test schema.org types - schema_text = self.facade._rdf_type_to_python_type(["https://schema.org/Text"]) - self.assertEqual(schema_text, str) - - def test_field_requirements(self): - """Test field requirement detection from restrictions""" - person_type = self.facade.get_type("Person") - - # name should be required (minCardinality: 1) - self.assertTrue(self.facade._is_field_required(person_type, "name")) - - # age should be optional (minCardinality: 0) - self.assertFalse(self.facade._is_field_required(person_type, "age")) - - def test_list_fields(self): - """Test list field detection""" - # Add a type with list property - list_prop = TypeProperty( - id="tags", - label="Tags", - range_includes=["http://www.w3.org/2001/XMLSchema#string"] - ) - - list_type = Type( - id="TaggedItem", - rdfs_property=[list_prop], - restrictions=[ - Restriction(property_type="tags", min_cardinality=0, max_cardinality=None) # Unbounded - ] - ) - - self.facade.addType(list_type) - - # Test list detection - self.assertTrue(self.facade._is_field_list(list_type, "tags")) - - # Export and test - TaggedModel = self.facade.export_pydantic_model("TaggedItem") - tagged = TaggedModel(tags=["tag1", "tag2"]) - self.assertEqual(tagged.tags, ["tag1", "tag2"]) - - def test_forward_references(self): - """Test forward references between models""" - # Add Organization type that references Person - org_name_prop = TypeProperty( - id="name", - label="Organization Name", - range_includes=["http://www.w3.org/2001/XMLSchema#string"] - ) - - org_members_prop = TypeProperty( - id="members", - label="Members", - range_includes=["Person"] # Forward reference - ) - - org_type = Type( - id="Organization", - rdfs_property=[org_name_prop, org_members_prop], - restrictions=[ - Restriction(property_type="name", min_cardinality=1, max_cardinality=1), - Restriction(property_type="members", min_cardinality=0, max_cardinality=None) - ] - ) - - self.facade.addType(org_type) - - # Export all models (should handle forward references) - models = self.facade.export_all_pydantic_models() - - # Test that both models were created - self.assertIn("Person", models) - self.assertIn("Organization", models) - - # Test basic functionality (forward ref might not work perfectly but shouldn't crash) - OrgModel = models["Organization"] - org = OrgModel(name="Test Corp") - self.assertEqual(org.name, "Test Corp") - - def test_nonexistent_type(self): - """Test error handling for nonexistent types""" - with self.assertRaises(ValueError): - self.facade.export_pydantic_model("NonExistentType") - - def test_custom_base_class(self): - """Test using custom base class""" - class CustomBase(BaseModel): - custom_field: str = "default" - - PersonModel = self.facade.export_pydantic_model("Person", base_class=CustomBase) - - # Should inherit from custom base - self.assertTrue(issubclass(PersonModel, CustomBase)) - - # Should have both custom and schema fields - person = PersonModel(name="Test") - self.assertEqual(person.name, "Test") - self.assertEqual(person.custom_field, "default") - - def test_field_metadata(self): - """Test that field metadata is preserved""" - PersonModel = self.facade.export_pydantic_model("Person") - - # Check model schema includes field descriptions - schema = PersonModel.model_json_schema() - self.assertIn("Person's name", schema["properties"]["name"]["description"]) - self.assertIn("Age in years", schema["properties"]["age"]["description"]) - - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_restriction.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_restriction.py deleted file mode 100644 index 8619cc3..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_restriction.py +++ /dev/null @@ -1,211 +0,0 @@ -import unittest -import sys -from pathlib import Path - -# Add source to path -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from lib_ro_crate_schema.crate.restriction import Restriction -from rdflib import OWL, Literal, XSD - - -class TestRestriction(unittest.TestCase): - """Test cases for the Restriction class""" - - def setUp(self): - """Set up test fixtures""" - self.basic_restriction = Restriction(property_type="testProperty") - - self.complete_restriction = Restriction( - id="complete_restriction", - property_type="name", - min_cardinality=1, - max_cardinality=1 - ) - - self.unbounded_restriction = Restriction( - property_type="tags", - min_cardinality=0, - max_cardinality=None # Unbounded - ) - - def test_restriction_creation(self): - """Test basic Restriction object creation""" - self.assertEqual(self.basic_restriction.property_type, "testProperty") - self.assertIsNone(self.basic_restriction.min_cardinality) - self.assertIsNone(self.basic_restriction.max_cardinality) - self.assertIsNotNone(self.basic_restriction.id) # Auto-generated UUID - - def test_restriction_with_cardinalities(self): - """Test restriction with explicit cardinalities""" - self.assertEqual(self.complete_restriction.property_type, "name") - self.assertEqual(self.complete_restriction.min_cardinality, 1) - self.assertEqual(self.complete_restriction.max_cardinality, 1) - - def test_unbounded_restriction(self): - """Test restriction with unbounded max cardinality""" - self.assertEqual(self.unbounded_restriction.property_type, "tags") - self.assertEqual(self.unbounded_restriction.min_cardinality, 0) - self.assertIsNone(self.unbounded_restriction.max_cardinality) - - def test_to_triples(self): - """Test RDF triple generation""" - triples = list(self.complete_restriction.to_triples()) - - # Should generate multiple triples - self.assertGreater(len(triples), 0) - - # Convert to string representation for easier testing - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Check for essential triples - type_triple_found = any("Restriction" in triple[2] for triple in triple_strs) - self.assertTrue(type_triple_found, "Should generate owl:Restriction type triple") - - on_property_found = any("onProperty" in triple[1] for triple in triple_strs) - self.assertTrue(on_property_found, "Should generate owl:onProperty triple") - - min_card_found = any("minCardinality" in triple[1] for triple in triple_strs) - self.assertTrue(min_card_found, "Should generate owl:minCardinality triple") - - max_card_found = any("maxCardinality" in triple[1] for triple in triple_strs) - self.assertTrue(max_card_found, "Should generate owl:maxCardinality triple") - - def test_minimal_restriction_triples(self): - """Test triple generation for restriction with no cardinalities""" - minimal = Restriction(property_type="minimal_prop") - triples = list(minimal.to_triples()) - - # Should at least generate type and onProperty triples - self.assertGreater(len(triples), 0) - - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - type_found = any("Restriction" in triple[2] for triple in triple_strs) - self.assertTrue(type_found, "Should generate owl:Restriction type") - - on_property_found = any("onProperty" in triple[1] for triple in triple_strs) - self.assertTrue(on_property_found, "Should generate owl:onProperty") - - # Should NOT generate cardinality triples when they're None - min_card_found = any("minCardinality" in triple[1] for triple in triple_strs) - max_card_found = any("maxCardinality" in triple[1] for triple in triple_strs) - self.assertFalse(min_card_found, "Should not generate minCardinality when None") - self.assertFalse(max_card_found, "Should not generate maxCardinality when None") - - def test_only_min_cardinality(self): - """Test restriction with only min cardinality set""" - restriction = Restriction( - property_type="min_only", - min_cardinality=1 - ) - - triples = list(restriction.to_triples()) - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - min_card_found = any("minCardinality" in triple[1] for triple in triple_strs) - max_card_found = any("maxCardinality" in triple[1] for triple in triple_strs) - - self.assertTrue(min_card_found, "Should generate minCardinality") - self.assertFalse(max_card_found, "Should not generate maxCardinality when None") - - def test_only_max_cardinality(self): - """Test restriction with only max cardinality set""" - restriction = Restriction( - property_type="max_only", - max_cardinality=5 - ) - - triples = list(restriction.to_triples()) - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - min_card_found = any("minCardinality" in triple[1] for triple in triple_strs) - max_card_found = any("maxCardinality" in triple[1] for triple in triple_strs) - - self.assertFalse(min_card_found, "Should not generate minCardinality when None") - self.assertTrue(max_card_found, "Should generate maxCardinality") - - def test_zero_cardinalities(self): - """Test restriction with zero cardinalities (explicit zeros)""" - restriction = Restriction( - property_type="zero_test", - min_cardinality=0, - max_cardinality=0 - ) - - triples = list(restriction.to_triples()) - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Zero cardinalities should be included (different from None) - min_card_found = any("minCardinality" in triple[1] and "0" in triple[2] for triple in triple_strs) - max_card_found = any("maxCardinality" in triple[1] and "0" in triple[2] for triple in triple_strs) - - self.assertTrue(min_card_found, "Should generate minCardinality=0") - self.assertTrue(max_card_found, "Should generate maxCardinality=0") - - def test_common_restriction_patterns(self): - """Test common restriction patterns used in RO-Crate schemas""" - - # Required single value (exactly one) - required_single = Restriction( - property_type="title", - min_cardinality=1, - max_cardinality=1 - ) - - # Optional single value (zero or one) - optional_single = Restriction( - property_type="description", - min_cardinality=0, - max_cardinality=1 - ) - - # Required multiple values (one or more) - required_multiple = Restriction( - property_type="author", - min_cardinality=1, - max_cardinality=None - ) - - # Optional multiple values (zero or more) - optional_multiple = Restriction( - property_type="keywords", - min_cardinality=0, - max_cardinality=None - ) - - # Test each pattern generates appropriate triples - patterns = [required_single, optional_single, required_multiple, optional_multiple] - - for restriction in patterns: - triples = list(restriction.to_triples()) - self.assertGreater(len(triples), 0, f"Restriction {restriction.property_type} should generate triples") - - # All should have type and onProperty - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - type_found = any("Restriction" in triple[2] for triple in triple_strs) - on_prop_found = any("onProperty" in triple[1] for triple in triple_strs) - - self.assertTrue(type_found, f"Restriction {restriction.property_type} should have type") - self.assertTrue(on_prop_found, f"Restriction {restriction.property_type} should have onProperty") - - def test_custom_id(self): - """Test restriction with custom ID""" - custom_id = "Person_name_restriction" - restriction = Restriction( - id=custom_id, - property_type="name", - min_cardinality=1 - ) - - self.assertEqual(restriction.id, custom_id) - - triples = list(restriction.to_triples()) - # The subject of triples should use the custom ID - subjects = set(str(triple[0]) for triple in triples) - custom_id_used = any(custom_id in subject for subject in subjects) - self.assertTrue(custom_id_used, "Should use custom ID in triples") - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_roundtrip.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_roundtrip.py deleted file mode 100644 index c23404a..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_roundtrip.py +++ /dev/null @@ -1,397 +0,0 @@ -import unittest -import sys -import json -import tempfile -from pathlib import Path - -# Add source to path -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from lib_ro_crate_schema.crate.schema_facade import SchemaFacade -from lib_ro_crate_schema.crate.type import Type -from lib_ro_crate_schema.crate.type_property import TypeProperty -from lib_ro_crate_schema.crate.literal_type import LiteralType -from lib_ro_crate_schema.crate.metadata_entry import MetadataEntry -from lib_ro_crate_schema.crate.restriction import Restriction - - -class TestRoundTripCycles(unittest.TestCase): - """Test round-trip conversion cycles to verify no data loss during import/export""" - - def setUp(self): - """Set up test fixtures with comprehensive schema""" - # Create a comprehensive test schema - - # Properties - self.name_prop = TypeProperty( - id="name", - range_includes=[LiteralType.STRING], - required=True, - label="Full Name", - comment="The complete name of the entity", - ontological_annotations=["https://schema.org/name"] - ) - - self.age_prop = TypeProperty( - id="age", - range_includes=[LiteralType.INTEGER], - required=False, - label="Age", - comment="Age in years" - ) - - self.email_prop = TypeProperty( - id="email", - range_includes=[LiteralType.STRING], - required=False, - label="Email Address" - ) - - self.knows_prop = TypeProperty( - id="knows", - range_includes=["Person"], - required=False, - label="Knows", - comment="People this person knows" - ) - - # Restrictions - self.name_restriction = Restriction( - id="Person_name_restriction", - property_type="name", - min_cardinality=1, - max_cardinality=1 - ) - - self.knows_restriction = Restriction( - id="Person_knows_restriction", - property_type="knows", - min_cardinality=0, - max_cardinality=None # Unbounded - ) - - # Types - self.person_type = Type( - id="Person", - subclass_of=["https://schema.org/Thing"], - ontological_annotations=["https://schema.org/Person"], - rdfs_property=[self.name_prop, self.age_prop, self.email_prop, self.knows_prop], - restrictions=[self.name_restriction, self.knows_restriction], - comment="A person entity with comprehensive metadata", - label="Person" - ) - - self.organization_type = Type( - id="Organization", - subclass_of=["https://schema.org/Thing"], - ontological_annotations=["https://schema.org/Organization"], - rdfs_property=[self.name_prop], - comment="An organization", - label="Organization" - ) - - # Metadata entries - self.person1 = MetadataEntry( - id="person1", - class_id="Person", - properties={ - "name": "Alice Johnson", - "age": 30, - "email": "alice@example.com" - }, - references={ - "knows": ["person2"] - } - ) - - self.person2 = MetadataEntry( - id="person2", - class_id="Person", - properties={ - "name": "Bob Smith", - "age": 25 - }, - references={ - "knows": ["person1"] # Mutual relationship - } - ) - - self.org1 = MetadataEntry( - id="org1", - class_id="Organization", - properties={ - "name": "Example Corp" - } - ) - - # Complete facade - self.original_facade = SchemaFacade( - types=[self.person_type, self.organization_type], - metadata_entries=[self.person1, self.person2, self.org1] - ) - - def test_export_import_roundtrip(self): - """Test export to file and import back maintains schema integrity""" - - with tempfile.TemporaryDirectory() as temp_dir: - # Export original facade - self.original_facade.write( - temp_dir, - name="Roundtrip Test", - description="Testing roundtrip conversion", - license="MIT" - ) - - # Import back from file - metadata_file = Path(temp_dir) / "ro-crate-metadata.json" - imported_facade = SchemaFacade.from_ro_crate(metadata_file) - - # Compare facades - self._compare_facades(self.original_facade, imported_facade, "File roundtrip") - - def test_json_dict_roundtrip(self): - """Test conversion to JSON dict and back maintains schema integrity""" - - # Convert to JSON dict - json_data = self.original_facade.to_json() - - # Import from dict - imported_facade = SchemaFacade.from_dict(json_data) - - # Compare facades - self._compare_facades(self.original_facade, imported_facade, "JSON dict roundtrip") - - def test_multiple_roundtrips(self): - """Test multiple export/import cycles to ensure stability""" - - current_facade = self.original_facade - - for cycle in range(3): # Test 3 cycles - with tempfile.TemporaryDirectory() as temp_dir: - # Export current facade - current_facade.write( - temp_dir, - name=f"Multi-roundtrip Cycle {cycle + 1}", - description="Testing multiple roundtrip cycles" - ) - - # Import back - metadata_file = Path(temp_dir) / "ro-crate-metadata.json" - current_facade = SchemaFacade.from_ro_crate(metadata_file) - - # Compare with original (should remain consistent) - self._compare_facades( - self.original_facade, - current_facade, - f"Multiple roundtrip cycle {cycle + 1}" - ) - - def test_triples_preservation(self): - """Test that RDF triples are preserved through roundtrip""" - - # Get original triples - original_triples = set() - for triple in self.original_facade.to_triples(): - # Normalize to string representation for comparison - triple_str = (str(triple[0]), str(triple[1]), str(triple[2])) - original_triples.add(triple_str) - - # Roundtrip via JSON - json_data = self.original_facade.to_json() - imported_facade = SchemaFacade.from_dict(json_data) - - # Get imported triples - imported_triples = set() - for triple in imported_facade.to_triples(): - triple_str = (str(triple[0]), str(triple[1]), str(triple[2])) - imported_triples.add(triple_str) - - # Compare triple sets - print(f"\nTriples preservation test:") - print(f"Original triples: {len(original_triples)}") - print(f"Imported triples: {len(imported_triples)}") - - # Find differences - only_in_original = original_triples - imported_triples - only_in_imported = imported_triples - original_triples - - if only_in_original: - print(f"Triples lost in import: {len(only_in_original)}") - for triple in list(only_in_original)[:5]: # Show first 5 - print(f" Lost: {triple}") - - if only_in_imported: - print(f"New triples in import: {len(only_in_imported)}") - for triple in list(only_in_imported)[:5]: # Show first 5 - print(f" New: {triple}") - - # Allow some differences due to RO-Crate structure additions - # But core schema triples should be preserved - self.assertGreater(len(imported_triples), 0, "Should have imported triples") - - def test_obenbis_roundtrip(self): - """Test roundtrip with the OpenBIS example if available""" - - obenbis_file = (Path(__file__).parent.parent.parent.parent / - "example" / "obenbis-one-publication" / "ro-crate-metadata.json") - - if not obenbis_file.exists(): - self.skipTest(f"OpenBIS example not found at {obenbis_file}") - - # Import OpenBIS RO-Crate - original_facade = SchemaFacade.from_ro_crate(obenbis_file) - - with tempfile.TemporaryDirectory() as temp_dir: - # Export it - original_facade.write( - temp_dir, - name="OpenBIS Roundtrip Test", - description="Testing OpenBIS RO-Crate roundtrip" - ) - - # Import back - metadata_file = Path(temp_dir) / "ro-crate-metadata.json" - imported_facade = SchemaFacade.from_ro_crate(metadata_file) - - # Basic consistency checks - print(f"\nOpenBIS roundtrip test:") - print(f"Original - Types: {len(original_facade.types)}, Entries: {len(original_facade.metadata_entries)}") - print(f"Imported - Types: {len(imported_facade.types)}, Entries: {len(imported_facade.metadata_entries)}") - - # Should have similar structure (allowing for some differences due to RO-Crate additions) - self.assertGreaterEqual( - len(imported_facade.types) + len(imported_facade.metadata_entries), - 0, - "Should have imported some entities" - ) - - def test_property_cardinality_preservation(self): - """Test that property cardinality information is preserved""" - - # Create a facade with specific cardinality requirements - required_prop = TypeProperty(id="required_field", range_includes=[LiteralType.STRING], required=True) - optional_prop = TypeProperty(id="optional_field", range_includes=[LiteralType.STRING], required=False) - - test_type = Type( - id="TestType", - rdfs_property=[required_prop, optional_prop] - ) - - test_facade = SchemaFacade(types=[test_type]) - - # Roundtrip via JSON - json_data = test_facade.to_json() - imported_facade = SchemaFacade.from_dict(json_data) - - # Check that cardinality info is preserved through restrictions - imported_type = imported_facade.get_type("TestType") - self.assertIsNotNone(imported_type) - - restrictions = imported_type.get_restrictions() - - # Find restrictions for our properties - required_restriction = None - optional_restriction = None - - for restriction in restrictions: - if restriction.property_type == "required_field": - required_restriction = restriction - elif restriction.property_type == "optional_field": - optional_restriction = restriction - - # Check cardinalities (if restrictions were generated) - if required_restriction: - self.assertEqual(required_restriction.min_cardinality, 1, "Required field should have min cardinality 1") - - if optional_restriction: - self.assertEqual(optional_restriction.min_cardinality, 0, "Optional field should have min cardinality 0") - - def test_ontological_annotations_preservation(self): - """Test that ontological annotations are preserved""" - - # Test facade with ontological annotations - json_data = self.original_facade.to_json() - imported_facade = SchemaFacade.from_dict(json_data) - - # Check Person type annotations - original_person = self.original_facade.get_type("Person") - imported_person = imported_facade.get_type("Person") - - if imported_person and original_person: - print(f"\nOntological annotations test:") - print(f"Original Person ontological annotations: {original_person.ontological_annotations}") - print(f"Imported Person ontological annotations: {imported_person.ontological_annotations}") - - # Should preserve ontological mapping - if original_person.ontological_annotations: - self.assertIsNotNone( - imported_person.ontological_annotations, - "Should preserve ontological annotations" - ) - - def _compare_facades(self, original: SchemaFacade, imported: SchemaFacade, test_name: str): - """Helper method to compare two facades for consistency""" - - print(f"\n{test_name} comparison:") - print(f"Original - Types: {len(original.types)}, Entries: {len(original.metadata_entries)}") - print(f"Imported - Types: {len(imported.types)}, Entries: {len(imported.metadata_entries)}") - - # Basic counts should be similar (allowing for RO-Crate structure additions) - self.assertGreaterEqual( - len(imported.types) + len(imported.metadata_entries), - len(original.types) + len(original.metadata_entries), - "Should preserve at least original entities" - ) - - # Check specific types are preserved - for original_type in original.types: - imported_type = imported.get_type(original_type.id) - if imported_type: # May not be preserved due to import/export limitations - self.assertEqual( - imported_type.id, - original_type.id, - f"Type ID should be preserved: {original_type.id}" - ) - - if original_type.label and imported_type.label: - self.assertEqual( - imported_type.label, - original_type.label, - f"Type label should be preserved: {original_type.id}" - ) - - # Check specific metadata entries are preserved - for original_entry in original.metadata_entries: - imported_entry = imported.get_entry(original_entry.id) - if imported_entry: # May not be preserved due to import/export limitations - self.assertEqual( - imported_entry.id, - original_entry.id, - f"Entry ID should be preserved: {original_entry.id}" - ) - - self.assertEqual( - imported_entry.class_id, - original_entry.class_id, - f"Entry class ID should be preserved: {original_entry.id}" - ) - - # Test that we can generate valid output from imported facade - try: - imported_json = imported.to_json() - self.assertIn("@context", imported_json) - self.assertIn("@graph", imported_json) - except Exception as e: - self.fail(f"Failed to generate JSON from imported facade: {e}") - - try: - imported_triples = list(imported.to_triples()) - self.assertGreater(len(imported_triples), 0, "Should generate triples from imported facade") - except Exception as e: - self.fail(f"Failed to generate triples from imported facade: {e}") - - print(f"✓ {test_name} completed successfully") - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_schema_facade.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_schema_facade.py deleted file mode 100644 index fe0e241..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_schema_facade.py +++ /dev/null @@ -1,337 +0,0 @@ -import unittest -import sys -import json -import tempfile -from pathlib import Path - -# Add source to path -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from lib_ro_crate_schema.crate.schema_facade import SchemaFacade -from lib_ro_crate_schema.crate.type import Type -from lib_ro_crate_schema.crate.type_property import TypeProperty -from lib_ro_crate_schema.crate.literal_type import LiteralType -from lib_ro_crate_schema.crate.metadata_entry import MetadataEntry -from lib_ro_crate_schema.crate.restriction import Restriction - - -class TestSchemaFacade(unittest.TestCase): - """Test cases for the SchemaFacade class""" - - def setUp(self): - """Set up test fixtures""" - # Create a basic schema with types and properties - self.name_property = TypeProperty( - id="name", - range_includes=[LiteralType.STRING], - required=True - ) - - self.age_property = TypeProperty( - id="age", - range_includes=[LiteralType.INTEGER], - required=False - ) - - self.person_type = Type( - id="Person", - rdfs_property=[self.name_property, self.age_property], - comment="A person entity", - label="Person" - ) - - self.person_entry = MetadataEntry( - id="person1", - class_id="Person", - properties={"name": "John Doe", "age": 30} - ) - - self.facade = SchemaFacade( - types=[self.person_type], - metadata_entries=[self.person_entry] - ) - - def test_facade_creation(self): - """Test basic SchemaFacade creation""" - empty_facade = SchemaFacade() - self.assertEqual(len(empty_facade.types), 0) - self.assertEqual(len(empty_facade.metadata_entries), 0) - - self.assertEqual(len(self.facade.types), 1) - self.assertEqual(len(self.facade.metadata_entries), 1) - - def test_fluent_api(self): - """Test fluent API methods""" - facade = SchemaFacade() - - result = facade.addType(self.person_type).addEntry(self.person_entry) - - # Check method chaining works - self.assertEqual(result, facade) - - # Check items were added - self.assertIn(self.person_type, facade.types) - self.assertIn(self.person_entry, facade.metadata_entries) - - def test_get_methods(self): - """Test getter methods""" - # Test get_types - types = self.facade.get_types() - self.assertEqual(len(types), 1) - self.assertEqual(types[0].id, "Person") - - # Test get_type - person_type = self.facade.get_type("Person") - self.assertIsNotNone(person_type) - self.assertEqual(person_type.id, "Person") - - non_existent = self.facade.get_type("NonExistent") - self.assertIsNone(non_existent) - - # Test get_entries - entries = self.facade.get_entries() - self.assertEqual(len(entries), 1) - self.assertEqual(entries[0].id, "person1") - - # Test get_entry - person_entry = self.facade.get_entry("person1") - self.assertIsNotNone(person_entry) - self.assertEqual(person_entry.id, "person1") - - # Test get_entries_by_class - person_entries = self.facade.get_entries_by_class("Person") - self.assertEqual(len(person_entries), 1) - self.assertEqual(person_entries[0].id, "person1") - - def test_java_api_compatibility(self): - """Test Java API compatibility methods""" - # Test property methods - properties = self.facade.get_property_types() - self.assertEqual(len(properties), 2) - property_ids = [prop.id for prop in properties] - self.assertIn("name", property_ids) - self.assertIn("age", property_ids) - - # Test get_property_type - name_prop = self.facade.get_property_type("name") - self.assertIsNotNone(name_prop) - self.assertEqual(name_prop.id, "name") - - # Test get_crate (basic functionality) - crate = self.facade.get_crate() - self.assertIsNotNone(crate) - - def test_to_triples(self): - """Test RDF triple generation""" - triples = list(self.facade.to_triples()) - - # Should generate triples for both types and metadata entries - self.assertGreater(len(triples), 0) - - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Should include type definition triples - class_triple_found = any("Class" in triple[2] for triple in triple_strs) - self.assertTrue(class_triple_found, "Should generate class definition triples") - - # Should include metadata entry triples - person_triple_found = any("person1" in triple[0] for triple in triple_strs) - self.assertTrue(person_triple_found, "Should generate metadata entry triples") - - def test_to_graph(self): - """Test RDF Graph generation""" - graph = self.facade.to_graph() - - # Should have triples - self.assertGreater(len(graph), 0) - - # Should have proper namespace binding - namespaces = dict(graph.namespaces()) - self.assertIn('base', namespaces) - - def test_to_json(self): - """Test JSON-LD generation""" - json_data = self.facade.to_json() - - self.assertIsInstance(json_data, dict) - self.assertIn("@context", json_data) - self.assertIn("@graph", json_data) - - def test_write_to_crate(self): - """Test writing to RO-Crate directory""" - with tempfile.TemporaryDirectory() as temp_dir: - self.facade.write( - temp_dir, - name="Test Crate", - description="A test RO-Crate", - license="MIT" - ) - - # Check that metadata file was created - metadata_file = Path(temp_dir) / "ro-crate-metadata.json" - self.assertTrue(metadata_file.exists()) - - # Check that the file contains valid JSON - with open(metadata_file, 'r') as f: - crate_data = json.load(f) - - self.assertIn("@context", crate_data) - self.assertIn("@graph", crate_data) - - def test_from_ro_crate_roundtrip(self): - """Test creating facade from RO-Crate and ensuring roundtrip consistency""" - with tempfile.TemporaryDirectory() as temp_dir: - # Write original facade - self.facade.write(temp_dir, name="Roundtrip Test") - - # Read back from file - metadata_file = Path(temp_dir) / "ro-crate-metadata.json" - imported_facade = SchemaFacade.from_ro_crate(metadata_file) - - # Check that types were imported - self.assertGreater(len(imported_facade.types), 0) - - # Check that metadata entries were imported - self.assertGreater(len(imported_facade.metadata_entries), 0) - - def test_from_dict(self): - """Test creating facade from dictionary""" - # Create a simple RO-Crate structure - crate_dict = { - "@context": ["https://w3id.org/ro/crate/1.1/context"], - "@graph": [ - { - "@id": "./", - "@type": "Dataset", - "name": "Test Dataset" - }, - { - "@id": "ro-crate-metadata.json", - "@type": "CreativeWork", - "about": {"@id": "./"} - }, - { - "@id": "Person", - "@type": "rdfs:Class", - "rdfs:label": "Person", - "rdfs:comment": "A person" - }, - { - "@id": "name", - "@type": "rdf:Property", - "rdfs:label": "Name", - "schema:domainIncludes": {"@id": "Person"}, - "schema:rangeIncludes": {"@id": "http://www.w3.org/2001/XMLSchema#string"} - }, - { - "@id": "person1", - "@type": "Person", - "name": "Alice Johnson" - } - ] - } - - facade = SchemaFacade.from_dict(crate_dict) - - # Should have imported the class - person_type = facade.get_type("Person") - self.assertIsNotNone(person_type) - self.assertEqual(person_type.label, "Person") - - # Should have imported the metadata entry - person_entry = facade.get_entry("person1") - self.assertIsNotNone(person_entry) - self.assertEqual(person_entry.class_id, "Person") - - def test_resolve_forward_refs(self): - """Test forward reference resolution""" - # This is mostly an internal method, but we can test it doesn't crash - self.facade.resolve_forward_refs() - - # Should still have the same number of types and entries - self.assertEqual(len(self.facade.types), 1) - self.assertEqual(len(self.facade.metadata_entries), 1) - - def test_add_property_type(self): - """Test adding standalone property to registry""" - new_prop = TypeProperty(id="email", range_includes=[LiteralType.STRING]) - - result = self.facade.add_property_type(new_prop) - - # Should return self for chaining - self.assertEqual(result, self.facade) - - # Should be able to retrieve the property - retrieved_prop = self.facade.get_property_type("email") - self.assertIsNotNone(retrieved_prop) - self.assertEqual(retrieved_prop.id, "email") - - def test_complex_schema(self): - """Test facade with complex schema including restrictions""" - # Create a type with custom restrictions - title_prop = TypeProperty(id="title", range_includes=[LiteralType.STRING]) - authors_prop = TypeProperty(id="authors", range_includes=["Person"]) - - title_restriction = Restriction( - property_type="title", - min_cardinality=1, - max_cardinality=1 - ) - - authors_restriction = Restriction( - property_type="authors", - min_cardinality=1, - max_cardinality=None # Unbounded - ) - - article_type = Type( - id="Article", - rdfs_property=[title_prop, authors_prop], - restrictions=[title_restriction, authors_restriction], - comment="A research article", - label="Article" - ) - - article_entry = MetadataEntry( - id="article1", - class_id="Article", - properties={"title": "Great Research"}, - references={"authors": ["person1"]} - ) - - complex_facade = SchemaFacade( - types=[self.person_type, article_type], - metadata_entries=[self.person_entry, article_entry] - ) - - # Test that complex schema works - self.assertEqual(len(complex_facade.types), 2) - self.assertEqual(len(complex_facade.metadata_entries), 2) - - # Test restrictions are included - article = complex_facade.get_type("Article") - restrictions = article.get_restrictions() - self.assertGreater(len(restrictions), 0) - - # Test triple generation works - triples = list(complex_facade.to_triples()) - self.assertGreater(len(triples), 0) - - def test_empty_facade_operations(self): - """Test operations on empty facade""" - empty_facade = SchemaFacade() - - # Should handle empty operations gracefully - self.assertEqual(len(empty_facade.get_types()), 0) - self.assertEqual(len(empty_facade.get_entries()), 0) - self.assertIsNone(empty_facade.get_type("NonExistent")) - self.assertIsNone(empty_facade.get_entry("NonExistent")) - self.assertEqual(len(empty_facade.get_entries_by_class("NonExistent")), 0) - - # Should still generate basic structure - json_data = empty_facade.to_json() - self.assertIn("@context", json_data) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_standalone_elements.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_standalone_elements.py deleted file mode 100644 index 995b780..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_standalone_elements.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python3 -""" -Test standalone properties and restrictions in SchemaFacade -""" - -import sys -sys.path.append('src') - -from lib_ro_crate_schema.crate.schema_facade import SchemaFacade -from lib_ro_crate_schema.crate.type_property import TypeProperty -from lib_ro_crate_schema.crate.restriction import Restriction -from lib_ro_crate_schema.crate.type import Type - -def test_standalone_elements(): - """Test adding and retrieving standalone properties and restrictions""" - - print("🧪 Testing standalone properties and restrictions...") - - # Create a facade - facade = SchemaFacade() - - # Test 1: Add standalone property - standalone_prop = TypeProperty( - id="globalProperty", - label="Global Property", - comment="A property that exists independently of any type", - range_includes=["xsd:string"] - ) - - facade.add_property_type(standalone_prop) - print(f"✅ Added standalone property: {standalone_prop.id}") - - # Test 2: Add standalone restriction - standalone_restriction = Restriction( - id="globalRestriction", - property_type="globalProperty", - min_cardinality=1, - max_cardinality=5 - ) - - facade.add_restriction(standalone_restriction) - print(f"✅ Added standalone restriction: {standalone_restriction.id}") - - # Test 3: Add a type with its own properties - person_name_prop = TypeProperty( - id="personName", - label="Person Name", - comment="Name property specific to Person type", - range_includes=["xsd:string"] - ) - - person_type = Type( - id="Person", - label="Person", - comment="A person entity", - rdfs_property=[person_name_prop] - ) - - facade.addType(person_type) - print(f"✅ Added type with attached property: {person_type.id}") - - # Test 4: Verify counts - all_properties = facade.get_property_types() - all_restrictions = facade.get_restrictions() - - print(f"\n📊 Summary:") - print(f" Total properties: {len(all_properties)}") - print(f" Total restrictions: {len(all_restrictions)}") - print(f" Total types: {len(facade.types)}") - - # Test 5: Check specific retrieval - retrieved_prop = facade.get_property_type("globalProperty") - retrieved_restriction = facade.get_restriction("globalRestriction") - - print(f"\n🔍 Specific retrieval:") - print(f" Retrieved global property: {'✅' if retrieved_prop else '❌'}") - print(f" Retrieved global restriction: {'✅' if retrieved_restriction else '❌'}") - - # Test 6: List all properties (standalone + type-attached) - print(f"\n📋 All properties found:") - for prop in all_properties: - is_standalone = any(p.id == prop.id for p in facade.property_types) - status = "standalone" if is_standalone else "type-attached" - print(f" - {prop.id} ({status})") - - # Test 7: Export to RDF and verify triples include standalone elements - print(f"\n🔄 RDF export test:") - graph = facade.to_graph() - triple_count = len(graph) - print(f" Generated {triple_count} RDF triples") - - # Test 8: Round-trip test - export and reimport - print(f"\n🔄 Round-trip test:") - import os - output_dir = "output_crates" - os.makedirs(output_dir, exist_ok=True) - - test_output_path = os.path.join(output_dir, "test_standalone_output") - facade.write(test_output_path, name="Standalone Elements Test") - - # Import back - imported_facade = SchemaFacade.from_ro_crate(test_output_path) - - imported_properties = imported_facade.get_property_types() - imported_restrictions = imported_facade.get_restrictions() - - print(f" Original properties: {len(all_properties)}") - print(f" Imported properties: {len(imported_properties)}") - print(f" Original restrictions: {len(all_restrictions)}") - print(f" Imported restrictions: {len(imported_restrictions)}") - - # Check if our standalone elements survived the round-trip - survived_global_prop = imported_facade.get_property_type("globalProperty") - survived_global_restr = imported_facade.get_restriction("globalRestriction") - - print(f" Standalone property survived: {'✅' if survived_global_prop else '❌'}") - print(f" Standalone restriction survived: {'✅' if survived_global_restr else '❌'}") - - print(f"\n🎉 Test completed!") - - # Verify test assertions instead of returning values - assert survived_global_prop is not None, "Standalone property should survive round-trip" - assert survived_global_restr is not None, "Standalone restriction should survive round-trip" - assert len(imported_properties) > 0, "Should have imported properties" - assert len(imported_restrictions) > 0, "Should have imported restrictions" - -if __name__ == "__main__": - test_standalone_elements() - print(f"\n📈 Test completed successfully!") \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_type.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_type.py deleted file mode 100644 index 97b775e..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_type.py +++ /dev/null @@ -1,144 +0,0 @@ -import unittest -import sys -from pathlib import Path - -# Add source to path -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from lib_ro_crate_schema.crate.type import Type -from lib_ro_crate_schema.crate.type_property import TypeProperty -from lib_ro_crate_schema.crate.literal_type import LiteralType -from lib_ro_crate_schema.crate.restriction import Restriction -from rdflib import RDFS, RDF, OWL, Literal, URIRef - - -class TestType(unittest.TestCase): - """Test cases for the Type class""" - - def setUp(self): - """Set up test fixtures""" - self.basic_type = Type(id="TestType") - - # Create a property for testing - self.test_property = TypeProperty( - id="testProperty", - range_includes=[LiteralType.STRING], - required=True - ) - - # Create a complete type with all features - self.complete_type = Type( - id="Person", - subclass_of=["https://schema.org/Thing"], - ontological_annotations=["https://schema.org/Person"], - rdfs_property=[self.test_property], - comment="A person entity", - label="Person" - ) - - def test_type_creation(self): - """Test basic Type object creation""" - self.assertEqual(self.basic_type.id, "TestType") - self.assertIsInstance(self.basic_type.subclass_of, list) - self.assertEqual(self.basic_type.subclass_of, ["https://schema.org/Thing"]) - - def test_fluent_api(self): - """Test fluent API methods""" - type_obj = Type(id="FluentTest") - result = (type_obj - .setLabel("Test Label") - .setComment("Test Comment") - .addProperty(self.test_property) - .setOntologicalAnnotations(["http://example.org/TestClass"])) - - # Check method chaining works - self.assertEqual(result, type_obj) - - # Check values were set - self.assertEqual(type_obj.label, "Test Label") - self.assertEqual(type_obj.comment, "Test Comment") - self.assertEqual(type_obj.ontological_annotations, ["http://example.org/TestClass"]) - self.assertIn(self.test_property, type_obj.rdfs_property) - - def test_java_api_compatibility(self): - """Test Java API compatibility methods""" - self.assertEqual(self.complete_type.getId(), "Person") - self.assertEqual(self.complete_type.getLabel(), "Person") - self.assertEqual(self.complete_type.getComment(), "A person entity") - self.assertEqual(self.complete_type.getSubClassOf(), ["https://schema.org/Thing"]) - self.assertEqual(self.complete_type.getOntologicalAnnotations(), ["https://schema.org/Person"]) - - def test_get_restrictions(self): - """Test restriction generation from properties""" - restrictions = self.complete_type.get_restrictions() - - self.assertIsInstance(restrictions, list) - self.assertTrue(len(restrictions) >= 1) - - # Find the restriction for our test property - test_prop_restriction = None - for restriction in restrictions: - if restriction.property_type == "testProperty": - test_prop_restriction = restriction - break - - self.assertIsNotNone(test_prop_restriction) - self.assertEqual(test_prop_restriction.min_cardinality, 1) # required=True - - def test_to_triples(self): - """Test RDF triple generation""" - triples = list(self.complete_type.to_triples()) - - # Should generate multiple triples - self.assertGreater(len(triples), 0) - - # Convert to list of tuples for easier testing - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Check for essential triples - look for Class in the object - type_triple_found = any("Class" in triple[2] for triple in triple_strs) - self.assertTrue(type_triple_found, "Should generate rdfs:Class type triple") - - label_triple_found = any("label" in triple[1] for triple in triple_strs) - self.assertTrue(label_triple_found, "Should generate rdfs:label triple") - - def test_empty_type(self): - """Test type with minimal configuration""" - empty_type = Type(id="MinimalType") - triples = list(empty_type.to_triples()) - - # Should at least generate the class type declaration - self.assertGreater(len(triples), 0) - - def test_property_addition(self): - """Test adding properties to a type""" - type_obj = Type(id="TestType") - - prop1 = TypeProperty(id="prop1", range_includes=[LiteralType.STRING]) - prop2 = TypeProperty(id="prop2", range_includes=[LiteralType.INTEGER]) - - type_obj.addProperty(prop1).addProperty(prop2) - - self.assertEqual(len(type_obj.rdfs_property), 2) - self.assertIn(prop1, type_obj.rdfs_property) - self.assertIn(prop2, type_obj.rdfs_property) - - def test_custom_restrictions(self): - """Test type with custom restrictions""" - custom_restriction = Restriction( - property_type="customProp", - min_cardinality=2, - max_cardinality=5 - ) - - type_obj = Type( - id="RestrictedType", - restrictions=[custom_restriction] - ) - - restrictions = type_obj.get_restrictions() - self.assertIn(custom_restriction, restrictions) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_type_property.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_type_property.py deleted file mode 100644 index 06e00cf..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_type_property.py +++ /dev/null @@ -1,187 +0,0 @@ -import unittest -import sys -from pathlib import Path - -# Add source to path -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from lib_ro_crate_schema.crate.type_property import TypeProperty -from lib_ro_crate_schema.crate.literal_type import LiteralType -from rdflib import RDF, RDFS, Literal, URIRef - - -class TestTypeProperty(unittest.TestCase): - """Test cases for the TypeProperty class""" - - def setUp(self): - """Set up test fixtures""" - self.basic_property = TypeProperty(id="basicProp") - - self.complete_property = TypeProperty( - id="completeProp", - domain_includes=["Person"], - range_includes=[LiteralType.STRING], - ontological_annotations=["https://schema.org/name"], - comment="A complete property for testing", - label="Complete Property", - required=True - ) - - def test_property_creation(self): - """Test basic TypeProperty object creation""" - self.assertEqual(self.basic_property.id, "basicProp") - self.assertEqual(self.basic_property.domain_includes, []) - self.assertEqual(self.basic_property.range_includes, []) - self.assertIsNone(self.basic_property.required) - - def test_fluent_api(self): - """Test fluent API methods""" - prop = TypeProperty(id="fluentTest") - result = (prop - .setLabel("Test Label") - .setComment("Test Comment") - .setTypes([LiteralType.STRING, LiteralType.INTEGER]) - .setRequired(True) - .setOntologicalAnnotations(["http://example.org/prop"])) - - # Check method chaining works - self.assertEqual(result, prop) - - # Check values were set - self.assertEqual(prop.label, "Test Label") - self.assertEqual(prop.comment, "Test Comment") - self.assertTrue(prop.required) - self.assertEqual(prop.range_includes, [LiteralType.STRING, LiteralType.INTEGER]) - self.assertEqual(prop.ontological_annotations, ["http://example.org/prop"]) - - def test_add_type(self): - """Test adding single type to range""" - prop = TypeProperty(id="testProp") - prop.addType(LiteralType.STRING) - prop.addType("CustomType") - - self.assertIn(LiteralType.STRING, prop.range_includes) - self.assertIn("CustomType", prop.range_includes) - - def test_java_api_compatibility(self): - """Test Java API compatibility methods""" - self.assertEqual(self.complete_property.getId(), "completeProp") - self.assertEqual(self.complete_property.getLabel(), "Complete Property") - self.assertEqual(self.complete_property.getComment(), "A complete property for testing") - self.assertEqual(self.complete_property.getDomain(), ["Person"]) - self.assertEqual(self.complete_property.getRange(), [LiteralType.STRING]) - self.assertEqual(self.complete_property.getOntologicalAnnotations(), ["https://schema.org/name"]) - - def test_cardinality_methods(self): - """Test cardinality getter methods""" - # Required property - required_prop = TypeProperty(id="required", required=True) - self.assertEqual(required_prop.get_min_cardinality(), 1) - self.assertEqual(required_prop.get_max_cardinality(), 1) - - # Optional property - optional_prop = TypeProperty(id="optional", required=False) - self.assertEqual(optional_prop.get_min_cardinality(), 0) - self.assertEqual(optional_prop.get_max_cardinality(), 1) - - # Unspecified property (defaults to optional) - unspecified_prop = TypeProperty(id="unspecified") - self.assertEqual(unspecified_prop.get_min_cardinality(), 0) - self.assertEqual(unspecified_prop.get_max_cardinality(), 1) - - def test_to_triples(self): - """Test RDF triple generation""" - triples = list(self.complete_property.to_triples()) - - # Should generate multiple triples - self.assertGreater(len(triples), 0) - - # Convert to string representation for easier testing - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Check for essential triples - type_triple_found = any("Property" in triple[2] for triple in triple_strs) - self.assertTrue(type_triple_found, "Should generate rdf:Property type triple") - - label_triple_found = any("label" in triple[1] for triple in triple_strs) - self.assertTrue(label_triple_found, "Should generate rdfs:label triple") - - domain_triple_found = any("domainIncludes" in triple[1] for triple in triple_strs) - self.assertTrue(domain_triple_found, "Should generate domainIncludes triple") - - def test_range_includes_xsd_types(self): - """Test handling of XSD data types in range_includes""" - prop = TypeProperty( - id="xsdTest", - range_includes=["xsd:string", "xsd:integer", "xsd:boolean"] - ) - - triples = list(prop.to_triples()) - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Should convert xsd: prefixes to full URIs - xsd_string_found = any("XMLSchema#string" in triple[2] for triple in triple_strs) - self.assertTrue(xsd_string_found, "Should convert xsd:string to full URI") - - def test_range_includes_base_types(self): - """Test handling of base: prefixed types in range_includes""" - prop = TypeProperty( - id="baseTest", - range_includes=["base:CustomType"] - ) - - triples = list(prop.to_triples()) - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Should handle base: prefixed types - base_type_found = any("CustomType" in triple[2] for triple in triple_strs) - self.assertTrue(base_type_found, "Should handle base: prefixed types") - - def test_ontological_annotations(self): - """Test ontological annotation handling""" - prop = TypeProperty( - id="ontoTest", - ontological_annotations=["https://schema.org/name", "http://purl.org/dc/terms/title"] - ) - - triples = list(prop.to_triples()) - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Should generate owl:equivalentProperty triples - equiv_prop_found = any("equivalentProperty" in triple[1] for triple in triple_strs) - self.assertTrue(equiv_prop_found, "Should generate owl:equivalentProperty triples") - - def test_empty_property(self): - """Test property with minimal configuration""" - empty_prop = TypeProperty(id="minimal") - triples = list(empty_prop.to_triples()) - - # Should at least generate the property type declaration - self.assertGreater(len(triples), 0) - - # Should be an rdf:Property - type_triple_found = any("Property" in str(triple) for triple in triples) - self.assertTrue(type_triple_found) - - def test_multiple_domains(self): - """Test property with multiple domain classes""" - prop = TypeProperty( - id="multiDomain", - domain_includes=["Person", "Organization", "Event"] - ) - - triples = list(prop.to_triples()) - triple_strs = [(str(s), str(p), str(o)) for s, p, o in triples] - - # Should generate domainIncludes for each domain - person_domain = any("Person" in triple[2] and "domainIncludes" in triple[1] for triple in triple_strs) - org_domain = any("Organization" in triple[2] and "domainIncludes" in triple[1] for triple in triple_strs) - event_domain = any("Event" in triple[2] and "domainIncludes" in triple[1] for triple in triple_strs) - - self.assertTrue(person_domain, "Should include Person in domain") - self.assertTrue(org_domain, "Should include Organization in domain") - self.assertTrue(event_domain, "Should include Event in domain") - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_unknown_namespaces.py b/0.2.x/lib/python/lib-ro-crate-schema/tests/test_unknown_namespaces.py deleted file mode 100644 index 3055778..0000000 --- a/0.2.x/lib/python/lib-ro-crate-schema/tests/test_unknown_namespaces.py +++ /dev/null @@ -1,247 +0,0 @@ -""" -Test for unknown namespace detection and resolution in JSON-LD contexts. - -This test verifies that the system can automatically detect and create prefixes -for namespaces that are not predefined in the namespace_prefixes dictionary. -""" - -import tempfile -import json -from pathlib import Path - -import pytest -from rocrate.rocrate import ROCrate - -from lib_ro_crate_schema.crate.schema_facade import SchemaFacade - - -class TestUnknownNamespaces: - """Test suite for unknown namespace handling.""" - - def test_unknown_namespace_detection_in_context(self): - """Test that unknown namespaces are automatically detected by get_context.""" - from lib_ro_crate_schema.crate.jsonld_utils import get_context - from rdflib import Graph, URIRef, Literal - from rdflib.namespace import RDF, RDFS - - # Create graph with unknown namespaces - g = Graph() - - # Add triples with unknown pokemon.org namespace - pokemon_ns = "http://pokemon.org/" - pikachu = URIRef(pokemon_ns + "pikachu") - pokemon_name = URIRef(pokemon_ns + "pokemonName") - electric_type = URIRef(pokemon_ns + "ElectricPokemon") - - g.add((pikachu, RDF.type, electric_type)) - g.add((pikachu, pokemon_name, Literal("Pikachu"))) - g.add((pokemon_name, RDF.type, RDF.Property)) - g.add((pokemon_name, RDFS.label, Literal("Pokemon Name"))) - - # Add triples with another unknown namespace - villains_ns = "http://villains.org/" - team_rocket = URIRef(villains_ns + "team_rocket") - criminal_org = URIRef(villains_ns + "CriminalOrganization") - motto = URIRef(villains_ns + "motto") - - g.add((team_rocket, RDF.type, criminal_org)) - g.add((team_rocket, motto, Literal("Prepare for trouble!"))) - - # Also add known namespace - schema_name = URIRef("https://schema.org/name") - g.add((pikachu, schema_name, Literal("Pikachu the Electric Mouse"))) - - # Test context generation - context = get_context(g) - - assert isinstance(context, list) - assert len(context) >= 2 - - # Check that both unknown namespaces were detected - detected_namespaces = {} - if len(context) > 1 and isinstance(context[1], dict): - detected_namespaces = context[1] - - assert "pokemon" in detected_namespaces - assert detected_namespaces["pokemon"] == "http://pokemon.org/" - assert "villains" in detected_namespaces - assert detected_namespaces["villains"] == "http://villains.org/" - assert "schema" in detected_namespaces - assert detected_namespaces["schema"] == "https://schema.org/" - - def test_known_namespaces_still_work(self): - """Test that predefined namespaces still work correctly.""" - from lib_ro_crate_schema.crate.jsonld_utils import get_context - from rdflib import Graph, URIRef, Literal - from rdflib.namespace import RDF, RDFS - - g = Graph() - - # Add triples with known namespaces used as predicates and types - person = URIRef("http://someone.example/john") - - # Use example.com as a predicate (will trigger base: namespace) - example_property = URIRef("http://example.com/customProperty") - g.add((person, example_property, Literal("Some value"))) - - # Use schema.org properties and types - schema_name = URIRef("https://schema.org/name") - g.add((person, schema_name, Literal("John Doe"))) - g.add((person, RDF.type, URIRef("https://schema.org/Person"))) - - # Use openbis.org as a predicate - openbis_property = URIRef("http://openbis.org/sampleId") - g.add((person, openbis_property, Literal("sample123"))) - - context = get_context(g) - - assert isinstance(context, list) - if len(context) > 1 and isinstance(context[1], dict): - namespaces = context[1] - assert "base" in namespaces - assert namespaces["base"] == "http://example.com/" - assert "schema" in namespaces - assert namespaces["schema"] == "https://schema.org/" - assert "openbis" in namespaces - assert namespaces["openbis"] == "http://openbis.org/" - - def test_prefix_collision_handling(self): - """Test that prefix collisions are handled gracefully.""" - from lib_ro_crate_schema.crate.jsonld_utils import get_context - from rdflib import Graph, URIRef, Literal - from rdflib.namespace import RDF - - g = Graph() - - # Create a scenario where we might have prefix collisions - # Use pokemon.org multiple times with DIFFERENT types (should get 'pokemon' prefix) - pokemon_uri1 = URIRef("http://pokemon.org/pikachu") - pokemon_uri2 = URIRef("http://pokemon.org/raichu") - g.add((pokemon_uri1, RDF.type, URIRef("http://pokemon.org/ElectricPokemon"))) - g.add((pokemon_uri2, RDF.type, URIRef("http://pokemon.org/EvolutionPokemon"))) - - # Use pokemon.com multiple times (should get 'pokemon1' or similar) - pokemon_com_uri1 = URIRef("http://pokemon.com/charizard") - pokemon_com_uri2 = URIRef("http://pokemon.com/blastoise") - g.add((pokemon_com_uri1, RDF.type, URIRef("http://pokemon.com/FirePokemon"))) - g.add((pokemon_com_uri2, RDF.type, URIRef("http://pokemon.com/WaterPokemon"))) - - context = get_context(g) - - if isinstance(context, list) and len(context) > 1 and isinstance(context[1], dict): - namespaces = context[1] - - # Both namespaces should be detected with different prefixes - pokemon_prefixes = [k for k, v in namespaces.items() - if 'pokemon.' in v] - assert len(pokemon_prefixes) == 2 - - # Verify the actual mappings exist - namespace_values = list(namespaces.values()) - assert "http://pokemon.org/" in namespace_values - assert "http://pokemon.com/" in namespace_values - - def test_minimum_usage_threshold(self): - """Test that namespaces need minimum usage count to be detected.""" - from lib_ro_crate_schema.crate.jsonld_utils import get_context - from rdflib import Graph, URIRef, Literal - from rdflib.namespace import RDF - - g = Graph() - - # Add only one URI from a namespace (below threshold) - single_use = URIRef("http://rarely-used.org/single") - g.add((single_use, RDF.type, URIRef("https://schema.org/Thing"))) - - # Add multiple URIs from another namespace (above threshold) - frequent_ns = "http://frequent.org/" - for i in range(3): - uri = URIRef(f"{frequent_ns}item{i}") - g.add((uri, RDF.type, URIRef(f"{frequent_ns}ItemType"))) - # Add another usage to ensure it meets the threshold - g.add((uri, URIRef(f"{frequent_ns}hasProperty"), Literal(f"value{i}"))) - - context = get_context(g) - - if isinstance(context, list) and len(context) > 1 and isinstance(context[1], dict): - namespaces = context[1] - - # frequent.org should be detected - assert "frequent" in namespaces - assert namespaces["frequent"] == "http://frequent.org/" - - # rarely-used.org should NOT be detected (only 1 usage) - rarely_used_prefixes = [k for k, v in namespaces.items() - if 'rarely-used.org' in v] - assert len(rarely_used_prefixes) == 0 - - -@pytest.fixture -def temp_ro_crate(): - """Create a temporary RO-Crate with unknown namespaces for testing.""" - crate = ROCrate() - - # Add entities with unknown namespaces - pokemon_entity = { - '@id': 'http://pokemon.org/pikachu', - '@type': 'http://pokemon.org/ElectricPokemon', - 'http://pokemon.org/pokemonName': 'Pikachu', - 'http://pokemon.org/type': 'Electric', - 'https://schema.org/name': 'Pikachu the Electric Mouse' - } - - villain_entity = { - '@id': 'http://villains.org/team_rocket', - '@type': 'http://villains.org/CriminalOrganization', - 'http://villains.org/motto': 'Prepare for trouble!', - 'https://schema.org/name': 'Team Rocket' - } - - crate.add_jsonld(pokemon_entity) - crate.add_jsonld(villain_entity) - - return crate - - -class TestRoundTripNamespaces: - """Test namespace handling through full import/export cycles.""" - - def test_rocrate_roundtrip_with_unknown_namespaces(self, temp_ro_crate): - """Test that unknown namespaces survive import/export cycles.""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - # Export original crate - temp_ro_crate.metadata.write(temp_path) - metadata_file = temp_path / 'ro-crate-metadata.json' - original_data = json.loads(metadata_file.read_text()) - - # Verify original contains full URIs - original_entities = original_data.get('@graph', []) - pokemon_entities = [e for e in original_entities - if 'pokemon.org' in e.get('@id', '')] - assert len(pokemon_entities) >= 1 - - # Import via SchemaFacade - imported_facade = SchemaFacade.from_ro_crate(temp_path) - assert len(imported_facade.metadata_entries) > 0 - - # Re-export and check context - final_crate = imported_facade.get_crate() - - with tempfile.TemporaryDirectory() as final_dir: - final_crate.metadata.write(final_dir) - final_metadata_file = Path(final_dir) / 'ro-crate-metadata.json' - final_data = json.loads(final_metadata_file.read_text()) - - # Check that some form of context enhancement occurred - final_context = final_data.get('@context', []) - assert isinstance(final_context, list) - if len(final_context) > 1: - assert isinstance(final_context[1], dict) - # Should have some namespace mappings - assert len(final_context[1]) > 0 - - -if __name__ == "__main__": - pytest.main([__file__]) \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/uv.lock b/0.2.x/lib/python/lib-ro-crate-schema/uv.lock index 948b1fc..1616c87 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/uv.lock +++ b/0.2.x/lib/python/lib-ro-crate-schema/uv.lock @@ -90,6 +90,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/32/e6/7c4006cf689ed7a4aa75dcf1f14acbc04e585714c220b5cc6d231096685a/coverage-7.11.2.tar.gz", hash = "sha256:ae43149b7732df15c3ca9879b310c48b71d08cd8a7ba77fda7f9108f78499e93", size = 814849, upload-time = "2025-11-08T20:26:33.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/00/57f3f8adaced9e4c74f482932e093176df7e400b4bb95dc1f3cd499511b5/coverage-7.11.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:38a5509fe7fabb6fb3161059b947641753b6529150ef483fc01c4516a546f2ad", size = 217125, upload-time = "2025-11-08T20:24:51.368Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/ff1a55673161608c895080950cdfbb6485c95e6fa57a92d2cd1e463717b3/coverage-7.11.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7e01ab8d69b6cffa2463e78a4d760a6b69dfebe5bf21837eabcc273655c7e7b3", size = 217499, upload-time = "2025-11-08T20:24:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/eaac01709ffbef291a12ca2526b6247f55ab17724e2297cc70921cd9a81f/coverage-7.11.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4776c6555a9f378f37fa06408f2e1cc1d06e4c4e06adb3d157a4926b549efbe", size = 248479, upload-time = "2025-11-08T20:24:54.825Z" }, + { url = "https://files.pythonhosted.org/packages/75/25/d846d2d08d182eeb30d1eba839fabdd9a3e6c710a1f187657b9c697bab23/coverage-7.11.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f70fa1ef17cba5dada94e144ea1b6e117d4f174666842d1da3aaf765d6eb477", size = 251074, upload-time = "2025-11-08T20:24:56.442Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7a/34c9402ad12bce609be4be1146a7d22a7fae8e9d752684b6315cce552a65/coverage-7.11.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:811bff1f93566a8556a9aeb078bd82573e37f4d802a185fba4cbe75468615050", size = 252318, upload-time = "2025-11-08T20:24:57.987Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2f/292fe3cea4cc1c4b8fb060fa60e565ab1b3bfc67bda74bedefb24b4a2407/coverage-7.11.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d0e80c9946da61cc0bf55dfd90d65707acc1aa5bdcb551d4285ea8906255bb33", size = 248641, upload-time = "2025-11-08T20:24:59.642Z" }, + { url = "https://files.pythonhosted.org/packages/c5/af/33ccb2aa2f43bbc330a1fccf84a396b90f2e61c00dccb7b72b2993a3c795/coverage-7.11.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:10f10c9acf584ef82bfaaa7296163bd11c7487237f1670e81fc2fa7e972be67b", size = 250457, upload-time = "2025-11-08T20:25:01.358Z" }, + { url = "https://files.pythonhosted.org/packages/bd/91/4b5b58f34e0587fbc5c1a28d644d9c20c13349c1072aea507b6e372c8f20/coverage-7.11.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fd3f7cc6cb999e3eff91a2998a70c662b0fcd3c123d875766147c530ca0d3248", size = 248421, upload-time = "2025-11-08T20:25:02.895Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d5/5c5ed220b15f490717522d241629c522fa22275549a6ccfbc96a3654b009/coverage-7.11.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e52a028a56889d3ad036c0420e866e4a69417d3203e2fc5f03dcb8841274b64c", size = 248244, upload-time = "2025-11-08T20:25:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/1e/27/504088aba40735132db838711d966e1314931ff9bddcd0e2ea6bc7e345a7/coverage-7.11.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f6f985e175dfa1fb8c0a01f47186720ae25d5e20c181cc5f3b9eba95589b8148", size = 250004, upload-time = "2025-11-08T20:25:06.633Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/4d61c0ad0d39656bd5e73fe41a93a34b063c90333258e6307aadcfcdbb97/coverage-7.11.2-cp313-cp313-win32.whl", hash = "sha256:e48b95abe2983be98cdf52900e07127eb7fe7067c87a700851f4f1f53d2b00e6", size = 219639, upload-time = "2025-11-08T20:25:08.27Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a7/a298afa025ebe7a2afd6657871a1ac2d9c49666ce00f9a35ee9df61a3bd8/coverage-7.11.2-cp313-cp313-win_amd64.whl", hash = "sha256:ea910cc737ee8553c81ad5c104bc5b135106ebb36f88be506c3493e001b4c733", size = 220445, upload-time = "2025-11-08T20:25:09.906Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a1/1825f5eadc0a0a6ea1c6e678827e1ec8c0494dbd23270016fccfc3358fbf/coverage-7.11.2-cp313-cp313-win_arm64.whl", hash = "sha256:ef2d3081562cd83f97984a96e02e7a294efa28f58d5e7f4e28920f59fd752b41", size = 219077, upload-time = "2025-11-08T20:25:11.777Z" }, + { url = "https://files.pythonhosted.org/packages/c0/61/98336c6f4545690b482e805c3a1a83fb2db4c19076307b187db3d421b5b3/coverage-7.11.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:87d7c7b0b2279e174f36d276e2afb7bf16c9ea04e824d4fa277eea1854f4cfd4", size = 217818, upload-time = "2025-11-08T20:25:13.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/ee/6dca6e5f1a4affba8d3224996d0e9145e6d67817da753cc436e48bb8d0e6/coverage-7.11.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:940d195f4c8ba3ec6e7c302c9f546cdbe63e57289ed535452bc52089b1634f1c", size = 218170, upload-time = "2025-11-08T20:25:15.284Z" }, + { url = "https://files.pythonhosted.org/packages/ec/17/9c9ca3ef09d3576027e77cf580eb599d8d655f9ca2456a26ca50c53e07e3/coverage-7.11.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3b92e10ca996b5421232dd6629b9933f97eb57ce374bca800ab56681fbeda2b", size = 259466, upload-time = "2025-11-08T20:25:17.373Z" }, + { url = "https://files.pythonhosted.org/packages/53/96/2001a596827a0b91ba5f627f21b5ce998fa1f27d861a8f6d909f5ea663ff/coverage-7.11.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:61d6a7cc1e7a7a761ac59dcc88cee54219fd4231face52bd1257cfd3df29ae9f", size = 261530, upload-time = "2025-11-08T20:25:19.085Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bb/fea7007035fdc3c40fcca0ab740da549ff9d38fa50b0d37cd808fbbf9683/coverage-7.11.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bee1911c44c52cad6b51d436aa8c6ff5ca5d414fa089c7444592df9e7b890be9", size = 263963, upload-time = "2025-11-08T20:25:21.168Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b3/7452071353441b632ebea42f6ad328a7ab592e4bc50a31f9921b41667017/coverage-7.11.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4c4423ea9c28749080b41e18ec74d658e6c9f148a6b47e719f3d7f56197f8227", size = 258644, upload-time = "2025-11-08T20:25:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/e6/05/6e56b1c2b3308f587508ad4b0a4cb76c8d6179fea2df148e071979b3eb77/coverage-7.11.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:689d3b4dd0d4c912ed8bfd7a1b5ff2c5ecb1fa16571840573174704ff5437862", size = 261539, upload-time = "2025-11-08T20:25:25.277Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/7afeeac2a49f651318e4a83f1d5f4d3d4f4092f1d451ac4aec8069cddbdb/coverage-7.11.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:75ef769be19d69ea71b0417d7fbf090032c444792579cdf9b166346a340987d5", size = 259153, upload-time = "2025-11-08T20:25:28.098Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/08f3b5c7500b2031cee74e8a01f9a5bc407f781ff6a826707563bb9dd5b7/coverage-7.11.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6681164bc697b93676945c8c814b76ac72204c395e11b71ba796a93b33331c24", size = 258043, upload-time = "2025-11-08T20:25:30.087Z" }, + { url = "https://files.pythonhosted.org/packages/ca/49/8e080e7622bd7c82df0f8324bbe0461ed1032a638b80046f1a53a88ea3a8/coverage-7.11.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4aa799c61869318d2b86c0d3c413d6805546aec42069f009cbb27df2eefb2790", size = 260243, upload-time = "2025-11-08T20:25:31.722Z" }, + { url = "https://files.pythonhosted.org/packages/dc/75/da033d8589661527b4a6d30c414005467e48fbccc0f3c10898af183e14e1/coverage-7.11.2-cp313-cp313t-win32.whl", hash = "sha256:9a6468e1a3a40d3d1f9120a9ff221d3eacef4540a6f819fff58868fe0bd44fa9", size = 220309, upload-time = "2025-11-08T20:25:33.9Z" }, + { url = "https://files.pythonhosted.org/packages/29/ef/8a477d41dbcde1f1179c13c43c9f77ee926b793fe3e5f1cf5d868a494679/coverage-7.11.2-cp313-cp313t-win_amd64.whl", hash = "sha256:30c437e8b51ce081fe3903c9e368e85c9a803b093fd062c49215f3bf4fd1df37", size = 221374, upload-time = "2025-11-08T20:25:35.88Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a3/4c3cdd737ed1f630b821430004c2d5f1088b9bc0a7115aa5ad7c40d7d5cb/coverage-7.11.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a35701fe0b5ee9d4b67d31aa76555237af32a36b0cf8dd33f8a74470cf7cd2f5", size = 219648, upload-time = "2025-11-08T20:25:37.572Z" }, + { url = "https://files.pythonhosted.org/packages/52/d1/43d17c299249085d6e0df36db272899e92aa09e68e27d3e92a4cf8d9523e/coverage-7.11.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7f933bc1fead57373922e383d803e1dd5ec7b5a786c220161152ebee1aa3f006", size = 217170, upload-time = "2025-11-08T20:25:39.254Z" }, + { url = "https://files.pythonhosted.org/packages/78/66/f21c03307079a0b7867b364af057430018a3d4a18ed1b99e1adaf5a0f305/coverage-7.11.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f80cb5b328e870bf3df0568b41643a85ee4b8ccd219a096812389e39aa310ea4", size = 217497, upload-time = "2025-11-08T20:25:41.277Z" }, + { url = "https://files.pythonhosted.org/packages/f0/dd/0a2257154c32f442fe3b4622501ab818ae4bd7cde33bd7a740630f6bd24c/coverage-7.11.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6b2498f86f2554ed6cb8df64201ee95b8c70fb77064a8b2ae8a7185e7a4a5f0", size = 248539, upload-time = "2025-11-08T20:25:43.349Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ca/c55ab0ee5ebfc4ab56cfc1b3585cba707342dc3f891fe19f02e07bc0c25f/coverage-7.11.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a913b21f716aa05b149a8656e9e234d9da04bc1f9842136ad25a53172fecc20e", size = 251057, upload-time = "2025-11-08T20:25:45.083Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/a149b88ebe714b76d95427d609e629446d1df5d232f4bdaec34e471da124/coverage-7.11.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5769159986eb174f0f66d049a52da03f2d976ac1355679371f1269e83528599", size = 252393, upload-time = "2025-11-08T20:25:47.272Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a4/a992c805e95c46f0ac1b83782aa847030cb52bbfd8fc9015cff30f50fb9e/coverage-7.11.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89565d7c9340858424a5ca3223bfefe449aeb116942cdc98cd76c07ca50e9db8", size = 248534, upload-time = "2025-11-08T20:25:49.034Z" }, + { url = "https://files.pythonhosted.org/packages/78/01/318ed024ae245dbc76152bc016919aef69c508a5aac0e2da5de9b1efea61/coverage-7.11.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b7fc943097fa48de00d14d2a2f3bcebfede024e031d7cd96063fe135f8cbe96e", size = 250412, upload-time = "2025-11-08T20:25:51.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f9/f05c7984ef48c8d1c6c1ddb243223b344dcd8c6c0d54d359e4e325e2fa7e/coverage-7.11.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:72a3d109ac233666064d60b29ae5801dd28bc51d1990e69f183a2b91b92d4baf", size = 248367, upload-time = "2025-11-08T20:25:53.399Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ac/461ed0dcaba0c727b760057ffa9837920d808a35274e179ff4a94f6f755a/coverage-7.11.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4648c90cf741fb61e142826db1557a44079de0ca868c5c5a363c53d852897e84", size = 248187, upload-time = "2025-11-08T20:25:55.402Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bf/8510ce8c7b1a8d682726df969e7523ee8aac23964b2c8301b8ce2400c1b4/coverage-7.11.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f1aa017b47e1879d7bac50161b00d2b886f2ff3882fa09427119e1b3572ede1", size = 249849, upload-time = "2025-11-08T20:25:57.186Z" }, + { url = "https://files.pythonhosted.org/packages/75/6f/ea1c8990ca35d607502c9e531f164573ea59bb6cd5cd4dc56d7cc3d1fcb5/coverage-7.11.2-cp314-cp314-win32.whl", hash = "sha256:44b6e04bb94e59927a2807cd4de86386ce34248eaea95d9f1049a72f81828c38", size = 219908, upload-time = "2025-11-08T20:25:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/1e/04/a64e2a8b9b65ae84670207dc6073e3d48ee9192646440b469e9b8c335d1f/coverage-7.11.2-cp314-cp314-win_amd64.whl", hash = "sha256:7ea36e981a8a591acdaa920704f8dc798f9fff356c97dbd5d5702046ae967ce1", size = 220724, upload-time = "2025-11-08T20:26:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/73/df/eb4e9f9d0d55f7ec2b55298c30931a665c2249c06e3d1d14c5a6df638c77/coverage-7.11.2-cp314-cp314-win_arm64.whl", hash = "sha256:4aaf2212302b6f748dde596424b0f08bc3e1285192104e2480f43d56b6824f35", size = 219296, upload-time = "2025-11-08T20:26:02.918Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b5/e9bb3b17a65fe92d1c7a2363eb5ae9893fafa578f012752ed40eee6aa3c8/coverage-7.11.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84e8e0f5ab5134a2d32d4ebadc18b433dbbeddd0b73481f816333b1edd3ff1c8", size = 217905, upload-time = "2025-11-08T20:26:04.633Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/1f38dd0b63a9d82fb3c9d7fbe1c9dab26ae77e5b45e801d129664e039034/coverage-7.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5db683000ff6217273071c752bd6a1d341b6dc5d6aaa56678c53577a4e70e78a", size = 218172, upload-time = "2025-11-08T20:26:06.677Z" }, + { url = "https://files.pythonhosted.org/packages/fd/5d/2aeb513c6841270783b216478c6edc65b128c6889850c5f77568aa3a3098/coverage-7.11.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2970c03fefee2a5f1aebc91201a0706a7d0061cc71ab452bb5c5345b7174a349", size = 259537, upload-time = "2025-11-08T20:26:08.481Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/ddd9b22ec1b5c69cc579b149619c354f981aaaafc072b92574f2d3d6c267/coverage-7.11.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9f28b900d96d83e2ae855b68d5cf5a704fa0b5e618999133fd2fb3bbe35ecb1", size = 261648, upload-time = "2025-11-08T20:26:10.551Z" }, + { url = "https://files.pythonhosted.org/packages/29/e2/8743b7281decd3f73b964389fea18305584dd6ba96f0aff91b4880b50310/coverage-7.11.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8b9a7ebc6a29202fb095877fd8362aab09882894d1c950060c76d61fb116114", size = 264061, upload-time = "2025-11-08T20:26:12.306Z" }, + { url = "https://files.pythonhosted.org/packages/00/1b/46daea7c4349c4530c62383f45148cc878845374b7a632e3ac2769b2f26a/coverage-7.11.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f8f6bcaa7fe162460abb38f7a5dbfd7f47cfc51e2a0bf0d3ef9e51427298391", size = 258580, upload-time = "2025-11-08T20:26:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/d7/53/f9b1c2d921d585dd6499e05bd71420950cac4e800f71525eb3d2690944fe/coverage-7.11.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:461577af3f8ad4da244a55af66c0731b68540ce571dbdc02598b5ec9e7a09e73", size = 261526, upload-time = "2025-11-08T20:26:16.353Z" }, + { url = "https://files.pythonhosted.org/packages/86/7d/55acee453a71a71b08b05848d718ce6ac4559d051b4a2c407b0940aa72be/coverage-7.11.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5b284931d57389ec97a63fb1edf91c68ec369cee44bc40b37b5c3985ba0a2914", size = 259135, upload-time = "2025-11-08T20:26:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3f/cf1e0217efdebab257eb0f487215fe02ff2b6f914cea641b2016c33358e1/coverage-7.11.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2ca963994d28e44285dc104cf94b25d8a7fd0c6f87cf944f46a23f473910703f", size = 257959, upload-time = "2025-11-08T20:26:19.894Z" }, + { url = "https://files.pythonhosted.org/packages/68/0e/e9be33e55346e650c3218a313e888df80418415462c63bceaf4b31e36ab5/coverage-7.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7d3fccd5781c5d29ca0bd1ea272630f05cd40a71d419e7e6105c0991400eb14", size = 260290, upload-time = "2025-11-08T20:26:22.05Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1d/9e93937c2a9bd255bb5efeff8c5df1c8322e508371f76f21a58af0e36a31/coverage-7.11.2-cp314-cp314t-win32.whl", hash = "sha256:f633da28958f57b846e955d28661b2b323d8ae84668756e1eea64045414dbe34", size = 220691, upload-time = "2025-11-08T20:26:24.043Z" }, + { url = "https://files.pythonhosted.org/packages/bf/30/893b5a67e2914cf2be8e99c511b8084eaa8c0585e42d8b3cd78208f5f126/coverage-7.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:410cafc1aba1f7eb8c09823d5da381be30a2c9b3595758a4c176fcfc04732731", size = 221800, upload-time = "2025-11-08T20:26:26.24Z" }, + { url = "https://files.pythonhosted.org/packages/2b/8b/6d93448c494a35000cc97d8d5d9c9b3774fa2b0c0d5be55f16877f962d71/coverage-7.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:595c6bb2b565cc2d930ee634cae47fa959dfd24cc0e8ae4cf2b6e7e131e0d1f7", size = 219838, upload-time = "2025-11-08T20:26:28.479Z" }, + { url = "https://files.pythonhosted.org/packages/05/7a/99766a75c88e576f47c2d9a06416ff5d95be9b42faca5c37e1ab77c4cd1a/coverage-7.11.2-py3-none-any.whl", hash = "sha256:2442afabe9e83b881be083238bb7cf5afd4a10e47f29b6094470338d2336b33c", size = 208891, upload-time = "2025-11-08T20:26:30.739Z" }, +] + [[package]] name = "frozendict" version = "2.4.6" @@ -119,6 +180,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -133,26 +203,33 @@ wheels = [ [[package]] name = "lib-ro-crate-schema" -version = "0.1.0" +version = "0.2.1" source = { editable = "." } dependencies = [ { name = "pydantic" }, - { name = "pydantic-rdf" }, { name = "pyld" }, { name = "pyshacl" }, - { name = "rdflib-jsonld" }, + { name = "rdflib" }, { name = "rocrate" }, ] +[package.optional-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] + [package.metadata] requires-dist = [ { name = "pydantic", specifier = ">=2.11.7" }, - { name = "pydantic-rdf", specifier = ">=0.2.0" }, { name = "pyld", specifier = ">=2.0.4" }, { name = "pyshacl", specifier = ">=0.30.1" }, - { name = "rdflib-jsonld", specifier = ">=0.6.2" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, + { name = "rdflib", specifier = ">=7.1.4" }, { name = "rocrate", specifier = ">=0.14.0" }, ] +provides-extras = ["dev"] [[package]] name = "lxml" @@ -227,6 +304,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "prettytable" version = "3.16.0" @@ -283,16 +369,12 @@ wheels = [ ] [[package]] -name = "pydantic-rdf" -version = "0.2.0" +name = "pygments" +version = "2.19.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "rdflib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9a/5a/1231b6e90cff4ddb6fa44c159593af737b45dc1d4738c63faa33ebe0e0be/pydantic_rdf-0.2.0.tar.gz", hash = "sha256:e1d9055cb6957f85957af6855fe7d99ded59025e2ac4fa35a5ca8e922ec9117f", size = 67412, upload-time = "2025-05-03T02:07:11.568Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/e4/ad85698626ec4d6d8ee3cd569a78191ef7ab1d6d729a346dadd6d445429f/pydantic_rdf-0.2.0-py3-none-any.whl", hash = "sha256:603b3b62b00970655c87bc4b7b3fa415679ffb585d4b8605127ecb4d20f1ad5c", size = 8831, upload-time = "2025-05-03T02:07:10.706Z" }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] @@ -333,6 +415,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/12/3747eff45147d416c054881b7c0902b21ac7767b905f666c714233688c81/pyshacl-0.30.1-py3-none-any.whl", hash = "sha256:d7e0c21b25e948bb643dbc5db6258da64a90a8ac89055c1fe562b469031072aa", size = 1290522, upload-time = "2025-03-14T23:28:12.72Z" }, ] +[[package]] +name = "pytest" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/1d/eb34f286b164c5e431a810a38697409cca1112cee04b287bb56ac486730b/pytest-9.0.0.tar.gz", hash = "sha256:8f44522eafe4137b0f35c9ce3072931a788a21ee40a2ed279e817d3cc16ed21e", size = 1562764, upload-time = "2025-11-08T17:25:33.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl", hash = "sha256:e5ccdf10b0bac554970ee88fc1a4ad0ee5d221f8ef22321f9b7e4584e19d7f96", size = 373364, upload-time = "2025-11-08T17:25:31.811Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -362,18 +474,6 @@ html = [ { name = "html5rdf" }, ] -[[package]] -name = "rdflib-jsonld" -version = "0.6.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rdflib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/1a/627de985dffc11b486eb07be86dc9a16c25b4877905f5f6a0be3633addb0/rdflib-jsonld-0.6.2.tar.gz", hash = "sha256:107cd3019d41354c31687e64af5e3fd3c3e3fa5052ce635f5ce595fd31853a63", size = 12449, upload-time = "2021-09-18T03:04:27.881Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/92/da92898b2aab0da78207afc9c035a71bedef3544966374c44e9627d761c5/rdflib_jsonld-0.6.2-py2.py3-none-any.whl", hash = "sha256:011afe67672353ca9978ab9a4bee964dff91f14042f2d8a28c22a573779d2f8b", size = 4029, upload-time = "2021-09-18T03:04:26.34Z" }, -] - [[package]] name = "requests" version = "2.32.4"