grimoire-model


Namegrimoire-model JSON
Version 0.3.3 PyPI version JSON
download
home_pageNone
SummaryDict-like model system with schema validation, derived fields, and inheritance for GRIMOIRE
upload_time2025-10-13 02:53:22
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseMIT
keywords gaming rpg tabletop model validation schema
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Grimoire Model

[![Tests](https://github.com/wyrdbound/grimoire-model/workflows/Tests/badge.svg)](https://github.com/wyrdbound/grimoire-model/actions)
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![Coverage](https://img.shields.io/badge/coverage-88%25-green.svg)](htmlcov/index.html)

**Dict-like model system with schema validation, derived fields, and inheritance for the Grimoire tabletop RPG engine.**

Grimoire Model provides a sophisticated, schema-driven model system that combines the familiar dict-like interface with powerful features like automatic field derivation, template-based expressions, model inheritance, and comprehensive validation. Designed to integrate seamlessly with `grimoire-context` for complete game state management.

## โœจ Features

- **๐Ÿ“š Dict-like Interface**: Familiar Python dictionary operations with schema validation
- **โœจ Dual Access Pattern**: Support for both dictionary-style (`obj['field']`) and attribute-style (`obj.field`) access
- **๐Ÿ”„ Reactive Derived Fields**: Automatic computation with dependency tracking and batch updates
- **๐Ÿงฌ Model Inheritance**: Multiple inheritance support with automatic namespace-based resolution
- **๐Ÿ“ Template Expressions**: Jinja2-powered field templates for dynamic content
- **๐ŸŽจ Template Engine Compatible**: Works seamlessly with Jinja2, Django templates, and other engines
- **๐Ÿท๏ธ Namespace Organization**: Global model registry with namespace-based organization
- **๐Ÿ›ก๏ธ Schema Validation**: Pydantic-based type checking and custom validation rules
- **๐Ÿ”ง Dependency Injection**: Pluggable resolvers for extensibility
- **โšก Performance Optimized**: Efficient batch updates and lazy evaluation
- **๐ŸŽฏ grimoire-context Integration**: Seamless interoperability with context management

## ๐Ÿš€ Quick Start

### Installation

```bash
pip install grimoire-model
```

### Basic Usage

```python
from grimoire_model import ModelDefinition, AttributeDefinition, create_model

# Define a character model schema
character_def = ModelDefinition(
    id="character",
    name="Player Character",
    namespace="rpg",  # Organize models in namespaces
    attributes={
        "name": AttributeDefinition(type="str", required=True),
        "level": AttributeDefinition(type="int", default=1),
        "hp": AttributeDefinition(type="int", default=100),
        "mp": AttributeDefinition(type="int", default=50),

        # Derived fields automatically update when dependencies change
        "max_hp": AttributeDefinition(
            type="int",
            derived="{{ level * 8 + hp }}"
        ),
        "character_summary": AttributeDefinition(
            type="str",
            derived="Level {{ level }} {{ name }} ({{ max_hp }} HP, {{ mp }} MP)"
        )
    }
)

# Create a character instance
character = create_model(character_def, {
    "name": "Aragorn",
    "level": 15,
    "hp": 120,
    "mp": 80
})

# Access data using BOTH dictionary-style AND attribute-style notation
print(character['name'])        # "Aragorn" (dictionary-style)
print(character.name)           # "Aragorn" (attribute-style)
print(character.max_hp)         # 240 (15 * 8 + 120)
print(character.character_summary)  # "Level 15 Aragorn (240 HP, 80 MP)"

# Updates work with both access patterns
character['level'] = 20         # Dictionary-style update
character.level = 20            # Attribute-style update (same result)
print(character.max_hp)         # 280 (automatically recalculated)
```

### Custom Primitive Types

Register domain-specific primitive types that should be treated as primitive values rather than complex model objects:

```python
from grimoire_model import register_primitive_type, ModelDefinition, AttributeDefinition, create_model_without_validation

# Register custom primitive types
register_primitive_type('roll')       # Dice roll notation
register_primitive_type('duration')   # Time periods
register_primitive_type('distance')   # Measurements

# Define a model using custom primitive types
weapon_def = ModelDefinition(
    id='weapon',
    name='Weapon',
    attributes={
        'name': AttributeDefinition(type='str', required=True),
        'damage': AttributeDefinition(type='roll', required=True)  # Custom primitive
    }
)

# Create model - custom primitives work like built-in types
weapon = create_model_without_validation(weapon_def, {
    'name': 'Longsword',
    'damage': '1d8'  # Stored as-is, like a string
})

print(weapon['damage'])  # '1d8'
```

Custom primitive types:
- Are stored as raw values without instantiation
- Don't require model registration
- Can have optional validators
- Support domain-specific type semantics
- Work in derived field templates

### Global Model Registry

Models are automatically registered in a global registry using namespaces:

```python
from grimoire_model import get_model

# Models auto-register when created
character_def = ModelDefinition(
    id="character",
    namespace="rpg",  # Registered in "rpg" namespace
    # ... attributes ...
)

# Retrieve from anywhere in your application
retrieved_def = get_model("rpg", "character")
new_character = create_model(retrieved_def, {"name": "Hero"})

# Perfect for inheritance - child models automatically find parents
base_def = ModelDefinition(id="base", namespace="rpg", ...)
child_def = ModelDefinition(id="child", namespace="rpg", extends=["base"], ...)
# No manual registry needed - inheritance resolves automatically!
```

### Model Inheritance with Namespaces

```python
from grimoire_model import get_model, clear_registry

# Base entity definition (auto-registered in namespace)
base_entity_def = ModelDefinition(
    id="base_entity",
    name="Base Entity",
    namespace="game",  # Registered in "game" namespace
    attributes={
        "id": AttributeDefinition(type="str", required=True),
        "name": AttributeDefinition(type="str", required=True),
        "description": AttributeDefinition(type="str", default="")
    }
)

# Character extends base entity (automatic inheritance resolution)
character_def = ModelDefinition(
    id="character",
    name="Character",
    namespace="game",  # Same namespace enables automatic inheritance
    extends=["base_entity"],  # Automatically finds base_entity in namespace
    attributes={
        "level": AttributeDefinition(type="int", default=1),
        "hp": AttributeDefinition(type="int", default=100),
        "max_hp": AttributeDefinition(
            type="int",
            derived="{{ level * 8 + hp }}"
        )
    }
)

# Create character with inherited fields (no registry needed!)
character = create_model(
    character_def,
    {
        "id": "char_001",          # From base_entity
        "name": "Legolas",         # From base_entity
        "description": "Elf archer", # From base_entity
        "level": 12,               # From character
        "hp": 96                   # From character
    }
)

print(character['id'])          # "char_001" (inherited)
print(character['name'])        # "Legolas" (inherited)
print(character['max_hp'])      # 192 (derived field)

# Retrieve models from global registry
retrieved_char_def = get_model("game", "character")
another_character = create_model(retrieved_char_def, {
    "id": "char_002",
    "name": "Gimli"
})
```

### Integration with grimoire-context

```python
from grimoire_context import GrimoireContext

# Create context with character model
context = GrimoireContext({
    'party': {
        'leader': character,
        'members': 4
    }
})

# Modify character through context - derived fields update automatically
context = context.set_variable('party.leader.level', 25)
updated_character = context.get_variable('party.leader')

print(updated_character['level'])   # 25
print(updated_character['max_hp'])  # 296 (automatically recalculated)
```

### Batch Updates for Performance

```python
# Batch multiple changes for better performance
character.batch_update({
    'level': 30,
    'hp': 150,
    'mp': 120
})

# All derived fields updated once after batch completion
print(character['max_hp'])  # 390 (30 * 8 + 150)
```

### Template Engine Integration

GrimoireModel objects support both dictionary-style and attribute-style access, making them fully compatible with template engines like Jinja2, Django templates, and others:

```python
from jinja2 import Template

# Create a weapon model
weapon_def = ModelDefinition(
    id="weapon",
    name="Weapon",
    attributes={
        "name": AttributeDefinition(type="str", required=True),
        "damage": AttributeDefinition(type="str", required=True),
        "bonus": AttributeDefinition(type="int", default=0),
    }
)

weapon = create_model(weapon_def, {
    "name": "Longsword",
    "damage": "1d8",
    "bonus": 2
})

# Use attribute access in Jinja2 templates
template = Template("{{ weapon.name }}: {{ weapon.damage }} +{{ weapon.bonus }}")
result = template.render(weapon=weapon)
print(result)  # "Longsword: 1d8 +2"

# Works with more complex templates
template = Template("""
{% if weapon.bonus > 0 %}
  {{ weapon.name }} ({{ weapon.damage }}+{{ weapon.bonus }})
{% else %}
  {{ weapon.name }} ({{ weapon.damage }})
{% endif %}
""")
```

This dual-access pattern (dictionary and attribute) provides:
- **Template Compatibility**: Works seamlessly with Jinja2, Django, and other template engines
- **Standard Python Behavior**: Objects behave like normal Python objects
- **IDE Support**: Better autocomplete and type hints
- **Backward Compatible**: All existing dictionary-style code continues to work

## ๐Ÿ“š Documentation

- **[Logging Configuration](LOGGING.md)** - Configure library logging output and integration

## ๐Ÿ“š Core Concepts

### Model Definitions

Model definitions are schemas that describe the structure, types, and behavior of your data:

```python
model_def = ModelDefinition(
    id="weapon",
    name="Weapon",
    namespace="combat",  # Organize in combat namespace
    description="Combat weapon with damage calculations",
    attributes={
        "name": AttributeDefinition(type="str", required=True),
        "base_damage": AttributeDefinition(type="int", default=1, range="1..50"),
        "enhancement": AttributeDefinition(type="int", default=0, range="0..10"),

        # Derived field with complex logic
        "total_damage": AttributeDefinition(
            type="int",
            derived="{{ base_damage + enhancement * 2 }}"
        ),
        "damage_category": AttributeDefinition(
            type="str",
            derived="{% if total_damage >= 20 %}High{% elif total_damage >= 10 %}Medium{% else %}Low{% endif %}"
        )
    },
    validations=[
        ValidationRule(
            expression="base_damage > 0",
            message="Base damage must be positive"
        )
    ]
)
```

### Template Expressions

Use Jinja2 templates for powerful derived field logic:

```python
# Simple expression
"max_hp": "{{ level * 8 + constitution * 2 }}"

# Conditional logic
"damage_bonus": "{% if strength >= 15 %}{{ (strength - 10) // 2 }}{% else %}0{% endif %}"

# Complex calculations
"skill_modifier": "{{ (skill_level + attribute_bonus - 10) // 2 }}"
```

### Validation Rules

Add custom validation logic to ensure data integrity:

```python
ValidationRule(
    expression="level >= 1 and level <= 100",
    message="Character level must be between 1 and 100"
),
ValidationRule(
    expression="hp > 0 or status == 'dead'",
    message="Living characters must have positive HP"
)
```

## ๐Ÿ”ง API Reference

### Core Classes

#### ModelDefinition

```python
ModelDefinition(
    id: str,                                    # Unique model identifier
    name: str,                                  # Human-readable name
    namespace: str = "default",                 # Namespace for organization and inheritance
    description: str = "",                      # Model description
    attributes: Dict[str, AttributeDefinition], # Field definitions
    extends: List[str] = None,                  # Parent model IDs (resolved in namespace)
    validations: List[ValidationRule] = None    # Validation rules
)
```

#### AttributeDefinition

```python
AttributeDefinition(
    type: str,                    # Data type (str, int, float, bool, list, dict, or custom primitive)
    required: bool = False,       # Whether field is required
    default: Any = None,          # Default value
    derived: str = None,          # Template expression for derived fields
    range: str = None,            # Value range constraint (e.g., "1..100")
    enum: List[Any] = None,       # Allowed values
    pattern: str = None,          # Regex pattern for strings
    description: str = ""         # Field description
)
```

#### GrimoireModel

```python
class GrimoireModel(MutableMapping):
    def __init__(
        self,
        model_definition: ModelDefinition,
        data: Dict[str, Any] = None,
        template_resolver: TemplateResolver = None,
        derived_field_resolver: DerivedFieldResolver = None,
        **kwargs
    )

    # Dict-like interface
    def __getitem__(self, key: str) -> Any
    def __setitem__(self, key: str, value: Any) -> None
    def __delitem__(self, key: str) -> None
    def __iter__(self) -> Iterator[str]
    def __len__(self) -> int
    def keys(), values(), items()

    # Attribute-style access (NEW in 0.3.2)
    def __getattr__(self, name: str) -> Any
    def __setattr__(self, name: str, value: Any) -> None
    # Enables: obj.field_name (read) and obj.field_name = value (write)

    # Batch operations
    def batch_update(self, updates: Dict[str, Any]) -> None

    # Path operations (dot notation)
    def get(self, path: str, default: Any = None) -> Any
    def set(self, path: str, value: Any) -> None
    def has(self, path: str) -> bool
    def delete(self, path: str) -> None
```

### Factory Functions

#### create_model

```python
def create_model(
    model_definition: ModelDefinition,
    data: Dict[str, Any] = None,
    template_resolver_type: str = "jinja2",
    derived_field_resolver_type: str = "batched",
    **kwargs
) -> GrimoireModel
```

Creates a model instance with default resolvers. Inheritance is automatically resolved from the global model registry using namespaces.

### Global Registry Functions

```python
from grimoire_model import register_model, get_model, clear_registry

# Register model manually (usually automatic)
register_model("my_namespace", "my_model", model_definition)

# Retrieve model from registry
model_def = get_model("my_namespace", "my_model")

# Clear all models (useful for testing)
clear_registry()

# Access registry directly for advanced operations
from grimoire_model import get_model_registry
registry = get_model_registry()
registry_dict = registry.get_registry_dict()
all_namespaces = registry.list_namespaces()
```

### Primitive Type Registry Functions

```python
from grimoire_model import (
    register_primitive_type,
    unregister_primitive_type,
    is_primitive_type,
    clear_primitive_registry
)

# Register a custom primitive type
register_primitive_type('roll')

# Register with optional validator
def validate_duration(value):
    if isinstance(value, str) and value.endswith('s'):
        return True, None
    return False, "Duration must end with 's'"

register_primitive_type('duration', validator=validate_duration)

# Check if a type is registered as primitive
is_primitive_type('roll')  # True
is_primitive_type('unknown')  # False

# Unregister a primitive type
unregister_primitive_type('roll')

# Clear all registered primitives (useful for testing)
clear_primitive_registry()
```

### Template Resolvers

- `Jinja2TemplateResolver`: Standard Jinja2 template syntax
- `ModelContextTemplateResolver`: Simple `$variable` substitution
- `CachingTemplateResolver`: Cached template compilation for performance

### Derived Field Resolvers

- `BatchedDerivedFieldResolver`: Batches updates for performance
- `DerivedFieldResolver`: Immediate update resolver

## ๐Ÿงช Development

### Setup

```bash
git clone https://github.com/wyrdbound/grimoire-model.git
cd grimoire-model
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
pip install -e ".[dev]"
```

### Running Tests

```bash
# Run all tests with coverage
/Users/justingaylor/src/grimoire-model/.venv/bin/python -m pytest --cov=grimoire_model --cov-report=term

# Run specific test file
/Users/justingaylor/src/grimoire-model/.venv/bin/python -m pytest tests/test_model.py

# Run with verbose output
/Users/justingaylor/src/grimoire-model/.venv/bin/python -m pytest -v

# Generate HTML coverage report
/Users/justingaylor/src/grimoire-model/.venv/bin/python -m pytest --cov=grimoire_model --cov-report=html
# Open htmlcov/index.html in browser
```

_Note: Use the virtual environment in the project root as specified in the development guidelines._

### Code Quality

```bash
# Install development dependencies
source .venv/bin/activate && pip install ruff mypy

# Linting and formatting
source .venv/bin/activate && ruff check .
source .venv/bin/activate && ruff format .

# Type checking
source .venv/bin/activate && mypy src/grimoire_model/

# Run all quality checks
source .venv/bin/activate && ruff check . && mypy src/grimoire_model/
```

### Running Examples

```bash
# Basic usage example
source .venv/bin/activate && python examples/01_basic_usage.py

# Advanced features and inheritance
source .venv/bin/activate && python examples/02_advanced_usage.py

# Inheritance and polymorphism
source .venv/bin/activate && python examples/03_inheritance_polymorphism.py

# Performance and integration testing
source .venv/bin/activate && python examples/04_performance_integration.py
```

## ๐Ÿ“‹ Requirements

- Python 3.8+
- pydantic >= 2.0.0
- pyrsistent >= 0.19.0
- jinja2 >= 3.1.0
- pyyaml >= 6.0

### Development Dependencies

- pytest >= 7.0.0
- pytest-cov >= 4.0.0
- pytest-mock >= 3.0.0
- hypothesis >= 6.0.0
- mypy >= 1.0.0
- ruff >= 0.1.0

## ๐ŸŽฏ Use Cases

Grimoire Model excels in scenarios requiring structured, validated data with complex relationships:

- **RPG Character Systems**: Stats, levels, equipment with derived values
- **Game Item Management**: Equipment, inventory, crafting systems
- **Rule Engine Data**: Complex game mechanics with interdependent calculations
- **Configuration Systems**: Hierarchical configs with inheritance and validation
- **Dynamic Content**: Template-based content generation with context awareness

## ๐Ÿ—๏ธ Architecture

The package follows clean architecture principles with clear separation of concerns:

- **Core Layer**: Model definitions, schemas, and the main GrimoireModel class
- **Resolver Layer**: Pluggable template and derived field resolution systems
- **Validation Layer**: Type checking, constraints, and custom validation rules
- **Utils Layer**: Inheritance resolution, path utilities, and helper functions
- **Integration Layer**: grimoire-context compatibility and factory functions

### Key Design Principles

1. **Dependency Injection**: All major components can be swapped via constructor injection
2. **Immutable Operations**: Uses pyrsistent for efficient immutable data structures
3. **Template-Driven**: Jinja2 templates provide powerful expression capabilities
4. **Performance-Focused**: Batch updates and lazy evaluation minimize overhead
5. **Type Safety**: Full type hints and Pydantic integration for runtime validation
6. **Explicit Errors**: Prefers explicit errors over fallbacks to maintain system stability

## ๐Ÿ“ˆ Performance

Current benchmarks (86% test coverage, 184 tests passing):

- **Model Creation**: ~1ms for simple models, ~5ms for complex inheritance
- **Field Updates**: ~0.1ms for direct fields, ~2ms for derived field cascades
- **Batch Updates**: 50-80% faster than individual updates for multiple fields
- **Memory Usage**: ~50KB per model instance (excluding data)
- **Template Resolution**: Cached compilation provides 10x speed improvement

## ๐Ÿ”„ Integration with grimoire-context

Seamless integration is automatically enabled when both packages are installed:

```python
from grimoire_model import create_model, ModelDefinition, AttributeDefinition
from grimoire_context import GrimoireContext

# Models work naturally in contexts
character = create_model(character_def, character_data)
context = GrimoireContext({'player': character})

# Context operations automatically handle model updates
updated_context = context.set_variable('player.level', 25)
updated_character = updated_context['player']

# Derived fields update automatically
print(updated_character['max_hp'])  # Recalculated based on new level
```

## ๐Ÿšจ Error Handling

The package provides a comprehensive exception hierarchy:

```python
from grimoire_model import (
    GrimoireModelError,           # Base exception
    ModelValidationError,          # Validation failures
    TemplateResolutionError,       # Template processing errors
    InheritanceError,              # Model inheritance issues
    DependencyError,               # Derived field dependency issues
    ConfigurationError             # Setup and configuration errors
)

try:
    character = create_model(character_def, invalid_data)
except ModelValidationError as e:
    print(f"Validation failed: {e}")
    print(f"Field: {e.field_name}")
    print(f"Value: {e.field_value}")
    print(f"Validation rule: {e.validation_rule}")
```

## ๐Ÿ” Advanced Features

### Custom Template Resolvers

```python
from grimoire_model.resolvers.template import TemplateResolver

class CustomTemplateResolver(TemplateResolver):
    def resolve_template(self, template: str, context: dict) -> str:
        # Custom template logic
        return processed_template

# Use custom resolver
model = GrimoireModel(
    model_def,
    data,
    template_resolver=CustomTemplateResolver()
)
```

### Custom Validators

```python
from grimoire_model.validation.validators import ValidationEngine

def custom_validator(value, rule_params):
    # Custom validation logic
    return is_valid, error_message

# Register custom validator
engine = ValidationEngine()
engine.register_validator("custom_rule", custom_validator)
```

### Multiple Inheritance

```python
# Multiple parent models (all in same namespace)
combat_def = ModelDefinition(
    id="character",
    namespace="game",  # All parent models must be in same namespace
    extends=["base_entity", "combatant", "spell_caster"],
    attributes={...}
)

# Automatic conflict resolution with left-to-right precedence
# Parents automatically resolved from "game" namespace
```

## ๐Ÿ“„ License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for complete terms and conditions.

## ๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

If you have questions about the project, please contact: wyrdbound@proton.me

---

**Copyright (c) 2025 The Wyrd One**

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "grimoire-model",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": "gaming, rpg, tabletop, model, validation, schema",
    "author": null,
    "author_email": "The Wyrd One <wyrdbound@proton.me>",
    "download_url": "https://files.pythonhosted.org/packages/de/d3/6a8b32deed04266e456ec1c9a4559739bbc65ae6bdf0eb4c25fbea6cd4d7/grimoire_model-0.3.3.tar.gz",
    "platform": null,
    "description": "# Grimoire Model\n\n[![Tests](https://github.com/wyrdbound/grimoire-model/workflows/Tests/badge.svg)](https://github.com/wyrdbound/grimoire-model/actions)\n[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)\n[![Coverage](https://img.shields.io/badge/coverage-88%25-green.svg)](htmlcov/index.html)\n\n**Dict-like model system with schema validation, derived fields, and inheritance for the Grimoire tabletop RPG engine.**\n\nGrimoire Model provides a sophisticated, schema-driven model system that combines the familiar dict-like interface with powerful features like automatic field derivation, template-based expressions, model inheritance, and comprehensive validation. Designed to integrate seamlessly with `grimoire-context` for complete game state management.\n\n## \u2728 Features\n\n- **\ud83d\udcda Dict-like Interface**: Familiar Python dictionary operations with schema validation\n- **\u2728 Dual Access Pattern**: Support for both dictionary-style (`obj['field']`) and attribute-style (`obj.field`) access\n- **\ud83d\udd04 Reactive Derived Fields**: Automatic computation with dependency tracking and batch updates\n- **\ud83e\uddec Model Inheritance**: Multiple inheritance support with automatic namespace-based resolution\n- **\ud83d\udcdd Template Expressions**: Jinja2-powered field templates for dynamic content\n- **\ud83c\udfa8 Template Engine Compatible**: Works seamlessly with Jinja2, Django templates, and other engines\n- **\ud83c\udff7\ufe0f Namespace Organization**: Global model registry with namespace-based organization\n- **\ud83d\udee1\ufe0f Schema Validation**: Pydantic-based type checking and custom validation rules\n- **\ud83d\udd27 Dependency Injection**: Pluggable resolvers for extensibility\n- **\u26a1 Performance Optimized**: Efficient batch updates and lazy evaluation\n- **\ud83c\udfaf grimoire-context Integration**: Seamless interoperability with context management\n\n## \ud83d\ude80 Quick Start\n\n### Installation\n\n```bash\npip install grimoire-model\n```\n\n### Basic Usage\n\n```python\nfrom grimoire_model import ModelDefinition, AttributeDefinition, create_model\n\n# Define a character model schema\ncharacter_def = ModelDefinition(\n    id=\"character\",\n    name=\"Player Character\",\n    namespace=\"rpg\",  # Organize models in namespaces\n    attributes={\n        \"name\": AttributeDefinition(type=\"str\", required=True),\n        \"level\": AttributeDefinition(type=\"int\", default=1),\n        \"hp\": AttributeDefinition(type=\"int\", default=100),\n        \"mp\": AttributeDefinition(type=\"int\", default=50),\n\n        # Derived fields automatically update when dependencies change\n        \"max_hp\": AttributeDefinition(\n            type=\"int\",\n            derived=\"{{ level * 8 + hp }}\"\n        ),\n        \"character_summary\": AttributeDefinition(\n            type=\"str\",\n            derived=\"Level {{ level }} {{ name }} ({{ max_hp }} HP, {{ mp }} MP)\"\n        )\n    }\n)\n\n# Create a character instance\ncharacter = create_model(character_def, {\n    \"name\": \"Aragorn\",\n    \"level\": 15,\n    \"hp\": 120,\n    \"mp\": 80\n})\n\n# Access data using BOTH dictionary-style AND attribute-style notation\nprint(character['name'])        # \"Aragorn\" (dictionary-style)\nprint(character.name)           # \"Aragorn\" (attribute-style)\nprint(character.max_hp)         # 240 (15 * 8 + 120)\nprint(character.character_summary)  # \"Level 15 Aragorn (240 HP, 80 MP)\"\n\n# Updates work with both access patterns\ncharacter['level'] = 20         # Dictionary-style update\ncharacter.level = 20            # Attribute-style update (same result)\nprint(character.max_hp)         # 280 (automatically recalculated)\n```\n\n### Custom Primitive Types\n\nRegister domain-specific primitive types that should be treated as primitive values rather than complex model objects:\n\n```python\nfrom grimoire_model import register_primitive_type, ModelDefinition, AttributeDefinition, create_model_without_validation\n\n# Register custom primitive types\nregister_primitive_type('roll')       # Dice roll notation\nregister_primitive_type('duration')   # Time periods\nregister_primitive_type('distance')   # Measurements\n\n# Define a model using custom primitive types\nweapon_def = ModelDefinition(\n    id='weapon',\n    name='Weapon',\n    attributes={\n        'name': AttributeDefinition(type='str', required=True),\n        'damage': AttributeDefinition(type='roll', required=True)  # Custom primitive\n    }\n)\n\n# Create model - custom primitives work like built-in types\nweapon = create_model_without_validation(weapon_def, {\n    'name': 'Longsword',\n    'damage': '1d8'  # Stored as-is, like a string\n})\n\nprint(weapon['damage'])  # '1d8'\n```\n\nCustom primitive types:\n- Are stored as raw values without instantiation\n- Don't require model registration\n- Can have optional validators\n- Support domain-specific type semantics\n- Work in derived field templates\n\n### Global Model Registry\n\nModels are automatically registered in a global registry using namespaces:\n\n```python\nfrom grimoire_model import get_model\n\n# Models auto-register when created\ncharacter_def = ModelDefinition(\n    id=\"character\",\n    namespace=\"rpg\",  # Registered in \"rpg\" namespace\n    # ... attributes ...\n)\n\n# Retrieve from anywhere in your application\nretrieved_def = get_model(\"rpg\", \"character\")\nnew_character = create_model(retrieved_def, {\"name\": \"Hero\"})\n\n# Perfect for inheritance - child models automatically find parents\nbase_def = ModelDefinition(id=\"base\", namespace=\"rpg\", ...)\nchild_def = ModelDefinition(id=\"child\", namespace=\"rpg\", extends=[\"base\"], ...)\n# No manual registry needed - inheritance resolves automatically!\n```\n\n### Model Inheritance with Namespaces\n\n```python\nfrom grimoire_model import get_model, clear_registry\n\n# Base entity definition (auto-registered in namespace)\nbase_entity_def = ModelDefinition(\n    id=\"base_entity\",\n    name=\"Base Entity\",\n    namespace=\"game\",  # Registered in \"game\" namespace\n    attributes={\n        \"id\": AttributeDefinition(type=\"str\", required=True),\n        \"name\": AttributeDefinition(type=\"str\", required=True),\n        \"description\": AttributeDefinition(type=\"str\", default=\"\")\n    }\n)\n\n# Character extends base entity (automatic inheritance resolution)\ncharacter_def = ModelDefinition(\n    id=\"character\",\n    name=\"Character\",\n    namespace=\"game\",  # Same namespace enables automatic inheritance\n    extends=[\"base_entity\"],  # Automatically finds base_entity in namespace\n    attributes={\n        \"level\": AttributeDefinition(type=\"int\", default=1),\n        \"hp\": AttributeDefinition(type=\"int\", default=100),\n        \"max_hp\": AttributeDefinition(\n            type=\"int\",\n            derived=\"{{ level * 8 + hp }}\"\n        )\n    }\n)\n\n# Create character with inherited fields (no registry needed!)\ncharacter = create_model(\n    character_def,\n    {\n        \"id\": \"char_001\",          # From base_entity\n        \"name\": \"Legolas\",         # From base_entity\n        \"description\": \"Elf archer\", # From base_entity\n        \"level\": 12,               # From character\n        \"hp\": 96                   # From character\n    }\n)\n\nprint(character['id'])          # \"char_001\" (inherited)\nprint(character['name'])        # \"Legolas\" (inherited)\nprint(character['max_hp'])      # 192 (derived field)\n\n# Retrieve models from global registry\nretrieved_char_def = get_model(\"game\", \"character\")\nanother_character = create_model(retrieved_char_def, {\n    \"id\": \"char_002\",\n    \"name\": \"Gimli\"\n})\n```\n\n### Integration with grimoire-context\n\n```python\nfrom grimoire_context import GrimoireContext\n\n# Create context with character model\ncontext = GrimoireContext({\n    'party': {\n        'leader': character,\n        'members': 4\n    }\n})\n\n# Modify character through context - derived fields update automatically\ncontext = context.set_variable('party.leader.level', 25)\nupdated_character = context.get_variable('party.leader')\n\nprint(updated_character['level'])   # 25\nprint(updated_character['max_hp'])  # 296 (automatically recalculated)\n```\n\n### Batch Updates for Performance\n\n```python\n# Batch multiple changes for better performance\ncharacter.batch_update({\n    'level': 30,\n    'hp': 150,\n    'mp': 120\n})\n\n# All derived fields updated once after batch completion\nprint(character['max_hp'])  # 390 (30 * 8 + 150)\n```\n\n### Template Engine Integration\n\nGrimoireModel objects support both dictionary-style and attribute-style access, making them fully compatible with template engines like Jinja2, Django templates, and others:\n\n```python\nfrom jinja2 import Template\n\n# Create a weapon model\nweapon_def = ModelDefinition(\n    id=\"weapon\",\n    name=\"Weapon\",\n    attributes={\n        \"name\": AttributeDefinition(type=\"str\", required=True),\n        \"damage\": AttributeDefinition(type=\"str\", required=True),\n        \"bonus\": AttributeDefinition(type=\"int\", default=0),\n    }\n)\n\nweapon = create_model(weapon_def, {\n    \"name\": \"Longsword\",\n    \"damage\": \"1d8\",\n    \"bonus\": 2\n})\n\n# Use attribute access in Jinja2 templates\ntemplate = Template(\"{{ weapon.name }}: {{ weapon.damage }} +{{ weapon.bonus }}\")\nresult = template.render(weapon=weapon)\nprint(result)  # \"Longsword: 1d8 +2\"\n\n# Works with more complex templates\ntemplate = Template(\"\"\"\n{% if weapon.bonus > 0 %}\n  {{ weapon.name }} ({{ weapon.damage }}+{{ weapon.bonus }})\n{% else %}\n  {{ weapon.name }} ({{ weapon.damage }})\n{% endif %}\n\"\"\")\n```\n\nThis dual-access pattern (dictionary and attribute) provides:\n- **Template Compatibility**: Works seamlessly with Jinja2, Django, and other template engines\n- **Standard Python Behavior**: Objects behave like normal Python objects\n- **IDE Support**: Better autocomplete and type hints\n- **Backward Compatible**: All existing dictionary-style code continues to work\n\n## \ud83d\udcda Documentation\n\n- **[Logging Configuration](LOGGING.md)** - Configure library logging output and integration\n\n## \ud83d\udcda Core Concepts\n\n### Model Definitions\n\nModel definitions are schemas that describe the structure, types, and behavior of your data:\n\n```python\nmodel_def = ModelDefinition(\n    id=\"weapon\",\n    name=\"Weapon\",\n    namespace=\"combat\",  # Organize in combat namespace\n    description=\"Combat weapon with damage calculations\",\n    attributes={\n        \"name\": AttributeDefinition(type=\"str\", required=True),\n        \"base_damage\": AttributeDefinition(type=\"int\", default=1, range=\"1..50\"),\n        \"enhancement\": AttributeDefinition(type=\"int\", default=0, range=\"0..10\"),\n\n        # Derived field with complex logic\n        \"total_damage\": AttributeDefinition(\n            type=\"int\",\n            derived=\"{{ base_damage + enhancement * 2 }}\"\n        ),\n        \"damage_category\": AttributeDefinition(\n            type=\"str\",\n            derived=\"{% if total_damage >= 20 %}High{% elif total_damage >= 10 %}Medium{% else %}Low{% endif %}\"\n        )\n    },\n    validations=[\n        ValidationRule(\n            expression=\"base_damage > 0\",\n            message=\"Base damage must be positive\"\n        )\n    ]\n)\n```\n\n### Template Expressions\n\nUse Jinja2 templates for powerful derived field logic:\n\n```python\n# Simple expression\n\"max_hp\": \"{{ level * 8 + constitution * 2 }}\"\n\n# Conditional logic\n\"damage_bonus\": \"{% if strength >= 15 %}{{ (strength - 10) // 2 }}{% else %}0{% endif %}\"\n\n# Complex calculations\n\"skill_modifier\": \"{{ (skill_level + attribute_bonus - 10) // 2 }}\"\n```\n\n### Validation Rules\n\nAdd custom validation logic to ensure data integrity:\n\n```python\nValidationRule(\n    expression=\"level >= 1 and level <= 100\",\n    message=\"Character level must be between 1 and 100\"\n),\nValidationRule(\n    expression=\"hp > 0 or status == 'dead'\",\n    message=\"Living characters must have positive HP\"\n)\n```\n\n## \ud83d\udd27 API Reference\n\n### Core Classes\n\n#### ModelDefinition\n\n```python\nModelDefinition(\n    id: str,                                    # Unique model identifier\n    name: str,                                  # Human-readable name\n    namespace: str = \"default\",                 # Namespace for organization and inheritance\n    description: str = \"\",                      # Model description\n    attributes: Dict[str, AttributeDefinition], # Field definitions\n    extends: List[str] = None,                  # Parent model IDs (resolved in namespace)\n    validations: List[ValidationRule] = None    # Validation rules\n)\n```\n\n#### AttributeDefinition\n\n```python\nAttributeDefinition(\n    type: str,                    # Data type (str, int, float, bool, list, dict, or custom primitive)\n    required: bool = False,       # Whether field is required\n    default: Any = None,          # Default value\n    derived: str = None,          # Template expression for derived fields\n    range: str = None,            # Value range constraint (e.g., \"1..100\")\n    enum: List[Any] = None,       # Allowed values\n    pattern: str = None,          # Regex pattern for strings\n    description: str = \"\"         # Field description\n)\n```\n\n#### GrimoireModel\n\n```python\nclass GrimoireModel(MutableMapping):\n    def __init__(\n        self,\n        model_definition: ModelDefinition,\n        data: Dict[str, Any] = None,\n        template_resolver: TemplateResolver = None,\n        derived_field_resolver: DerivedFieldResolver = None,\n        **kwargs\n    )\n\n    # Dict-like interface\n    def __getitem__(self, key: str) -> Any\n    def __setitem__(self, key: str, value: Any) -> None\n    def __delitem__(self, key: str) -> None\n    def __iter__(self) -> Iterator[str]\n    def __len__(self) -> int\n    def keys(), values(), items()\n\n    # Attribute-style access (NEW in 0.3.2)\n    def __getattr__(self, name: str) -> Any\n    def __setattr__(self, name: str, value: Any) -> None\n    # Enables: obj.field_name (read) and obj.field_name = value (write)\n\n    # Batch operations\n    def batch_update(self, updates: Dict[str, Any]) -> None\n\n    # Path operations (dot notation)\n    def get(self, path: str, default: Any = None) -> Any\n    def set(self, path: str, value: Any) -> None\n    def has(self, path: str) -> bool\n    def delete(self, path: str) -> None\n```\n\n### Factory Functions\n\n#### create_model\n\n```python\ndef create_model(\n    model_definition: ModelDefinition,\n    data: Dict[str, Any] = None,\n    template_resolver_type: str = \"jinja2\",\n    derived_field_resolver_type: str = \"batched\",\n    **kwargs\n) -> GrimoireModel\n```\n\nCreates a model instance with default resolvers. Inheritance is automatically resolved from the global model registry using namespaces.\n\n### Global Registry Functions\n\n```python\nfrom grimoire_model import register_model, get_model, clear_registry\n\n# Register model manually (usually automatic)\nregister_model(\"my_namespace\", \"my_model\", model_definition)\n\n# Retrieve model from registry\nmodel_def = get_model(\"my_namespace\", \"my_model\")\n\n# Clear all models (useful for testing)\nclear_registry()\n\n# Access registry directly for advanced operations\nfrom grimoire_model import get_model_registry\nregistry = get_model_registry()\nregistry_dict = registry.get_registry_dict()\nall_namespaces = registry.list_namespaces()\n```\n\n### Primitive Type Registry Functions\n\n```python\nfrom grimoire_model import (\n    register_primitive_type,\n    unregister_primitive_type,\n    is_primitive_type,\n    clear_primitive_registry\n)\n\n# Register a custom primitive type\nregister_primitive_type('roll')\n\n# Register with optional validator\ndef validate_duration(value):\n    if isinstance(value, str) and value.endswith('s'):\n        return True, None\n    return False, \"Duration must end with 's'\"\n\nregister_primitive_type('duration', validator=validate_duration)\n\n# Check if a type is registered as primitive\nis_primitive_type('roll')  # True\nis_primitive_type('unknown')  # False\n\n# Unregister a primitive type\nunregister_primitive_type('roll')\n\n# Clear all registered primitives (useful for testing)\nclear_primitive_registry()\n```\n\n### Template Resolvers\n\n- `Jinja2TemplateResolver`: Standard Jinja2 template syntax\n- `ModelContextTemplateResolver`: Simple `$variable` substitution\n- `CachingTemplateResolver`: Cached template compilation for performance\n\n### Derived Field Resolvers\n\n- `BatchedDerivedFieldResolver`: Batches updates for performance\n- `DerivedFieldResolver`: Immediate update resolver\n\n## \ud83e\uddea Development\n\n### Setup\n\n```bash\ngit clone https://github.com/wyrdbound/grimoire-model.git\ncd grimoire-model\npython -m venv .venv\nsource .venv/bin/activate  # On Windows: .venv\\Scripts\\activate\npip install -e \".[dev]\"\n```\n\n### Running Tests\n\n```bash\n# Run all tests with coverage\n/Users/justingaylor/src/grimoire-model/.venv/bin/python -m pytest --cov=grimoire_model --cov-report=term\n\n# Run specific test file\n/Users/justingaylor/src/grimoire-model/.venv/bin/python -m pytest tests/test_model.py\n\n# Run with verbose output\n/Users/justingaylor/src/grimoire-model/.venv/bin/python -m pytest -v\n\n# Generate HTML coverage report\n/Users/justingaylor/src/grimoire-model/.venv/bin/python -m pytest --cov=grimoire_model --cov-report=html\n# Open htmlcov/index.html in browser\n```\n\n_Note: Use the virtual environment in the project root as specified in the development guidelines._\n\n### Code Quality\n\n```bash\n# Install development dependencies\nsource .venv/bin/activate && pip install ruff mypy\n\n# Linting and formatting\nsource .venv/bin/activate && ruff check .\nsource .venv/bin/activate && ruff format .\n\n# Type checking\nsource .venv/bin/activate && mypy src/grimoire_model/\n\n# Run all quality checks\nsource .venv/bin/activate && ruff check . && mypy src/grimoire_model/\n```\n\n### Running Examples\n\n```bash\n# Basic usage example\nsource .venv/bin/activate && python examples/01_basic_usage.py\n\n# Advanced features and inheritance\nsource .venv/bin/activate && python examples/02_advanced_usage.py\n\n# Inheritance and polymorphism\nsource .venv/bin/activate && python examples/03_inheritance_polymorphism.py\n\n# Performance and integration testing\nsource .venv/bin/activate && python examples/04_performance_integration.py\n```\n\n## \ud83d\udccb Requirements\n\n- Python 3.8+\n- pydantic >= 2.0.0\n- pyrsistent >= 0.19.0\n- jinja2 >= 3.1.0\n- pyyaml >= 6.0\n\n### Development Dependencies\n\n- pytest >= 7.0.0\n- pytest-cov >= 4.0.0\n- pytest-mock >= 3.0.0\n- hypothesis >= 6.0.0\n- mypy >= 1.0.0\n- ruff >= 0.1.0\n\n## \ud83c\udfaf Use Cases\n\nGrimoire Model excels in scenarios requiring structured, validated data with complex relationships:\n\n- **RPG Character Systems**: Stats, levels, equipment with derived values\n- **Game Item Management**: Equipment, inventory, crafting systems\n- **Rule Engine Data**: Complex game mechanics with interdependent calculations\n- **Configuration Systems**: Hierarchical configs with inheritance and validation\n- **Dynamic Content**: Template-based content generation with context awareness\n\n## \ud83c\udfd7\ufe0f Architecture\n\nThe package follows clean architecture principles with clear separation of concerns:\n\n- **Core Layer**: Model definitions, schemas, and the main GrimoireModel class\n- **Resolver Layer**: Pluggable template and derived field resolution systems\n- **Validation Layer**: Type checking, constraints, and custom validation rules\n- **Utils Layer**: Inheritance resolution, path utilities, and helper functions\n- **Integration Layer**: grimoire-context compatibility and factory functions\n\n### Key Design Principles\n\n1. **Dependency Injection**: All major components can be swapped via constructor injection\n2. **Immutable Operations**: Uses pyrsistent for efficient immutable data structures\n3. **Template-Driven**: Jinja2 templates provide powerful expression capabilities\n4. **Performance-Focused**: Batch updates and lazy evaluation minimize overhead\n5. **Type Safety**: Full type hints and Pydantic integration for runtime validation\n6. **Explicit Errors**: Prefers explicit errors over fallbacks to maintain system stability\n\n## \ud83d\udcc8 Performance\n\nCurrent benchmarks (86% test coverage, 184 tests passing):\n\n- **Model Creation**: ~1ms for simple models, ~5ms for complex inheritance\n- **Field Updates**: ~0.1ms for direct fields, ~2ms for derived field cascades\n- **Batch Updates**: 50-80% faster than individual updates for multiple fields\n- **Memory Usage**: ~50KB per model instance (excluding data)\n- **Template Resolution**: Cached compilation provides 10x speed improvement\n\n## \ud83d\udd04 Integration with grimoire-context\n\nSeamless integration is automatically enabled when both packages are installed:\n\n```python\nfrom grimoire_model import create_model, ModelDefinition, AttributeDefinition\nfrom grimoire_context import GrimoireContext\n\n# Models work naturally in contexts\ncharacter = create_model(character_def, character_data)\ncontext = GrimoireContext({'player': character})\n\n# Context operations automatically handle model updates\nupdated_context = context.set_variable('player.level', 25)\nupdated_character = updated_context['player']\n\n# Derived fields update automatically\nprint(updated_character['max_hp'])  # Recalculated based on new level\n```\n\n## \ud83d\udea8 Error Handling\n\nThe package provides a comprehensive exception hierarchy:\n\n```python\nfrom grimoire_model import (\n    GrimoireModelError,           # Base exception\n    ModelValidationError,          # Validation failures\n    TemplateResolutionError,       # Template processing errors\n    InheritanceError,              # Model inheritance issues\n    DependencyError,               # Derived field dependency issues\n    ConfigurationError             # Setup and configuration errors\n)\n\ntry:\n    character = create_model(character_def, invalid_data)\nexcept ModelValidationError as e:\n    print(f\"Validation failed: {e}\")\n    print(f\"Field: {e.field_name}\")\n    print(f\"Value: {e.field_value}\")\n    print(f\"Validation rule: {e.validation_rule}\")\n```\n\n## \ud83d\udd0d Advanced Features\n\n### Custom Template Resolvers\n\n```python\nfrom grimoire_model.resolvers.template import TemplateResolver\n\nclass CustomTemplateResolver(TemplateResolver):\n    def resolve_template(self, template: str, context: dict) -> str:\n        # Custom template logic\n        return processed_template\n\n# Use custom resolver\nmodel = GrimoireModel(\n    model_def,\n    data,\n    template_resolver=CustomTemplateResolver()\n)\n```\n\n### Custom Validators\n\n```python\nfrom grimoire_model.validation.validators import ValidationEngine\n\ndef custom_validator(value, rule_params):\n    # Custom validation logic\n    return is_valid, error_message\n\n# Register custom validator\nengine = ValidationEngine()\nengine.register_validator(\"custom_rule\", custom_validator)\n```\n\n### Multiple Inheritance\n\n```python\n# Multiple parent models (all in same namespace)\ncombat_def = ModelDefinition(\n    id=\"character\",\n    namespace=\"game\",  # All parent models must be in same namespace\n    extends=[\"base_entity\", \"combatant\", \"spell_caster\"],\n    attributes={...}\n)\n\n# Automatic conflict resolution with left-to-right precedence\n# Parents automatically resolved from \"game\" namespace\n```\n\n## \ud83d\udcc4 License\n\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE) file for complete terms and conditions.\n\n## \ud83e\udd1d Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.\n\nIf you have questions about the project, please contact: wyrdbound@proton.me\n\n---\n\n**Copyright (c) 2025 The Wyrd One**\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Dict-like model system with schema validation, derived fields, and inheritance for GRIMOIRE",
    "version": "0.3.3",
    "project_urls": {
        "Homepage": "https://github.com/wyrdbound/grimoire-model",
        "Issues": "https://github.com/wyrdbound/grimoire-model/issues",
        "Repository": "https://github.com/wyrdbound/grimoire-model"
    },
    "split_keywords": [
        "gaming",
        " rpg",
        " tabletop",
        " model",
        " validation",
        " schema"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "48a231f47851df7d52b58896d707c442077b59d23a938ef7f6cbde32a4c1e418",
                "md5": "8f2e41bdaf888bb8ccb2e143d5319e63",
                "sha256": "431bdb22f54e5b341d462e55ede4835fdd6d8c78c597ce9f54055300965144d7"
            },
            "downloads": -1,
            "filename": "grimoire_model-0.3.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "8f2e41bdaf888bb8ccb2e143d5319e63",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 48738,
            "upload_time": "2025-10-13T02:53:21",
            "upload_time_iso_8601": "2025-10-13T02:53:21.748156Z",
            "url": "https://files.pythonhosted.org/packages/48/a2/31f47851df7d52b58896d707c442077b59d23a938ef7f6cbde32a4c1e418/grimoire_model-0.3.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "ded36a8b32deed04266e456ec1c9a4559739bbc65ae6bdf0eb4c25fbea6cd4d7",
                "md5": "58517fc2105f700076cc5716556f1c13",
                "sha256": "bad7eafcf9c26b7e82176ff8620099eec416f8f7551fdbc0ba0b3a31085302e5"
            },
            "downloads": -1,
            "filename": "grimoire_model-0.3.3.tar.gz",
            "has_sig": false,
            "md5_digest": "58517fc2105f700076cc5716556f1c13",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 83198,
            "upload_time": "2025-10-13T02:53:22",
            "upload_time_iso_8601": "2025-10-13T02:53:22.995622Z",
            "url": "https://files.pythonhosted.org/packages/de/d3/6a8b32deed04266e456ec1c9a4559739bbc65ae6bdf0eb4c25fbea6cd4d7/grimoire_model-0.3.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-10-13 02:53:22",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "wyrdbound",
    "github_project": "grimoire-model",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "grimoire-model"
}
        
Elapsed time: 1.13978s