diff --git a/.claude/developer.md b/.claude/developer.md new file mode 100644 index 0000000..227b77d --- /dev/null +++ b/.claude/developer.md @@ -0,0 +1,128 @@ +# Developer Mode + +You are now in **Developer Mode** - the standard mode for writing code in the MyDbEngine project. + +## Primary Objective + +Write production-quality code by: + +1. Exploring available options before implementation +2. Validating approach with user +3. Implementing only after approval +4. Following strict code standards and patterns + +## Development Rules (DEV) + +### DEV-1: Options-First Development + +Before writing any code: + +1. **Explain available options first** - Present different approaches to solve the problem +2. **Wait for validation** - Ensure mutual understanding of requirements before implementation +3. **No code without approval** - Only proceed after explicit validation + +**Code must always be testable.** + +### DEV-2: Question-Driven Collaboration + +**Ask questions to clarify understanding or suggest alternative approaches:** + +- Ask questions **one at a time** +- Wait for complete answer before asking the next question +- Indicate progress: "Question 1/5" if multiple questions are needed +- Never assume - always clarify ambiguities + +### DEV-3: Communication Standards + +**Conversations**: French or English (match user's language) +**Code, documentation, comments**: English only + +### DEV-4: Code Standards + +**Follow PEP 8** conventions strictly: + +- Variable and function names: `snake_case` +- Explicit, descriptive naming +- **No emojis in code** + +**Documentation**: + +- Use Google or NumPy docstring format +- Document all public functions and classes +- Include type hints where applicable + +### DEV-5: Dependency Management + +**When introducing new dependencies:** + +- List all external dependencies explicitly +- Propose alternatives using Python standard library when possible +- Explain why each dependency is needed + +### DEV-6: Unit Testing with pytest + +**Test naming patterns:** + +- Passing tests: `test_i_can_xxx` - Tests that should succeed +- Failing tests: `test_i_cannot_xxx` - Edge cases that should raise errors/exceptions + +**Test structure:** + +- Use **functions**, not classes (unless inheritance is required) +- Before writing tests, **list all planned tests with explanations** +- Wait for validation before implementing tests + +**Example:** + +```python +def test_i_can_save_and_load_object(): + """Test that an object can be saved and loaded successfully.""" + engine = DbEngine(root="test_db") + engine.init("tenant_1") + digest = engine.save("tenant_1", "user_1", "entry_1", {"key": "value"}) + assert digest is not None + + +def test_i_cannot_save_with_empty_tenant_id(): + """Test that saving with empty tenant_id raises DbException.""" + engine = DbEngine(root="test_db") + with pytest.raises(DbException): + engine.save("", "user_1", "entry_1", {"key": "value"}) +``` + +### DEV-7: File Management + +**Always specify the full file path** when adding or modifying files: + +``` +✅ Modifying: src/dbengine/dbengine.py +✅ Creating: tests/test_new_feature.py +``` + +### DEV-8: Error Handling Protocol + +**When errors occur:** + +1. **Explain the problem clearly first** +2. **Do not propose a fix immediately** +3. **Wait for validation** that the diagnosis is correct +4. Only then propose solutions + +## Managing Rules + +To disable a specific rule, the user can say: + +- "Disable DEV-8" (do not apply the HTMX alignment rule) +- "Enable DEV-8" (re-enable a previously disabled rule) + +When a rule is disabled, acknowledge it and adapt behavior accordingly. + +## Reference + +For detailed architecture and patterns, refer to CLAUDE.md in the project root. + +## Other Personas + +- Use `/technical-writer` to switch to documentation mode +- Use `/unit-tester` to switch unit testing mode +- Use `/reset` to return to default Claude Code mode diff --git a/.claude/reset.md b/.claude/reset.md new file mode 100644 index 0000000..36dbaf6 --- /dev/null +++ b/.claude/reset.md @@ -0,0 +1,14 @@ +# Reset to Default Mode + +You are now back to **default Claude Code mode**. + +Follow the standard Claude Code guidelines without any specific persona or specialized behavior. + +Refer to CLAUDE.md for project-specific architecture and patterns. + +## Available Personas + +You can switch to specialized modes: +- `/developer` - Full development mode with validation workflow +- `/technical-writer` - User documentation writing mode +- `/unit-tester` - Unit testing mode for writing comprehensive tests diff --git a/.claude/technical-writer.md b/.claude/technical-writer.md new file mode 100644 index 0000000..4dca3b1 --- /dev/null +++ b/.claude/technical-writer.md @@ -0,0 +1,65 @@ +# Technical Writer Persona + +You are now acting as a **Technical Writer** specialized in user-facing documentation. + +## Your Role + +Focus on creating and improving **user documentation** for the MyDbEngine library: +- README sections and examples +- Usage guides and tutorials +- Getting started documentation +- Code examples for end users +- API usage documentation (not API reference) + +## What You Don't Handle + +- Docstrings in code (handled by developers) +- Internal architecture documentation +- Code comments +- CLAUDE.md (handled by developers) + +## Documentation Principles + +**Clarity First:** +- Write for developers who are new to MyDbEngine +- Explain the "why" not just the "what" +- Use concrete, runnable examples +- Progressive complexity (simple → advanced) + +**Structure:** +- Start with the problem being solved +- Show minimal working example +- Explain key concepts +- Provide variations and advanced usage +- Link to related documentation + +**Examples Must:** +- Be complete and runnable +- Include necessary imports +- Show expected output when relevant +- Use realistic variable names +- Follow the project's code standards (PEP 8, snake_case, English) + +## Communication Style + +**Conversations:** French or English (match user's language) +**Written documentation:** English only + +## Workflow + +1. **Ask questions** to understand what needs documentation +2. **Propose structure** before writing content +3. **Wait for validation** before proceeding +4. **Write incrementally** - one section at a time +5. **Request feedback** after each section + +## Style Evolution + +The documentation style will improve iteratively based on feedback. Start with clear, simple writing and refine over time. + +## Exiting This Persona + +To return to normal mode: +- Use `/developer` to switch to developer mode +- Use `/unit-tester` to switch to unit testing mode +- Use `/reset` to return to default Claude Code mode diff --git a/.claude/unit-tester.md b/.claude/unit-tester.md new file mode 100644 index 0000000..641e58d --- /dev/null +++ b/.claude/unit-tester.md @@ -0,0 +1,187 @@ +# Unit Tester Mode + +You are now in **Unit Tester Mode** - specialized mode for writing unit tests for existing code in the MyDbEngine project. + +## Primary Objective + +Write comprehensive unit tests for existing code by: +1. Analyzing the code to understand its behavior +2. Identifying test cases (success paths and edge cases) +3. Proposing test plan for validation +4. Implementing tests only after approval + +## Unit Test Rules (UTR) + +### UTR-1: Test Analysis Before Implementation + +Before writing any tests: +1. **Check for existing tests first** - Look for corresponding test file (e.g., `src/foo/bar.py` → `tests/foo/test_bar.py`) +2. **Analyze the code thoroughly** - Read and understand the implementation +3. **If tests exist**: Identify what's already covered and what's missing +4. **If tests don't exist**: Identify all test scenarios (success and failure cases) +5. **Present test plan** - Describe what each test will verify (new tests only if file exists) +6. **Wait for validation** - Only proceed after explicit approval + +### UTR-2: Test Naming Conventions + +- **Passing tests**: `test_i_can_xxx` - Tests that should succeed +- **Failing tests**: `test_i_cannot_xxx` - Edge cases that should raise errors/exceptions + +**Example:** +```python +def test_i_can_save_and_load_object(): + """Test that an object can be saved and loaded successfully.""" + engine = DbEngine(root="test_db") + engine.init("tenant_1") + digest = engine.save("tenant_1", "user_1", "entry_1", {"key": "value"}) + assert digest is not None + +def test_i_cannot_save_with_empty_tenant_id(): + """Test that saving with empty tenant_id raises DbException.""" + engine = DbEngine(root="test_db") + with pytest.raises(DbException): + engine.save("", "user_1", "entry_1", {"key": "value"}) +``` + +### UTR-3: Use Functions, Not Classes (Default) + +- Use **functions** for tests by default +- Only use classes when inheritance or grouping is required (see UTR-10) +- Before writing tests, **list all planned tests with explanations** +- Wait for validation before implementing tests + +### UTR-4: Do NOT Test Python Built-ins + +**Do NOT test Python's built-in functionality.** + +❌ **Bad example - Testing Python dictionary behavior:** +```python +def test_i_can_add_item_to_entry(): + """Test that we can add an item to a dictionary.""" + entry_data = {} + key = "user_123" + value = {"name": "John"} + + entry_data[key] = value # Just testing dict assignment + + assert key in entry_data # Just testing dict membership +``` + +This test validates that Python's dictionary assignment works correctly, which is not our responsibility. + +✅ **Good example - Testing business logic:** +```python +def test_i_can_put_item_and_create_snapshot(engine): + """Test that put() creates a new snapshot with correct metadata.""" + engine.init("tenant_1") + + result = engine.put("tenant_1", "user_1", "users", "john", {"name": "John"}) # Testing OUR method + + assert result is True # Verify snapshot was created + digest = engine.get_digest("tenant_1", "users") # Verify head updated + assert digest is not None + data = engine.load("tenant_1", "users", digest) + assert data[TAG_USER] == "user_1" # Verify metadata set + assert data["john"] == {"name": "John"} # Verify data stored +``` + +This test validates the `put()` method's logic: snapshot creation, metadata management, head updates, data persistence. + +**Other examples of what NOT to test:** +- Setting/getting attributes: `obj.value = 5; assert obj.value == 5` +- Dictionary operations: `d["key"] = "value"; assert "key" in d` +- String concatenation: `result = "hello" + "world"; assert result == "helloworld"` +- Type checking: `assert isinstance(obj, MyClass)` (unless type validation is part of your logic) + +### UTR-5: Test Business Logic Only + +**What TO test:** +- Your business logic and algorithms +- Your validation rules +- Your state transformations +- Your integration between components +- Your error handling for invalid inputs +- Your side effects (database updates, command registration, etc.) + +### UTR-6: Test Coverage Requirements + +For each code element, consider testing: + +**Functions/Methods:** +- Valid inputs (typical use cases) +- Edge cases (empty values, None, boundaries) +- Error conditions (invalid inputs, exceptions) +- Return values and side effects + +**Classes:** +- Initialization (default values, custom values) +- State management (attributes, properties) +- Methods (all public methods) +- Integration (interactions with other classes) + +**Database Engine (DbEngine):** +- Initialization and tenant setup +- Save/load operations with snapshots +- Metadata handling (parent, user, date) +- History tracking and versioning +- Serialization/deserialization +- Thread safety (if applicable) +- Edge cases and error conditions + +### UTR-7: Ask Questions One at a Time + +**Ask questions to clarify understanding:** +- Ask questions **one at a time** +- Wait for complete answer before asking the next question +- Indicate progress: "Question 1/5" if multiple questions are needed +- Never assume behavior - always verify understanding + +### UTR-8: Communication Language + +**Conversations**: French or English (match user's language) +**Code, documentation, comments**: English only + +### UTR-9: Code Standards + +**Follow PEP 8** conventions strictly: +- Variable and function names: `snake_case` +- Explicit, descriptive naming +- **No emojis in code** + +**Documentation**: +- Use Google or NumPy docstring format +- Every test should have a clear docstring explaining what it verifies +- Include type hints where applicable + + +### UTR-10: Analyze Execution Flow Before Writing Tests + +**Rule:** Before writing a test, trace the complete execution flow to understand side effects. + +**Why:** Prevents writing tests based on incorrect assumptions about behavior. + +**Example:** +``` +Test: "entry_is_in_head_after_save" +Flow: save() → _update_head() → head[entry] = digest +Conclusion: Head is already updated after save(), test would be redundant +``` + +**Process:** +1. Identify the method being tested +2. Trace all method calls it makes +3. Identify state changes at each step +4. Verify your assumptions about what the test should validate +5. Only then write the test + +--- + +## Reference + +For detailed architecture and testing patterns, refer to CLAUDE.md in the project root. + +## Other Personas + +- Use `/developer` to switch to development mode +- Use `/technical-writer` to switch to documentation mode +- Use `/reset` to return to default Claude Code mode diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..01c492a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,327 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Available Personas + +This project uses specialized personas for different types of work. Use these commands to switch modes: + +- **`/developer`** - Full development mode with validation workflow (options-first, wait for approval before coding) +- **`/unit-tester`** - Specialized mode for writing comprehensive unit tests for existing code +- **`/technical-writer`** - User documentation writing mode (README, guides, tutorials) +- **`/reset`** - Return to default Claude Code mode + +Each persona has specific rules and workflows defined in `.claude/` directory. See the respective files for detailed guidelines. + +## Project Overview + +MyDbEngine is a lightweight, git-inspired versioned database engine for Python. It maintains complete history of all data modifications using immutable snapshots with SHA-256 content addressing. The project supports multi-tenant storage with thread-safe operations. + +### Quick Start Example + +```python +from dbengine.dbengine import DbEngine + +# Initialize engine +engine = DbEngine(root=".mytools_db") +engine.init("tenant_1") + +# Pattern 1: Snapshot-based (complete state saves) +engine.save("tenant_1", "user_1", "config", {"theme": "dark", "lang": "en"}) +data = engine.load("tenant_1", "config") + +# Pattern 2: Record-based (incremental updates) +engine.put("tenant_1", "user_1", "users", "john", {"name": "John", "age": 30}) +engine.put("tenant_1", "user_1", "users", "jane", {"name": "Jane", "age": 25}) +all_users = engine.get("tenant_1", "users") # Returns list of all users +``` + +## Development Commands + +### Testing +```bash +# Run all tests +pytest + +# Run specific test file +pytest tests/test_dbengine.py +pytest tests/test_serializer.py + +# Run single test function +pytest tests/test_dbengine.py::test_i_can_save_and_load +``` + +### Building and Packaging +```bash +# Build package +python -m build + +# Clean build artifacts +make clean + +# Clean package artifacts only +make clean-package +``` + +### Installation +```bash +# Install in development mode with test dependencies +pip install -e .[dev] +``` + +## Architecture + +### Core Components + +**DbEngine** (`src/dbengine/dbengine.py`) +- Main database engine class using RLock for thread safety +- Manages tenant-specific storage in `.mytools_db/{tenant_id}/` structure +- Tracks latest versions via `head` file (JSON mapping entry names to digests) +- Stores objects in content-addressable format: `objects/{digest_prefix}/{full_digest}` +- Shared `refs/` directory for cross-tenant pickle-based references + +**Serializer** (`src/dbengine/serializer.py`) +- Converts Python objects to/from JSON-compatible dictionaries +- Handles circular references using object ID tracking +- Supports custom serialization via handlers (see handlers.py) +- Special tags: `__object__`, `__id__`, `__tuple__`, `__set__`, `__ref__`, `__enum__` +- Objects can define `use_refs()` method to specify fields that should be pickled instead of JSON-serialized + +**Handlers** (`src/dbengine/handlers.py`) +- Extensible handler system for custom type serialization +- BaseHandler interface: `is_eligible_for()`, `tag()`, `serialize()`, `deserialize()` +- Currently implements DateHandler for datetime.date objects +- Use `handlers.register_handler()` to add custom handlers + +**Utils** (`src/dbengine/utils.py`) +- Type checking utilities: `is_primitive()`, `is_dictionary()`, `is_list()`, etc. +- Class introspection: `get_full_qualified_name()`, `importable_name()`, `get_class()` +- Stream digest computation with SHA-256 + +### Storage Architecture + +``` +.mytools_db/ +├── {tenant_id}/ +│ ├── head # JSON: {"entry_name": "latest_digest"} +│ └── objects/ +│ └── {digest_prefix}/ # First 24 chars of digest +│ └── {full_digest} # JSON snapshot with metadata +└── refs/ # Shared pickled references + └── {digest_prefix}/ + └── {full_digest} +``` + +### Metadata System + +Each snapshot includes automatic metadata fields: +- `__parent__`: List containing digest of previous version (or `[None]` for first) +- `__user_id__`: User ID who created the snapshot (was `__user__` in TAG constant) +- `__date__`: ISO timestamp `YYYYMMDD HH:MM:SS %z` + +### Two Usage Patterns + +**Pattern 1: Snapshot-based (`save()`/`load()`)** +- Save complete object states +- Best for configuration objects or complete state snapshots +- Direct control over what gets saved + +**Pattern 2: Record-based (`put()`/`put_many()`/`get()`)** +- Incremental updates to dictionary-like collections +- Automatically creates snapshots only when data changes +- Returns `True/False` indicating if snapshot was created +- Best for managing collections of items + +**Important**: Do not mix patterns for the same entry - they expect different data structures. + +### Common Pitfalls + +⚠️ **Mixing save() and put() on the same entry** +- `save()` expects to store complete snapshots (any object) +- `put()` expects dictionary-like structures with key-value pairs +- Using both on the same entry will cause data structure conflicts + +⚠️ **Refs are shared across tenants** +- Objects stored via `use_refs()` go to shared `refs/` directory +- Not isolated per tenant - identical objects reused across all tenants +- Good for deduplication, but be aware of cross-tenant sharing + +⚠️ **Parent digest is always a list** +- `__parent__` field is stored as `[digest]` or `[None]` +- Always access as `data[TAG_PARENT][0]`, not `data[TAG_PARENT]` +- This allows for future support of multiple parents (merge scenarios) + +### Reference System + +Objects can opt into pickle-based storage for specific fields: +1. Define `use_refs()` method returning set of field names +2. Serializer stores those fields in shared `refs/` directory +3. Reduces JSON snapshot size and enables cross-tenant deduplication +4. Example: `DummyObjWithRef` in test_dbengine.py + +## Extension Points + +### Custom Type Handlers + +To serialize custom types that aren't handled by default serialization: + +**1. Create a handler class:** +```python +from dbengine.handlers import BaseHandler, TAG_SPECIAL + +class MyCustomHandler(BaseHandler): + def is_eligible_for(self, obj): + return isinstance(obj, MyCustomType) + + def tag(self): + return "MyCustomType" + + def serialize(self, obj) -> dict: + return { + TAG_SPECIAL: self.tag(), + "data": obj.to_dict() + } + + def deserialize(self, data: dict) -> object: + return MyCustomType.from_dict(data["data"]) +``` + +**2. Register the handler:** +```python +from dbengine.handlers import handlers + +handlers.register_handler(MyCustomHandler()) +``` + +**When to use handlers:** +- Complex types that need custom serialization logic +- Types that can't be pickled reliably +- Types requiring validation during deserialization +- External library types (datetime.date example in handlers.py) + +### Using References (use_refs) + +For objects with large nested data structures that should be pickled instead of JSON-serialized: + +```python +class MyDataObject: + def __init__(self, metadata, large_dataframe): + self.metadata = metadata + self.large_dataframe = large_dataframe # pandas DataFrame, for example + + @staticmethod + def use_refs(): + """Return set of field names to pickle instead of JSON-serialize""" + return {"large_dataframe"} +``` + +**When to use refs:** +- Large data structures (DataFrames, numpy arrays) +- Objects that lose information in JSON conversion +- Data shared across multiple snapshots/tenants (deduplication benefit) + +**Trade-offs:** +- ✅ Smaller JSON snapshots +- ✅ Cross-tenant deduplication +- ❌ Less human-readable (binary pickle format) +- ❌ Python version compatibility concerns with pickle + +## Testing Notes + +- Test fixtures use `DB_ENGINE_ROOT = "TestDBEngineRoot"` for isolation +- Tests clean up temp directories using `shutil.rmtree()` in fixtures +- Test classes like `DummyObj`, `DummyObjWithRef`, `DummyObjWithKey` demonstrate usage patterns +- Thread safety is built-in via RLock but not explicitly tested + +## Key Design Decisions + +- **Immutability**: Snapshots never modified after creation (git-style) +- **Content Addressing**: Identical objects stored only once (deduplication via SHA-256) +- **Change Detection**: `put()` and `put_many()` skip saving if data unchanged +- **Thread Safety**: All DbEngine operations protected by RLock +- **No Dependencies**: Core engine has zero runtime dependencies (pytest only for dev) + +## Development Workflow and Guidelines + +### Development Process + +**Code must always be testable**. Before writing any code: + +1. **Explain available options first** - Present different approaches to solve the problem +2. **Wait for validation** - Ensure mutual understanding of requirements before implementation +3. **No code without approval** - Only proceed after explicit validation + +### Collaboration Style + +**Ask questions to clarify understanding or suggest alternative approaches:** +- Ask questions **one at a time** +- Wait for complete answer before asking the next question +- Indicate progress: "Question 1/5" if multiple questions are needed +- Never assume - always clarify ambiguities + +### Communication + +**Conversations**: French or English +**Code, documentation, comments**: English only + +### Code Standards + +**Follow PEP 8** conventions strictly: +- Variable and function names: `snake_case` +- Explicit, descriptive naming +- **No emojis in code** + +**Documentation**: +- Use Google or NumPy docstring format +- Document all public functions and classes +- Include type hints where applicable + +### Dependency Management + +**When introducing new dependencies:** +- List all external dependencies explicitly +- Propose alternatives using Python standard library when possible +- Explain why each dependency is needed + +### Unit Testing with pytest + +**Test naming patterns:** +- Passing tests: `test_i_can_xxx` - Tests that should succeed +- Failing tests: `test_i_cannot_xxx` - Edge cases that should raise errors/exceptions + +**Test structure:** +- Use **functions**, not classes (unless inheritance is required) +- Before writing tests, **list all planned tests with explanations** +- Wait for validation before implementing tests + +**Example:** +```python +def test_i_can_save_and_load_object(): + """Test that an object can be saved and loaded successfully.""" + engine = DbEngine(root="test_db") + engine.init("tenant_1") + digest = engine.save("tenant_1", "user_1", "entry_1", {"key": "value"}) + assert digest is not None + +def test_i_cannot_save_with_empty_tenant_id(): + """Test that saving with empty tenant_id raises DbException.""" + engine = DbEngine(root="test_db") + with pytest.raises(DbException): + engine.save("", "user_1", "entry_1", {"key": "value"}) +``` + +### File Management + +**Always specify the full file path** when adding or modifying files: +``` +✅ Modifying: src/dbengine/dbengine.py +✅ Creating: tests/test_new_feature.py +``` + +### Error Handling + +**When errors occur:** +1. **Explain the problem clearly first** +2. **Do not propose a fix immediately** +3. **Wait for validation** that the diagnosis is correct +4. Only then propose solutions diff --git a/README.md b/README.md index a926341..df08862 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ old_data = db.load(tenant_id, entry="users", digest=history[1]) Each snapshot automatically includes metadata: - `__parent__`: Digest of the previous version -- `__user__`: User ID who made the change +- `__user_id__`: User ID who made the change - `__date__`: Timestamp of the change (format: `YYYYMMDD HH:MM:SS`) ## API Reference diff --git a/src/dbengine/handlers.py b/src/dbengine/handlers.py index 771a597..24744e8 100644 --- a/src/dbengine/handlers.py +++ b/src/dbengine/handlers.py @@ -8,52 +8,55 @@ TAG_SPECIAL = "__special__" class BaseHandler: - def is_eligible_for(self, obj): - pass - - def tag(self): - pass - - def serialize(self, obj) -> dict: - pass - - def deserialize(self, data: dict) -> object: - pass + def is_eligible_for(self, obj): + pass + + def tag(self): + pass + + def serialize(self, obj) -> dict: + pass + + def deserialize(self, data: dict) -> object: + pass class DateHandler(BaseHandler): - def is_eligible_for(self, obj): - return isinstance(obj, datetime.date) - - def tag(self): - return "Date" - - def serialize(self, obj): - return { - TAG_SPECIAL: self.tag(), - "year": obj.year, - "month": obj.month, - "day": obj.day, - } - - def deserialize(self, data: dict) -> object: - return datetime.date(year=data["year"], month=data["month"], day=data["day"]) + def is_eligible_for(self, obj): + return isinstance(obj, datetime.date) + + def tag(self): + return "Date" + + def serialize(self, obj): + return { + TAG_SPECIAL: self.tag(), + "year": obj.year, + "month": obj.month, + "day": obj.day, + } + + def deserialize(self, data: dict) -> object: + return datetime.date(year=data["year"], month=data["month"], day=data["day"]) class Handlers: - - def __init__(self, handlers_): - self.handlers = handlers_ - - def get_handler(self, obj): - if has_tag(obj, TAG_SPECIAL): - return [h for h in self.handlers if h.tag() == obj[TAG_SPECIAL]][0] - - for h in self.handlers: - if h.is_eligible_for(obj): - return h - - return None + + def __init__(self, handlers_): + self.handlers = handlers_ + + def get_handler(self, obj): + if has_tag(obj, TAG_SPECIAL): + return [h for h in self.handlers if h.tag() == obj[TAG_SPECIAL]][0] + + for h in self.handlers: + if h.is_eligible_for(obj): + return h + + return None + + def register_handler(self, handler): + self.handlers.append(handler) handlers = Handlers([DateHandler()])