Added Claude management
This commit is contained in:
128
.claude/developer.md
Normal file
128
.claude/developer.md
Normal file
@@ -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
|
||||||
14
.claude/reset.md
Normal file
14
.claude/reset.md
Normal file
@@ -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
|
||||||
65
.claude/technical-writer.md
Normal file
65
.claude/technical-writer.md
Normal file
@@ -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
|
||||||
187
.claude/unit-tester.md
Normal file
187
.claude/unit-tester.md
Normal file
@@ -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
|
||||||
327
CLAUDE.md
Normal file
327
CLAUDE.md
Normal file
@@ -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
|
||||||
@@ -113,7 +113,7 @@ old_data = db.load(tenant_id, entry="users", digest=history[1])
|
|||||||
Each snapshot automatically includes metadata:
|
Each snapshot automatically includes metadata:
|
||||||
|
|
||||||
- `__parent__`: Digest of the previous version
|
- `__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`)
|
- `__date__`: Timestamp of the change (format: `YYYYMMDD HH:MM:SS`)
|
||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|||||||
@@ -55,5 +55,8 @@ class Handlers:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def register_handler(self, handler):
|
||||||
|
self.handlers.append(handler)
|
||||||
|
|
||||||
|
|
||||||
handlers = Handlers([DateHandler()])
|
handlers = Handlers([DateHandler()])
|
||||||
|
|||||||
Reference in New Issue
Block a user