mirror of
https://github.com/researchobjectschema/ro-crate-interoperability-profile.git
synced 2026-06-06 17:28:42 +02:00
Cleanup and adjustment to depreciated features
Added Manifest for publishing
This commit is contained in:
@@ -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.**
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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/
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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():
|
||||
|
||||
@@ -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(...)")
|
||||
@@ -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()
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
-2
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
test_basic_export()
|
||||
test_json_ld_structure()
|
||||
print("\n✅ All export tests passed!")
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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!")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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__])
|
||||
+125
-25
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user