resolvekit 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- resolvekit/README.md +134 -0
- resolvekit/__init__.py +67 -0
- resolvekit/api/README.md +165 -0
- resolvekit/api/__init__.py +10 -0
- resolvekit/api/convenience.py +53 -0
- resolvekit/api/resolver.py +457 -0
- resolvekit/builders/README.md +173 -0
- resolvekit/builders/__init__.py +0 -0
- resolvekit/calibration/README.md +351 -0
- resolvekit/calibration/__init__.py +12 -0
- resolvekit/calibration/calibrator.py +184 -0
- resolvekit/calibration/features.py +139 -0
- resolvekit/calibration/models.py +78 -0
- resolvekit/cli/README.md +215 -0
- resolvekit/cli/__init__.py +0 -0
- resolvekit/cli/main.py +18 -0
- resolvekit/config.py +128 -0
- resolvekit/constants.py +252 -0
- resolvekit/constraints/README.md +102 -0
- resolvekit/constraints/__init__.py +17 -0
- resolvekit/constraints/constraint_engine.py +111 -0
- resolvekit/constraints/hierarchy_validator.py +148 -0
- resolvekit/constraints/membership_validator.py +60 -0
- resolvekit/constraints/protocols.py +33 -0
- resolvekit/constraints/temporal_validator.py +43 -0
- resolvekit/constraints/type_validator.py +42 -0
- resolvekit/data/README.md +165 -0
- resolvekit/data/__init__.py +14 -0
- resolvekit/data/alias_repository.py +206 -0
- resolvekit/data/code_repository.py +85 -0
- resolvekit/data/context_filters.py +49 -0
- resolvekit/data/db_manager.py +196 -0
- resolvekit/data/entity_repository.py +466 -0
- resolvekit/data/membership_repository.py +107 -0
- resolvekit/data/query_builder.py +177 -0
- resolvekit/data/schema.py +122 -0
- resolvekit/disambiguation/README.md +72 -0
- resolvekit/disambiguation/__init__.py +0 -0
- resolvekit/extraction/README.md +204 -0
- resolvekit/extraction/__init__.py +0 -0
- resolvekit/matchers/README.md +77 -0
- resolvekit/matchers/__init__.py +65 -0
- resolvekit/matchers/alias_exact.py +65 -0
- resolvekit/matchers/canonical_name.py +62 -0
- resolvekit/matchers/cascade.py +127 -0
- resolvekit/matchers/code_validators.py +250 -0
- resolvekit/matchers/exact_code.py +177 -0
- resolvekit/matchers/fts_matcher.py +106 -0
- resolvekit/matchers/fuzzy_matcher.py +142 -0
- resolvekit/matchers/priorities.py +174 -0
- resolvekit/matchers/protocols.py +75 -0
- resolvekit/normalization/README.md +192 -0
- resolvekit/normalization/__init__.py +8 -0
- resolvekit/normalization/normalizer.py +164 -0
- resolvekit/overlays/README.md +226 -0
- resolvekit/overlays/__init__.py +0 -0
- resolvekit/types.py +534 -0
- resolvekit/utils/README.md +188 -0
- resolvekit/utils/__init__.py +48 -0
- resolvekit/utils/cache.py +109 -0
- resolvekit/utils/dates.py +339 -0
- resolvekit/utils/errors.py +145 -0
- resolvekit/utils/files.py +366 -0
- resolvekit/utils/logging.py +219 -0
- resolvekit/utils/text.py +475 -0
- resolvekit/utils/validation.py +301 -0
- resolvekit-0.0.1.dist-info/METADATA +36 -0
- resolvekit-0.0.1.dist-info/RECORD +70 -0
- resolvekit-0.0.1.dist-info/WHEEL +4 -0
- resolvekit-0.0.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Normalization Module
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
The normalization module handles text preprocessing to ensure consistent string matching across different input formats and languages. It provides three preset levels optimized for different use cases.
|
|
6
|
+
|
|
7
|
+
## Implementation Status
|
|
8
|
+
|
|
9
|
+
✅ **Implemented** - Phase A complete with 94% test coverage
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Three Preset Levels**: strict, standard, aggressive
|
|
14
|
+
- **Performance-First**: LRU caching for repeated queries (10,000 entry cache)
|
|
15
|
+
- **Unicode Handling**: NFKD normalization for consistent representation
|
|
16
|
+
- **Diacritic Removal**: Configurable diacritical mark handling
|
|
17
|
+
- **Batch Operations**: Efficient processing of multiple texts
|
|
18
|
+
- **Graceful Error Handling**: Handles edge cases (empty strings, None, etc.)
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from resolvekit.normalization import TextNormalizer
|
|
24
|
+
|
|
25
|
+
# Create normalizer (stateless, reusable)
|
|
26
|
+
normalizer = TextNormalizer()
|
|
27
|
+
|
|
28
|
+
# Basic usage (default: standard level)
|
|
29
|
+
result = normalizer.normalize("Côte d'Ivoire")
|
|
30
|
+
print(result) # Output: "cote d'ivoire"
|
|
31
|
+
|
|
32
|
+
# Batch processing
|
|
33
|
+
countries = ["France", "Côte d'Ivoire", "Germany"]
|
|
34
|
+
normalized = normalizer.normalize_batch(countries)
|
|
35
|
+
print(normalized) # ['france', "cote d'ivoire", 'germany']
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Normalization Levels
|
|
39
|
+
|
|
40
|
+
### Strict
|
|
41
|
+
**Use for**: Exact matching where case and diacritics matter (e.g., ISO codes)
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
normalizer.normalize("USA", level="strict") # "USA"
|
|
45
|
+
normalizer.normalize("Côte d'Ivoire", level="strict") # "Côte d'Ivoire" (decomposed unicode)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- ✅ Unicode normalization (NFKD)
|
|
49
|
+
- ✅ Whitespace normalization
|
|
50
|
+
- ❌ Diacritic removal
|
|
51
|
+
- ❌ Case folding
|
|
52
|
+
- ❌ Punctuation removal
|
|
53
|
+
|
|
54
|
+
### Standard (Default)
|
|
55
|
+
**Use for**: General entity resolution and fuzzy matching
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
normalizer.normalize("Côte d'Ivoire") # "cote d'ivoire"
|
|
59
|
+
normalizer.normalize("U.S.A.") # "u.s.a."
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- ✅ Unicode normalization (NFKD)
|
|
63
|
+
- ✅ Diacritic removal
|
|
64
|
+
- ✅ Case folding
|
|
65
|
+
- ✅ Whitespace normalization
|
|
66
|
+
- ❌ Punctuation removal
|
|
67
|
+
|
|
68
|
+
### Aggressive
|
|
69
|
+
**Use for**: Noisy data, user input with typos, low-quality sources
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
normalizer.normalize("U.S.A.", level="aggressive") # "usa"
|
|
73
|
+
normalizer.normalize("Côte-d'Ivoire!!!", level="aggressive") # "cote divoire"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- ✅ Unicode normalization (NFKD)
|
|
77
|
+
- ✅ Diacritic removal
|
|
78
|
+
- ✅ Case folding
|
|
79
|
+
- ✅ Whitespace normalization
|
|
80
|
+
- ✅ Punctuation removal
|
|
81
|
+
|
|
82
|
+
## API Reference
|
|
83
|
+
|
|
84
|
+
### TextNormalizer
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
class TextNormalizer:
|
|
88
|
+
def normalize(text: str, level: str = "standard") -> str:
|
|
89
|
+
"""Normalize single text with LRU caching."""
|
|
90
|
+
|
|
91
|
+
def normalize_batch(texts: list[str], level: str = "standard") -> list[str]:
|
|
92
|
+
"""Normalize multiple texts efficiently."""
|
|
93
|
+
|
|
94
|
+
def get_level_config(level: str) -> NormalizationLevel:
|
|
95
|
+
"""Get configuration for a level (for introspection)."""
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Examples
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from resolvekit.normalization import TextNormalizer
|
|
102
|
+
|
|
103
|
+
normalizer = TextNormalizer()
|
|
104
|
+
|
|
105
|
+
# Different levels
|
|
106
|
+
normalizer.normalize("Türkiye", level="strict") # "Türkiye"
|
|
107
|
+
normalizer.normalize("Türkiye", level="standard") # "turkiye"
|
|
108
|
+
normalizer.normalize("Türkiye", level="aggressive") # "turkiye"
|
|
109
|
+
|
|
110
|
+
# Batch processing
|
|
111
|
+
texts = ["France", "Germany", "Italy"]
|
|
112
|
+
normalized = normalizer.normalize_batch(texts) # ["france", "germany", "italy"]
|
|
113
|
+
|
|
114
|
+
# Edge cases (handled gracefully)
|
|
115
|
+
normalizer.normalize("") # ""
|
|
116
|
+
normalizer.normalize(" ") # ""
|
|
117
|
+
normalizer.normalize(None) # ""
|
|
118
|
+
|
|
119
|
+
# Introspection
|
|
120
|
+
config = normalizer.get_level_config("standard")
|
|
121
|
+
print(config.remove_diacritics) # True
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Performance
|
|
125
|
+
|
|
126
|
+
The normalization module is optimized for high-throughput entity resolution:
|
|
127
|
+
|
|
128
|
+
- **Caching**: 10,000-entry LRU cache for repeated queries
|
|
129
|
+
- **Speed**: < 0.001ms per cached normalization
|
|
130
|
+
- **Batch**: Efficient config reuse for multiple texts
|
|
131
|
+
- **Coverage**: 94% test coverage with performance benchmarks
|
|
132
|
+
|
|
133
|
+
### Performance Examples
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# Cached calls are 10-100x faster
|
|
137
|
+
normalizer.normalize("France") # First call: ~0.01ms
|
|
138
|
+
normalizer.normalize("France") # Cached: ~0.0001ms
|
|
139
|
+
|
|
140
|
+
# Batch processing is efficient
|
|
141
|
+
texts = ["France"] * 1000
|
|
142
|
+
normalizer.normalize_batch(texts) # Config reused for all texts
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Design Principles
|
|
146
|
+
|
|
147
|
+
1. **Thin Orchestrator**: Wraps existing `utils.text` functions
|
|
148
|
+
2. **Preset Levels**: Simple API with sensible defaults
|
|
149
|
+
3. **Performance-First**: LRU caching from day one
|
|
150
|
+
4. **Graceful Degradation**: Handles edge cases without crashing
|
|
151
|
+
5. **Testable**: 19 comprehensive tests with 94% coverage
|
|
152
|
+
|
|
153
|
+
## Integration
|
|
154
|
+
|
|
155
|
+
The normalizer integrates with the matcher cascade:
|
|
156
|
+
|
|
157
|
+
| Matcher | Level | Rationale |
|
|
158
|
+
|---------|-------|-----------|
|
|
159
|
+
| ExactCodeMatcher | strict | Codes are case-sensitive |
|
|
160
|
+
| CanonicalNameMatcher | standard | Canonical names need normalization |
|
|
161
|
+
| AliasExactMatcher | standard | Aliases need diacritic/case handling |
|
|
162
|
+
| FuzzyMatchers | standard/aggressive | Depends on data quality |
|
|
163
|
+
|
|
164
|
+
## Future Enhancements
|
|
165
|
+
|
|
166
|
+
- **Transliteration**: Cyrillic→Latin, Arabic→Latin (deferred to Phase B+)
|
|
167
|
+
- **Custom levels**: User-defined normalization configs
|
|
168
|
+
- **Language hints**: Language-aware normalization rules
|
|
169
|
+
- **Normalization metadata**: Track applied transformations for debugging
|
|
170
|
+
|
|
171
|
+
## Testing
|
|
172
|
+
|
|
173
|
+
Run normalization tests:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# All normalization tests
|
|
177
|
+
uv run pytest tests/test_normalization.py -v
|
|
178
|
+
|
|
179
|
+
# Performance tests only
|
|
180
|
+
uv run pytest tests/test_normalization.py::TestPerformance -v
|
|
181
|
+
|
|
182
|
+
# With coverage
|
|
183
|
+
uv run pytest tests/test_normalization.py --cov=src/resolvekit/normalization
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Files
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
src/resolvekit/normalization/
|
|
190
|
+
├── __init__.py # Exports TextNormalizer, NormalizationLevel
|
|
191
|
+
└── normalizer.py # Implementation (40 SLOC, 94% coverage)
|
|
192
|
+
```
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Text normalization module for resolvekit."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from resolvekit.utils.cache import lru_cache
|
|
8
|
+
from resolvekit.utils.text import (
|
|
9
|
+
case_fold,
|
|
10
|
+
normalize_unicode,
|
|
11
|
+
normalize_whitespace,
|
|
12
|
+
remove_diacritics,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class NormalizationLevel:
|
|
18
|
+
"""Configuration for a normalization level."""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
unicode_norm: bool
|
|
22
|
+
remove_diacritics: bool
|
|
23
|
+
case_fold: bool
|
|
24
|
+
normalize_whitespace: bool
|
|
25
|
+
remove_punctuation: bool
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TextNormalizer:
|
|
29
|
+
"""Text normalizer with preset levels and LRU caching."""
|
|
30
|
+
|
|
31
|
+
LEVELS: ClassVar[dict[str, NormalizationLevel]] = {
|
|
32
|
+
"strict": NormalizationLevel(
|
|
33
|
+
name="strict",
|
|
34
|
+
unicode_norm=True,
|
|
35
|
+
remove_diacritics=False,
|
|
36
|
+
case_fold=False,
|
|
37
|
+
normalize_whitespace=True,
|
|
38
|
+
remove_punctuation=False,
|
|
39
|
+
),
|
|
40
|
+
"standard": NormalizationLevel(
|
|
41
|
+
name="standard",
|
|
42
|
+
unicode_norm=True,
|
|
43
|
+
remove_diacritics=True,
|
|
44
|
+
case_fold=True,
|
|
45
|
+
normalize_whitespace=True,
|
|
46
|
+
remove_punctuation=False,
|
|
47
|
+
),
|
|
48
|
+
"aggressive": NormalizationLevel(
|
|
49
|
+
name="aggressive",
|
|
50
|
+
unicode_norm=True,
|
|
51
|
+
remove_diacritics=True,
|
|
52
|
+
case_fold=True,
|
|
53
|
+
normalize_whitespace=True,
|
|
54
|
+
remove_punctuation=True,
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@lru_cache(maxsize=10000)
|
|
59
|
+
def normalize(self, text: str, level: str = "standard") -> str:
|
|
60
|
+
"""
|
|
61
|
+
Normalize text using preset level.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
text: Text to normalize
|
|
65
|
+
level: Normalization level (strict, standard, aggressive)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Normalized text
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
>>> normalizer = TextNormalizer()
|
|
72
|
+
>>> normalizer.normalize("Côte d'Ivoire")
|
|
73
|
+
"cote d'ivoire"
|
|
74
|
+
"""
|
|
75
|
+
# Handle empty/whitespace-only strings
|
|
76
|
+
if not text or not text.strip():
|
|
77
|
+
return ""
|
|
78
|
+
|
|
79
|
+
# Get level config
|
|
80
|
+
level_config = self.LEVELS[level]
|
|
81
|
+
|
|
82
|
+
# Apply normalizations
|
|
83
|
+
return self._apply_normalizations(text, level_config)
|
|
84
|
+
|
|
85
|
+
def _apply_normalizations(self, text: str, config: NormalizationLevel) -> str:
|
|
86
|
+
"""
|
|
87
|
+
Apply normalizations based on configuration.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
text: Text to normalize
|
|
91
|
+
config: Normalization level configuration
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Normalized text
|
|
95
|
+
"""
|
|
96
|
+
# Unicode normalization
|
|
97
|
+
if config.unicode_norm:
|
|
98
|
+
text = normalize_unicode(text)
|
|
99
|
+
|
|
100
|
+
# Remove diacritics
|
|
101
|
+
if config.remove_diacritics:
|
|
102
|
+
text = remove_diacritics(text)
|
|
103
|
+
|
|
104
|
+
# Case folding
|
|
105
|
+
if config.case_fold:
|
|
106
|
+
text = case_fold(text)
|
|
107
|
+
|
|
108
|
+
# Normalize whitespace
|
|
109
|
+
if config.normalize_whitespace:
|
|
110
|
+
text = normalize_whitespace(text)
|
|
111
|
+
|
|
112
|
+
# Remove punctuation
|
|
113
|
+
if config.remove_punctuation:
|
|
114
|
+
text = self._remove_punctuation(text)
|
|
115
|
+
|
|
116
|
+
return text
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _remove_punctuation(text: str) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Remove punctuation from text.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
text: Input text
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Text without punctuation
|
|
128
|
+
"""
|
|
129
|
+
# Remove common punctuation but keep spaces and alphanumerics
|
|
130
|
+
return re.sub(r"[^\w\s]", "", text)
|
|
131
|
+
|
|
132
|
+
def normalize_batch(self, texts: list[str], level: str = "standard") -> list[str]:
|
|
133
|
+
"""
|
|
134
|
+
Normalize multiple texts efficiently.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
texts: List of texts to normalize
|
|
138
|
+
level: Normalization level (strict, standard, aggressive)
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
List of normalized texts
|
|
142
|
+
|
|
143
|
+
Examples:
|
|
144
|
+
>>> normalizer = TextNormalizer()
|
|
145
|
+
>>> normalizer.normalize_batch(["France", "Germany"])
|
|
146
|
+
["france", "germany"]
|
|
147
|
+
"""
|
|
148
|
+
# Delegate to normalize() to reuse guard logic and benefit from cache
|
|
149
|
+
return [self.normalize(text, level) for text in texts]
|
|
150
|
+
|
|
151
|
+
def get_level_config(self, level: str) -> NormalizationLevel:
|
|
152
|
+
"""
|
|
153
|
+
Get configuration for a normalization level.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
level: Normalization level name
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
NormalizationLevel configuration
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
KeyError: If level name is invalid
|
|
163
|
+
"""
|
|
164
|
+
return self.LEVELS[level]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Overlays Module
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
The overlays module manages custom data packs that extend or override the base data pack. Overlays enable users to install domain-specific packs (e.g., humanitarian-orgs-pack, schools-pack, corporate-entities-pack) or create their own custom aliases and entities.
|
|
6
|
+
|
|
7
|
+
## Components
|
|
8
|
+
|
|
9
|
+
### Core Components
|
|
10
|
+
|
|
11
|
+
1. **Overlay Manager** (`overlay_manager.py`)
|
|
12
|
+
- Load and attach overlay databases
|
|
13
|
+
- Manage precedence rules
|
|
14
|
+
- Handle overlay conflicts
|
|
15
|
+
|
|
16
|
+
2. **Overlay Writer** (`overlay_writer.py`)
|
|
17
|
+
- Create new overlay packs
|
|
18
|
+
- Add/update aliases, entities, memberships
|
|
19
|
+
- Build FTS indexes for overlays
|
|
20
|
+
|
|
21
|
+
3. **Overlay Validator** (`validator.py`)
|
|
22
|
+
- Validate overlay schemas
|
|
23
|
+
- Check for conflicts with base pack
|
|
24
|
+
- Verify referential integrity
|
|
25
|
+
|
|
26
|
+
4. **Overlay Merger** (`merger.py`)
|
|
27
|
+
- Merge queries across base + overlays
|
|
28
|
+
- Apply precedence rules
|
|
29
|
+
- Deduplicate results
|
|
30
|
+
|
|
31
|
+
### Data Models
|
|
32
|
+
|
|
33
|
+
- `overlay_config.py`: Overlay configuration and metadata
|
|
34
|
+
- `precedence.py`: Precedence rule engine
|
|
35
|
+
|
|
36
|
+
## Overlay Structure
|
|
37
|
+
|
|
38
|
+
Each overlay is a SQLite database with the same schema as base pack:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
my-overlay.sqlite
|
|
42
|
+
├── entities (custom entities only)
|
|
43
|
+
├── aliases (custom aliases + overrides)
|
|
44
|
+
├── aliases_fts (FTS index)
|
|
45
|
+
├── codes (custom codes)
|
|
46
|
+
├── memberships (custom memberships)
|
|
47
|
+
└── manifest (overlay metadata)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Overlay Manifest
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"pack_name": "humanitarian-orgs.overlay",
|
|
55
|
+
"version": "0.3.0",
|
|
56
|
+
"base_compat": ">=1.2.0",
|
|
57
|
+
"components": {
|
|
58
|
+
"overlay_sqlite": "humanitarian-orgs.overlay.sqlite",
|
|
59
|
+
"fts_built": true,
|
|
60
|
+
"calibration_delta": null
|
|
61
|
+
},
|
|
62
|
+
"description": "Humanitarian organization names and aliases",
|
|
63
|
+
"author": "OCHA",
|
|
64
|
+
"created": "2025-10-20T10:00:00Z"
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Note:** Precedence is not stored in the manifest. It's determined by the order in which overlays are loaded (earlier in list = higher precedence).
|
|
69
|
+
|
|
70
|
+
## Precedence Rules
|
|
71
|
+
|
|
72
|
+
Precedence is determined by the order in the overlays list:
|
|
73
|
+
|
|
74
|
+
- **First overlay in list**: Highest precedence (100)
|
|
75
|
+
- **Second overlay**: Precedence 99
|
|
76
|
+
- **Third overlay**: Precedence 98
|
|
77
|
+
- **...and so on** (max 5 overlays)
|
|
78
|
+
- **Base pack**: Precedence 0 (lowest)
|
|
79
|
+
|
|
80
|
+
When the same alias maps to different entities:
|
|
81
|
+
1. Use the highest precedence mapping (earliest in the list)
|
|
82
|
+
2. Log warning about conflict
|
|
83
|
+
3. Optional: Surface conflict to user for resolution
|
|
84
|
+
|
|
85
|
+
**Example:**
|
|
86
|
+
```python
|
|
87
|
+
resolver = Resolver(overlays=["personal.sqlite", "humanitarian.sqlite", "schools.sqlite"])
|
|
88
|
+
# personal.sqlite: precedence 100 (highest)
|
|
89
|
+
# humanitarian.sqlite: precedence 99
|
|
90
|
+
# schools.sqlite: precedence 98
|
|
91
|
+
# base pack: precedence 0 (lowest)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Creating Overlays
|
|
95
|
+
|
|
96
|
+
### From CSV
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from resolvekit.overlays import OverlayWriter
|
|
100
|
+
|
|
101
|
+
# Create overlay from CSV
|
|
102
|
+
writer = OverlayWriter("my-overlay")
|
|
103
|
+
|
|
104
|
+
# Add aliases
|
|
105
|
+
writer.add_aliases_from_csv("custom_aliases.csv")
|
|
106
|
+
# CSV columns: dcid, alias_text, alias_type, language
|
|
107
|
+
|
|
108
|
+
# Add custom entities
|
|
109
|
+
writer.add_entities_from_csv("custom_entities.csv")
|
|
110
|
+
# CSV columns: dcid, canonical_name, entity_type, parent_dcid
|
|
111
|
+
|
|
112
|
+
# Add memberships
|
|
113
|
+
writer.add_memberships_from_csv("memberships.csv")
|
|
114
|
+
# CSV columns: entity_dcid, group_dcid, valid_from, valid_until
|
|
115
|
+
|
|
116
|
+
# Build and save
|
|
117
|
+
writer.build("my-overlay.sqlite")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### From YAML
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
# custom_data.yaml
|
|
124
|
+
entities:
|
|
125
|
+
- dcid: "custom/MyOrg"
|
|
126
|
+
name: "My Organization"
|
|
127
|
+
type: "organization"
|
|
128
|
+
aliases:
|
|
129
|
+
- "MyOrg"
|
|
130
|
+
- "The Organization"
|
|
131
|
+
codes:
|
|
132
|
+
internal_id: "ORG-001"
|
|
133
|
+
|
|
134
|
+
- dcid: "country/COD" # Extend existing entity
|
|
135
|
+
custom_aliases:
|
|
136
|
+
- "Congo-Kinshasa"
|
|
137
|
+
- "DROC" # Internal shorthand
|
|
138
|
+
|
|
139
|
+
groups:
|
|
140
|
+
- dcid: "custom/OurPartners"
|
|
141
|
+
name: "Our Partner Countries"
|
|
142
|
+
members:
|
|
143
|
+
- country/KEN
|
|
144
|
+
- country/TZA
|
|
145
|
+
- country/UGA
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Via API
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from resolvekit.overlays import create_overlay
|
|
152
|
+
|
|
153
|
+
overlay = create_overlay(
|
|
154
|
+
name="my-custom-overlay",
|
|
155
|
+
base_pack_version="1.2.0"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Add custom alias
|
|
159
|
+
overlay.add_alias(
|
|
160
|
+
entity_dcid="country/COD",
|
|
161
|
+
alias_text="Congo-Kinshasa",
|
|
162
|
+
alias_type="exonym",
|
|
163
|
+
language="en"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Add custom entity
|
|
167
|
+
overlay.add_entity(
|
|
168
|
+
dcid="custom/MyEntity",
|
|
169
|
+
canonical_name="My Custom Entity",
|
|
170
|
+
entity_type="organization"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Save overlay
|
|
174
|
+
overlay.save("my-custom.overlay.sqlite")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Note:** Precedence is determined when loading overlays into the Resolver, not when creating them.
|
|
178
|
+
|
|
179
|
+
## Loading Overlays
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from resolvekit.api import Resolver
|
|
183
|
+
|
|
184
|
+
# Load resolver with overlays (order = precedence)
|
|
185
|
+
resolver = Resolver(
|
|
186
|
+
overlays=[
|
|
187
|
+
"path/to/personal-customizations.sqlite", # Highest precedence (100)
|
|
188
|
+
"path/to/humanitarian-orgs.sqlite", # Precedence 99
|
|
189
|
+
"path/to/schools-pack.sqlite" # Precedence 98
|
|
190
|
+
]
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Overlays are automatically applied to all queries
|
|
194
|
+
result = resolver.resolve("DROC") # Matches custom alias from highest-precedence overlay
|
|
195
|
+
|
|
196
|
+
# Maximum 5 overlays supported
|
|
197
|
+
# Order in the list determines precedence (first = highest)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Conflict Resolution
|
|
201
|
+
|
|
202
|
+
When conflicts detected:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
# Log warning
|
|
206
|
+
logger.warning(
|
|
207
|
+
"Alias 'Georgia' maps to both country/GEO (precedence: 0) "
|
|
208
|
+
"and geoId/13 (precedence: 50). Using geoId/13."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Optionally return conflict info
|
|
212
|
+
result = resolver.resolve("Georgia", return_conflicts=True)
|
|
213
|
+
if result.conflicts:
|
|
214
|
+
print(f"Conflicts: {result.conflicts}")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Design Principles
|
|
218
|
+
|
|
219
|
+
1. **Non-destructive**: Overlays don't modify base pack
|
|
220
|
+
2. **Composable**: Multiple overlays can coexist
|
|
221
|
+
3. **Validated**: Schema validation on overlay creation
|
|
222
|
+
4. **Transparent**: Clear precedence rules, conflict reporting
|
|
223
|
+
|
|
224
|
+
## Implementation Priority
|
|
225
|
+
|
|
226
|
+
**Phase C** - Overlay system and builders
|
|
File without changes
|