rolodexter 2.6.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rolodexter Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,307 @@
1
+ Metadata-Version: 2.4
2
+ Name: rolodexter
3
+ Version: 2.6.0
4
+ Summary: The universal contact field mapper โ€” route messy, inconsistent contact data to a clean canonical schema.
5
+ Author: Rolodexter Contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/rolodexter/rolodexter
8
+ Project-URL: Repository, https://github.com/rolodexter/rolodexter
9
+ Project-URL: Documentation, https://github.com/rolodexter/rolodexter#readme
10
+ Project-URL: Issues, https://github.com/rolodexter/rolodexter/issues
11
+ Project-URL: Changelog, https://github.com/rolodexter/rolodexter/blob/main/CHANGELOG.md
12
+ Keywords: contact,mapper,normalizer,etl,crm,field-mapping,data-cleaning,hubspot,salesforce,mailchimp
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Office/Business
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: phonenumbers>=8.0
29
+ Requires-Dist: nameparser>=1.1
30
+ Provides-Extra: fuzzy
31
+ Requires-Dist: rapidfuzz>=3.0; extra == "fuzzy"
32
+ Provides-Extra: i18n
33
+ Requires-Dist: deep-translator>=1.11; extra == "i18n"
34
+ Requires-Dist: unidecode>=1.3; extra == "i18n"
35
+ Provides-Extra: all
36
+ Requires-Dist: rapidfuzz>=3.0; extra == "all"
37
+ Requires-Dist: deep-translator>=1.11; extra == "all"
38
+ Requires-Dist: unidecode>=1.3; extra == "all"
39
+ Provides-Extra: dev
40
+ Requires-Dist: rapidfuzz>=3.0; extra == "dev"
41
+ Requires-Dist: deep-translator>=1.11; extra == "dev"
42
+ Requires-Dist: unidecode>=1.3; extra == "dev"
43
+ Requires-Dist: pytest>=8.0; extra == "dev"
44
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
45
+ Requires-Dist: ruff>=0.4; extra == "dev"
46
+ Requires-Dist: mypy>=1.10; extra == "dev"
47
+ Requires-Dist: deptry>=0.23; extra == "dev"
48
+ Requires-Dist: vulture>=2.11; extra == "dev"
49
+ Requires-Dist: pylint>=3.0; extra == "dev"
50
+ Dynamic: license-file
51
+
52
+ <div align="center">
53
+
54
+ # ๐Ÿ“‡ Rolodexter
55
+
56
+ **The universal contact field mapper.**
57
+
58
+ Route messy, inconsistent contact data from *any* source to a clean, canonical schema.
59
+
60
+ [![CI](https://github.com/rolodexter/rolodexter/actions/workflows/ci.yml/badge.svg)](https://github.com/rolodexter/rolodexter/actions/workflows/ci.yml)
61
+ [![PyPI](https://img.shields.io/pypi/v/rolodexter)](https://pypi.org/project/rolodexter/)
62
+ [![Python](https://img.shields.io/pypi/pyversions/rolodexter)](https://pypi.org/project/rolodexter/)
63
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
64
+
65
+ </div>
66
+
67
+ ---
68
+
69
+ ## The Problem
70
+
71
+ Every CRM, email platform, and CSV export uses different field names for the same data:
72
+
73
+ | Service | First Name | Phone | Company |
74
+ | ---------- | ------------ | ----------------- | ----------------------- |
75
+ | HubSpot | `firstname` | `mobilephone` | `company` |
76
+ | Salesforce | `FirstName` | `MobilePhone` | `Company` |
77
+ | Mailchimp | `FNAME` | `PHONE` | `COMPANY` |
78
+ | Google CSV | `Given Name` | `Phone 1 - Value` | `Organization 1 - Name` |
79
+ | Random CSV | `Column A` | `Column B` | `Column C` |
80
+
81
+ ## The Solution
82
+
83
+ ```python
84
+ from rolodexter import ContactMapper
85
+
86
+ mapper = ContactMapper()
87
+
88
+ result = mapper.map_payload({
89
+ "fname": "jane",
90
+ "surname": "doe",
91
+ "mobile": "+1-555-019-9876",
92
+ "employer": "Tech Corp",
93
+ "Column 1": "jane.doe@example.com", # auto-detected by shape
94
+ })
95
+
96
+ print(result.normalized)
97
+ # {
98
+ # "first_name": "Jane",
99
+ # "last_name": "Doe",
100
+ # "phone": "+15550199876",
101
+ # "company": "Tech Corp",
102
+ # "email": "jane.doe@example.com"
103
+ # }
104
+ ```
105
+
106
+ ## Installation
107
+
108
+ ```bash
109
+ # Core (zero dependencies)
110
+ pip install rolodexter
111
+
112
+ # With fuzzy matching for typo recovery
113
+ pip install rolodexter[fuzzy]
114
+
115
+ # With on-demand i18n translation (40 languages)
116
+ pip install rolodexter[i18n]
117
+
118
+ # Everything
119
+ pip install rolodexter[all]
120
+
121
+ # Development
122
+ pip install rolodexter[dev]
123
+ ```
124
+
125
+ ## Features
126
+
127
+ ### ๐ŸŽฏ Four-Layer Matching Pipeline
128
+
129
+ Every field runs through the strategy chain in priority order:
130
+
131
+ 1. **Service Match** โ€” instant lookup against 20+ platform-specific dictionaries
132
+ 2. **Exact Match** โ€” O(1) hit against 300+ known aliases
133
+ 3. **Fuzzy Match** โ€” `rapidfuzz` catches typos like `"phne_nmbr"` โ†’ `phone`
134
+ 4. **Heuristic Match** โ€” regex detects emails, phones, URLs, postal codes by *data shape*
135
+
136
+ ### ๐Ÿ“Š Confidence Scoring
137
+
138
+ Every match comes with a confidence score (0.0โ€“1.0):
139
+
140
+ ```python
141
+ match = mapper.identify("fname")
142
+ # FieldMatch(original='fname', canonical='first_name', confidence=1.0, strategy='exact')
143
+
144
+ match = mapper.identify("phne")
145
+ # FieldMatch(original='phne', canonical='phone', confidence=0.85, strategy='fuzzy')
146
+
147
+ match = mapper.identify("Column X", value="jane@test.com")
148
+ # FieldMatch(original='Column X', canonical='email', confidence=0.6, strategy='heuristic')
149
+ ```
150
+
151
+ ### ๐Ÿ”Œ 20+ Service Profiles
152
+
153
+ Built-in mappings for:
154
+
155
+ | CRM / Sales | Email / Marketing | Productivity | Other |
156
+ | ----------- | ------------------ | --------------- | -------- |
157
+ | HubSpot | Mailchimp | Google Contacts | Stripe |
158
+ | Salesforce | SendGrid | Apple Contacts | Notion |
159
+ | Pipedrive | Brevo (Sendinblue) | Outlook | Airtable |
160
+ | Zoho | ConvertKit (Kit) | LinkedIn Export | โ€” |
161
+ | Close CRM | ActiveCampaign | โ€” | โ€” |
162
+ | Freshsales | Omnisend | โ€” | โ€” |
163
+ | โ€” | Beehiiv | โ€” | โ€” |
164
+ | โ€” | Resend | โ€” | โ€” |
165
+ | โ€” | Intercom | โ€” | โ€” |
166
+
167
+ ### ๐ŸŒ On-Demand i18n (40 Languages)
168
+
169
+ English ships by default. Request any of 40 supported languages and aliases are generated on the fly via Google Translate, then cached so translation only happens once:
170
+
171
+ ```python
172
+ from rolodexter import ContactMapper
173
+
174
+ # Load Spanish aliases on demand
175
+ mapper = ContactMapper(languages=["es"])
176
+ result = mapper.map_payload({"correo_electronico": "juan@example.com"})
177
+ print(result.normalized["email"]) # juan@example.com
178
+ ```
179
+
180
+ ```bash
181
+ # CLI: generate and cache all 40 languages
182
+ python -m rolodexter.i18n
183
+
184
+ # Or specific languages
185
+ python -m rolodexter.i18n --languages es,fr,de
186
+
187
+ # List supported languages
188
+ python -m rolodexter.i18n --list
189
+ ```
190
+
191
+ ### ๐Ÿ”„ Cross-Service Translation
192
+
193
+ ```python
194
+ # Translate HubSpot data directly to Salesforce schema
195
+ salesforce_data = mapper.translate(
196
+ hubspot_payload,
197
+ from_service="hubspot",
198
+ to_service="salesforce",
199
+ )
200
+ ```
201
+
202
+ ### ๐Ÿงน Value Normalization
203
+
204
+ Automatic cleanup on matched fields:
205
+
206
+ - **Phone** โ†’ strips formatting, adds `+` for international
207
+ - **Email** โ†’ lowercase, trimmed
208
+ - **Names** โ†’ title case with particle awareness (`"jane van der berg"` โ†’ `"Jane van der Berg"`)
209
+ - **Addresses** โ†’ excess whitespace collapsed, title-cased
210
+
211
+ ### ๐Ÿ“ฆ Batch Processing
212
+
213
+ ```python
214
+ results = mapper.map_batch([contact1, contact2, contact3, ...])
215
+ ```
216
+
217
+ ### ๐Ÿ“ˆ Rich Diagnostics
218
+
219
+ ```python
220
+ result = mapper.map_payload(data)
221
+
222
+ print(result.match_rate) # 0.857
223
+ print(result.matched_count) # 6
224
+ print(result.unmatched_count) # 1
225
+ print(result.to_dict()) # Full JSON-serializable report
226
+ ```
227
+
228
+ ## API Reference
229
+
230
+ ### `ContactMapper`
231
+
232
+ ```python
233
+ ContactMapper(
234
+ *,
235
+ patterns=None, # Custom pattern dict
236
+ patterns_path=None, # Path to custom patterns.json
237
+ default_service=None, # Default service profile
238
+ normalize=True, # Apply value normalization
239
+ strategies=None, # Override strategy pipeline
240
+ languages=None, # i18n: None=English only, "es", ["es","fr"], "all"
241
+ )
242
+ ```
243
+
244
+ **Methods:**
245
+
246
+ | Method | Description |
247
+ | ------------------------------------------------- | ----------------------------- |
248
+ | `identify(header, *, value, service)` | Resolve a single field header |
249
+ | `map_payload(payload, *, service)` | Normalize an entire dict |
250
+ | `map_batch(payloads, *, service)` | Process multiple payloads |
251
+ | `translate(payload, *, from_service, to_service)` | Cross-service translation |
252
+
253
+ ### `CanonicalField`
254
+
255
+ Enum of all 50+ canonical fields. Inherits from `str` for JSON compatibility:
256
+
257
+ ```python
258
+ from rolodexter import CanonicalField
259
+
260
+ assert CanonicalField.EMAIL == "email"
261
+ assert CanonicalField.PHONE.value == "phone"
262
+ ```
263
+
264
+ ### Custom Patterns
265
+
266
+ ```python
267
+ custom = {
268
+ "fields": {
269
+ "first_name": ["fname", "given", "nombre"],
270
+ "loyalty_tier": ["tier", "vip_level", "membership"],
271
+ },
272
+ "services": {
273
+ "my_crm": {
274
+ "contact_first": "first_name",
275
+ "loyalty": "loyalty_tier",
276
+ }
277
+ }
278
+ }
279
+
280
+ mapper = ContactMapper(patterns=custom)
281
+ ```
282
+
283
+ ## Architecture
284
+
285
+ ```
286
+ rolodexter/
287
+ โ”œโ”€โ”€ __init__.py # Public API
288
+ โ”œโ”€โ”€ core.py # ContactMapper, PatternRegistry, strategies, normalizers
289
+ โ”œโ”€โ”€ _phone.py # Built-in E.164 phone parser (zero deps)
290
+ โ”œโ”€โ”€ i18n.py # On-demand i18n generator (40 languages, cached)
291
+ โ””โ”€โ”€ _data/
292
+ โ”œโ”€โ”€ patterns.json # Master truth table (550+ aliases, 20+ services)
293
+ โ””โ”€โ”€ i18n/ # Cached language files (generated on demand)
294
+ ```
295
+
296
+ ## Contributing
297
+
298
+ ```bash
299
+ git clone https://github.com/rolodexter/rolodexter.git
300
+ cd rolodexter
301
+ pip install -e ".[dev]"
302
+ pytest
303
+ ```
304
+
305
+ ## License
306
+
307
+ MIT โ€” see [LICENSE](LICENSE).
@@ -0,0 +1,256 @@
1
+ <div align="center">
2
+
3
+ # ๐Ÿ“‡ Rolodexter
4
+
5
+ **The universal contact field mapper.**
6
+
7
+ Route messy, inconsistent contact data from *any* source to a clean, canonical schema.
8
+
9
+ [![CI](https://github.com/rolodexter/rolodexter/actions/workflows/ci.yml/badge.svg)](https://github.com/rolodexter/rolodexter/actions/workflows/ci.yml)
10
+ [![PyPI](https://img.shields.io/pypi/v/rolodexter)](https://pypi.org/project/rolodexter/)
11
+ [![Python](https://img.shields.io/pypi/pyversions/rolodexter)](https://pypi.org/project/rolodexter/)
12
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
13
+
14
+ </div>
15
+
16
+ ---
17
+
18
+ ## The Problem
19
+
20
+ Every CRM, email platform, and CSV export uses different field names for the same data:
21
+
22
+ | Service | First Name | Phone | Company |
23
+ | ---------- | ------------ | ----------------- | ----------------------- |
24
+ | HubSpot | `firstname` | `mobilephone` | `company` |
25
+ | Salesforce | `FirstName` | `MobilePhone` | `Company` |
26
+ | Mailchimp | `FNAME` | `PHONE` | `COMPANY` |
27
+ | Google CSV | `Given Name` | `Phone 1 - Value` | `Organization 1 - Name` |
28
+ | Random CSV | `Column A` | `Column B` | `Column C` |
29
+
30
+ ## The Solution
31
+
32
+ ```python
33
+ from rolodexter import ContactMapper
34
+
35
+ mapper = ContactMapper()
36
+
37
+ result = mapper.map_payload({
38
+ "fname": "jane",
39
+ "surname": "doe",
40
+ "mobile": "+1-555-019-9876",
41
+ "employer": "Tech Corp",
42
+ "Column 1": "jane.doe@example.com", # auto-detected by shape
43
+ })
44
+
45
+ print(result.normalized)
46
+ # {
47
+ # "first_name": "Jane",
48
+ # "last_name": "Doe",
49
+ # "phone": "+15550199876",
50
+ # "company": "Tech Corp",
51
+ # "email": "jane.doe@example.com"
52
+ # }
53
+ ```
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ # Core (zero dependencies)
59
+ pip install rolodexter
60
+
61
+ # With fuzzy matching for typo recovery
62
+ pip install rolodexter[fuzzy]
63
+
64
+ # With on-demand i18n translation (40 languages)
65
+ pip install rolodexter[i18n]
66
+
67
+ # Everything
68
+ pip install rolodexter[all]
69
+
70
+ # Development
71
+ pip install rolodexter[dev]
72
+ ```
73
+
74
+ ## Features
75
+
76
+ ### ๐ŸŽฏ Four-Layer Matching Pipeline
77
+
78
+ Every field runs through the strategy chain in priority order:
79
+
80
+ 1. **Service Match** โ€” instant lookup against 20+ platform-specific dictionaries
81
+ 2. **Exact Match** โ€” O(1) hit against 300+ known aliases
82
+ 3. **Fuzzy Match** โ€” `rapidfuzz` catches typos like `"phne_nmbr"` โ†’ `phone`
83
+ 4. **Heuristic Match** โ€” regex detects emails, phones, URLs, postal codes by *data shape*
84
+
85
+ ### ๐Ÿ“Š Confidence Scoring
86
+
87
+ Every match comes with a confidence score (0.0โ€“1.0):
88
+
89
+ ```python
90
+ match = mapper.identify("fname")
91
+ # FieldMatch(original='fname', canonical='first_name', confidence=1.0, strategy='exact')
92
+
93
+ match = mapper.identify("phne")
94
+ # FieldMatch(original='phne', canonical='phone', confidence=0.85, strategy='fuzzy')
95
+
96
+ match = mapper.identify("Column X", value="jane@test.com")
97
+ # FieldMatch(original='Column X', canonical='email', confidence=0.6, strategy='heuristic')
98
+ ```
99
+
100
+ ### ๐Ÿ”Œ 20+ Service Profiles
101
+
102
+ Built-in mappings for:
103
+
104
+ | CRM / Sales | Email / Marketing | Productivity | Other |
105
+ | ----------- | ------------------ | --------------- | -------- |
106
+ | HubSpot | Mailchimp | Google Contacts | Stripe |
107
+ | Salesforce | SendGrid | Apple Contacts | Notion |
108
+ | Pipedrive | Brevo (Sendinblue) | Outlook | Airtable |
109
+ | Zoho | ConvertKit (Kit) | LinkedIn Export | โ€” |
110
+ | Close CRM | ActiveCampaign | โ€” | โ€” |
111
+ | Freshsales | Omnisend | โ€” | โ€” |
112
+ | โ€” | Beehiiv | โ€” | โ€” |
113
+ | โ€” | Resend | โ€” | โ€” |
114
+ | โ€” | Intercom | โ€” | โ€” |
115
+
116
+ ### ๐ŸŒ On-Demand i18n (40 Languages)
117
+
118
+ English ships by default. Request any of 40 supported languages and aliases are generated on the fly via Google Translate, then cached so translation only happens once:
119
+
120
+ ```python
121
+ from rolodexter import ContactMapper
122
+
123
+ # Load Spanish aliases on demand
124
+ mapper = ContactMapper(languages=["es"])
125
+ result = mapper.map_payload({"correo_electronico": "juan@example.com"})
126
+ print(result.normalized["email"]) # juan@example.com
127
+ ```
128
+
129
+ ```bash
130
+ # CLI: generate and cache all 40 languages
131
+ python -m rolodexter.i18n
132
+
133
+ # Or specific languages
134
+ python -m rolodexter.i18n --languages es,fr,de
135
+
136
+ # List supported languages
137
+ python -m rolodexter.i18n --list
138
+ ```
139
+
140
+ ### ๐Ÿ”„ Cross-Service Translation
141
+
142
+ ```python
143
+ # Translate HubSpot data directly to Salesforce schema
144
+ salesforce_data = mapper.translate(
145
+ hubspot_payload,
146
+ from_service="hubspot",
147
+ to_service="salesforce",
148
+ )
149
+ ```
150
+
151
+ ### ๐Ÿงน Value Normalization
152
+
153
+ Automatic cleanup on matched fields:
154
+
155
+ - **Phone** โ†’ strips formatting, adds `+` for international
156
+ - **Email** โ†’ lowercase, trimmed
157
+ - **Names** โ†’ title case with particle awareness (`"jane van der berg"` โ†’ `"Jane van der Berg"`)
158
+ - **Addresses** โ†’ excess whitespace collapsed, title-cased
159
+
160
+ ### ๐Ÿ“ฆ Batch Processing
161
+
162
+ ```python
163
+ results = mapper.map_batch([contact1, contact2, contact3, ...])
164
+ ```
165
+
166
+ ### ๐Ÿ“ˆ Rich Diagnostics
167
+
168
+ ```python
169
+ result = mapper.map_payload(data)
170
+
171
+ print(result.match_rate) # 0.857
172
+ print(result.matched_count) # 6
173
+ print(result.unmatched_count) # 1
174
+ print(result.to_dict()) # Full JSON-serializable report
175
+ ```
176
+
177
+ ## API Reference
178
+
179
+ ### `ContactMapper`
180
+
181
+ ```python
182
+ ContactMapper(
183
+ *,
184
+ patterns=None, # Custom pattern dict
185
+ patterns_path=None, # Path to custom patterns.json
186
+ default_service=None, # Default service profile
187
+ normalize=True, # Apply value normalization
188
+ strategies=None, # Override strategy pipeline
189
+ languages=None, # i18n: None=English only, "es", ["es","fr"], "all"
190
+ )
191
+ ```
192
+
193
+ **Methods:**
194
+
195
+ | Method | Description |
196
+ | ------------------------------------------------- | ----------------------------- |
197
+ | `identify(header, *, value, service)` | Resolve a single field header |
198
+ | `map_payload(payload, *, service)` | Normalize an entire dict |
199
+ | `map_batch(payloads, *, service)` | Process multiple payloads |
200
+ | `translate(payload, *, from_service, to_service)` | Cross-service translation |
201
+
202
+ ### `CanonicalField`
203
+
204
+ Enum of all 50+ canonical fields. Inherits from `str` for JSON compatibility:
205
+
206
+ ```python
207
+ from rolodexter import CanonicalField
208
+
209
+ assert CanonicalField.EMAIL == "email"
210
+ assert CanonicalField.PHONE.value == "phone"
211
+ ```
212
+
213
+ ### Custom Patterns
214
+
215
+ ```python
216
+ custom = {
217
+ "fields": {
218
+ "first_name": ["fname", "given", "nombre"],
219
+ "loyalty_tier": ["tier", "vip_level", "membership"],
220
+ },
221
+ "services": {
222
+ "my_crm": {
223
+ "contact_first": "first_name",
224
+ "loyalty": "loyalty_tier",
225
+ }
226
+ }
227
+ }
228
+
229
+ mapper = ContactMapper(patterns=custom)
230
+ ```
231
+
232
+ ## Architecture
233
+
234
+ ```
235
+ rolodexter/
236
+ โ”œโ”€โ”€ __init__.py # Public API
237
+ โ”œโ”€โ”€ core.py # ContactMapper, PatternRegistry, strategies, normalizers
238
+ โ”œโ”€โ”€ _phone.py # Built-in E.164 phone parser (zero deps)
239
+ โ”œโ”€โ”€ i18n.py # On-demand i18n generator (40 languages, cached)
240
+ โ””โ”€โ”€ _data/
241
+ โ”œโ”€โ”€ patterns.json # Master truth table (550+ aliases, 20+ services)
242
+ โ””โ”€โ”€ i18n/ # Cached language files (generated on demand)
243
+ ```
244
+
245
+ ## Contributing
246
+
247
+ ```bash
248
+ git clone https://github.com/rolodexter/rolodexter.git
249
+ cd rolodexter
250
+ pip install -e ".[dev]"
251
+ pytest
252
+ ```
253
+
254
+ ## License
255
+
256
+ MIT โ€” see [LICENSE](LICENSE).