Busabase
Busabase is an open-source (MIT) database and workspace for AI agents — data, docs, skills, and apps in one place. Run it locally, self-host it inside your network, or use Busabase Cloud, and turn age
翻譯準備中
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Universal contact field mapper for Python and JavaScript. Routes messy contact data from any CRM, CSV, or email platform into one clean canonical schema.
翻譯準備中
正在載入完整 README。 以下為已儲存的摘錄。
The universal contact field mapper.
Route messy, inconsistent contact data from any source to a clean, canonical schema.
RoloDexter is an open-source Python and TypeScript library that maps messy,
inconsistent contact field names, like HubSpot's firstname or a CSV's
Column A, onto one canonical schema of 62 fields. It resolves each header
through a four-layer pipeline, exact, normalized, fuzzy, heuristic, scores
every match's confidence, and normalizes the matched values so the output is
ready to store.
RoloDexter is maintained as a dual-package repository:檢視完整 README ↗The universal contact field mapper.
Route messy, inconsistent contact data from any source to a clean, canonical schema.
RoloDexter is an open-source Python and TypeScript library that maps messy,
inconsistent contact field names, like HubSpot's firstname or a CSV's
Column A, onto one canonical schema of 62 fields. It resolves each header
through a four-layer pipeline, exact, normalized, fuzzy, heuristic, scores
every match's confidence, and normalizes the matched values so the output is
ready to store.
RoloDexter is maintained as a dual-package repository:
| Ecosystem | Package | Source | Package metadata | Publish target |
|---|---|---|---|---|
| Python | rolodexter | src/rolodexter | pyproject.toml | PyPI |
| JavaScript / TypeScript | rolodexter | packages/js/src | packages/js/package.json | npm |
The Python package remains the canonical implementation and owns the shared
patterns.json alias table. The NPM package lives under packages/js, syncs
that alias table during build, and has its own README, TypeScript sources,
tests, package metadata, and publish workflow.
Every CRM, email platform, and CSV export uses different field names for the same data:
| Service | First Name | Phone | Company |
|---|---|---|---|
| HubSpot | firstname | mobilephone | company |
| Salesforce | FirstName | MobilePhone | Company |
| Mailchimp | FNAME | PHONE | COMPANY |
| Google CSV | Given Name | Phone 1 - Value | Organization 1 - Name |
| Random CSV | Column A | Column B | Column C |
from rolodexter import ContactMapper
mapper = ContactMapper()
result = mapper.map_payload({
"fname": "jane",
"surname": "doe",
"mobile": "+1-650-253-0000",
"employer": "Tech Corp",
"Column 1": "[email protected]", # auto-detected by value shape
})
print(result.normalized)
# {
# "first_name": "Jane",
# "last_name": "Doe",
# "phone": "+16502530000",
# "company": "Tech Corp",
# "email": "[email protected]"
# }
# Core (phonenumbers + nameparser)
pip install rolodexter
# With fuzzy matching for typo recovery
pip install rolodexter[fuzzy]
# With on-demand i18n cache generation dependencies (40 languages)
pip install rolodexter[i18n-generate]
# Everything
pip install rolodexter[all]
# Development
pip install rolodexter[dev]
npm install rolodexter
The NPM package source lives in packages/js. For local development:
cd packages/js
npm install
npm test
# See what a file maps to before you write anything
rolodexter profile contacts.csv
# Same report as JSON, for scripting
rolodexter profile contacts.csv --json
# Skip value-level normalization for a faster pass on a big file
rolodexter profile huge.csv --no-normalize --max-rows 5000
profile never writes mapped output. It reports the match rate, which
canonical fields got populated, which headers went unmapped, and categorized
warning counts, so you can see what an import will drop or flag before
running map for real. It accepts the same --region, --languages,
--min-confidence, and --override flags as map.
Every field runs through the strategy chain in priority order:
CamelCase, dot.path, space → underscore, vendor and address prefixes (hs_email, Billing City), a form question's leading "Your" (Your company name), and similar variationsrapidfuzz catches typos like "phne_nmbr" → phoneEvery match comes with a confidence score (0.0–1.0):
match = mapper.identify("fname")
# FieldMatch(original='fname', canonical='first_name', confidence=1.0, strategy='exact')
match = mapper.identify("phne")
# FieldMatch(original='phne', canonical='phone', confidence=0.7, strategy='fuzzy')
match = mapper.identify("Column X", value="[email protected]")
# FieldMatch(original='Column X', canonical='email', confidence=0.6, strategy='heuristic')
For vendor-specific or account-level field names that won't be in the standard alias table:
mapper = ContactMapper(
overrides={
"MMERGE6": "company", # Mailchimp custom merge field
"cf_lead_score": "tags",
}
)
# Extract phones embedded in arbitrary string values
result = mapper.map_payload(
{"notes": "call me at +1-650-253-0000 or +44 20 7946 0958"},
extract_embedded_phones=True,
)
print(result.get_all_phones())
# ['+16502530000', '+442079460958']
Fields like tags are automatically list-normalised: comma-separated strings, JSON arrays, and Python lists all collapse to a clean list:
result = mapper.map_payload({"tags": "vip, newsletter, beta"})
print(result.normalized["tags"])
# ['vip', 'newsletter', 'beta']
English ships by default. Install rolodexter[i18n-generate] to generate any
of 40 supported language caches with the i18n CLI or API, then pass those
languages to ContactMapper; runtime loading is cache-only and never
translates during mapper construction:
from rolodexter import ContactMapper
# Load Spanish aliases from a generated cache
mapper = ContactMapper(languages=["es"])
result = mapper.map_payload({"correo_electronico": "[email protected]"})
print(result.normalized["email"]) # [email protected]
# CLI: generate and cache all 40 languages
python -m rolodexter.i18n
# Or specific languages
python -m rolodexter.i18n --languages es,fr,de
# Bound network behavior during generation
python -m rolodexter.i18n --languages es,fr --timeout 10 --retries 1 --workers 4
# List supported languages
python -m rolodexter.i18n --list
Supported: Spanish, French, German, Portuguese, Italian, Dutch, Polish, Romanian, Turkish, Russian, Japanese, Chinese (Simplified), Korean, Arabic, Hindi, Swedish, Danish, Norwegian, Finnish, Czech, Ukrainian, Greek, Hungarian, Thai, Vietnamese, Indonesian, Malay, Hebrew, Bulgarian, Croatian, Slovak, Slovenian, Serbian, Lithuanian, Latvian, Estonian, Catalan, Filipino, Swahili, Afrikaans.
Automatic cleanup on matched fields:
+16502530000)"jane van der berg" → "Jane van der Berg")list[str]birthday, created_at, updated_at, last_contacted. Ambiguous values (03/04/2024, a two-digit year) are left unchanged and reported as a warning instead of guessed."Deutschland" → "DE")"california" → "CA")results = mapper.map_batch([contact1, contact2, contact3, ...])
# Constant-memory streaming for huge CSV/JSONL exports:
import csv
with open("contacts.csv") as fh:
for result in mapper.map_stream(csv.DictReader(fh)):
save(result.normalized)
# Preview import readiness without retaining mapped rows:
profile = mapper.profile(contacts, max_rows=1_000)
print(profile.explain())
import pandas as pd
from rolodexter import ContactMapper
df = pd.read_csv("hubspot_export.csv")
clean = ContactMapper().map_dataframe(df) # pip install rolodexter[pandas]
# Columns renamed to canonical fields, values normalized, unmatched columns kept.
# Map a CSV/JSON/JSONL export to the canonical schema
rolodexter map contacts.csv -o clean.csv --region US
# Stream JSON Lines, drop low-confidence guesses, fail loudly
rolodexter map export.jsonl --min-confidence 0.8 --strict -o out.jsonl
# JSON/CSV output paths are bounded; JSONL output remains streaming
rolodexter map huge.jsonl --format jsonl --max-materialized-rows 100000
# Keep processing after bad rows, preserving failures in a JSONL quarantine file
rolodexter map export.jsonl --strict --on-error quarantine -o clean.jsonl
# Carry unmapped columns through instead of dropping them, and drop rows
# that share an identity key (email/phone/source id) with an earlier row
rolodexter map contacts.csv --keep-unmapped --dedupe -o clean.csv
# Force a vendor-specific column to a canonical field
rolodexter map contacts.csv --override "MMERGE3=full_address" -o clean.csv
# Save the resolved header plan, then replay it on a later import so columns
# route identically even after a patterns.json change (a mapping lockfile)
rolodexter map jan.csv --schema-out plan.json -o jan-clean.csv
rolodexter map feb.csv --schema-in plan.json -o feb-clean.csv
# See exactly how a header resolves
rolodexter explain "Job Titel" --value CEO
# 'Job Titel' -> job_title [fuzzy, conf=0.70]
rolodexter --version
rolodexter fields # list every canonical field
# Non-fatal issues are reported, never silent:
result = mapper.map_payload({"mobile": "not a phone"})
print(result.warnings)
# ("'mobile': phone value 'not a phone' could not be normalized to E.164 ...",)
# Each warning is a str subclass carrying its category, for grouping by code
# instead of matching substrings of the message text:
print(result.warnings[0].category) # 'phone_normalization'
# Demand high-confidence mappings; fail loudly on any problem:
mapper = ContactMapper(strict=True, confidence_threshold=0.8)
print(result.explain()) # human-readable resolution + warnings
schema = mapper.compile_schema(["First Name", "Mobile Phone", "Org"])
schema.column_map() # {'First Name': 'first_name', 'Mobile Phone': 'phone', 'Org': 'company'}
schema.apply(row) # reuse the resolved plan per row
# Save the plan and replay it later so columns route identically, even after
# a patterns.json update (the "map --schema-out" / "--schema-in" CLI flags
# do this for you):
plan = schema.to_dict()
schema2 = MappingSchema.from_dict(plan, mapper)
result = mapper.map_payload(data)
print(result.match_rate) # 0.857
print(result.matched_count) # 6
print(result.unmatched_count) # 1
print(result.get_all_phones()) # ['+16502530000']
print(result.get_all_emails()) # ['[email protected]']
print(result.get_identity_keys())
# ['email:[email protected]', 'phone:+16502530000', 'source:hubspot:123']
print(result.to_dict()) # Full JSON-serializable report
# Flatten one level of nesting with depth=2
result = mapper.map_payload(
{"contact": {"fname": "Jane", "lname": "Doe"}},
depth=2,
)
# Accesses "contact.fname" and "contact.lname"
ContactMapperContactMapper(
*,
patterns=None, # Custom pattern dict (overrides built-in)
patterns_path=None, # Path to a custom patterns.json file
normalize=True, # Apply value normalization after mapping
strategies=None, # Override the default strategy pipeline
languages=None, # None=English only | "es" | ["es","fr"] | "all"
overrides=None, # Extra alias→canonical mappings {"MMERGE6": "company"}
default_region="US", # ISO-3166 region for phone parsing/E.164
strict=False, # Raise NormalizationError on any warning
confidence_threshold=0.0, # Drop matches below this confidence to unmapped
header_cache_max_size=4096,# Bound header-resolution cache; None=unbounded
)
Methods:
| Method | Description |
|---|---|
identify(header, *, value) | Resolve a single header to a FieldMatch |
trace_header(header, *, value) | Every pipeline layer's verdict, in order |
map_payload(payload, *, depth, ...) | Normalize an entire dict → MappingResult |
map_batch(payloads, *, ...) | Process a list of payloads → list[MappingResult] |
map_stream(iterable, *, ...) | Lazily yield results (constant memory) |
profile(iterable, *, max_rows, ...) | Aggregate import-readiness diagnostics |
compile_schema(headers) | Resolve headers once → reusable MappingSchema |
map_dataframe(df) | Rename/normalize a pandas DataFrame |
clear_cache() | Clear cached header-resolution verdicts |
cache_info() | Inspect header cache size/configuration |
registry | Access the underlying PatternRegistry |
map_dataframe() guarantees unique output labels. It skips reserved
<canonical>__N names when an unmatched source column already uses one, and
rejects duplicate input labels because they cannot be renamed without
ambiguity.
FieldMatchFieldMatch(
original='fname',
canonical='first_name',
confidence=1.0,
strategy='exact', # 'exact' | 'normalized' | 'fuzzy' | 'heuristic' | 'none'
is_matched=True,
)
MappingResult| Attribute / Method | Type | Description |
|---|---|---|
normalized | dict | Canonical key → cleaned value |
unmapped | dict | Fields that couldn't be resolved |
field_matches | tuple[FieldMatch, ...] | Full match detail for every input field |
match_rate | float | Fraction of fields successfully matched |
matched_count | int | Count of matched fields |
unmatched_count | int | Count of unmatched fields |
warnings | tuple[str, ...] | Non-fatal issues (failed E.164, dropped matches) |
get_match(header) | FieldMatch | None | O(1) lookup of the match for an input header |
get_all_phones() | list[str] | All phone values across all phone-adjacent fields |
get_all_emails() | list[str] | All email values, flattened and deduplicated |
get_identity_keys() | list[str] | Stable email/phone/source keys for deduplication |
explain() | str | Human-readable resolution + warnings summary |
to_dict() | dict | Full JSON-serializable report |
MappingProfileContactMapper.profile() consumes any iterable in constant memory and reports
rows and fields seen, aggregate match rate, canonical-field counts, unmapped
header counts, strategy usage, and categorized warnings. Use max_rows for a
bounded preview; the next iterator item is left untouched.
profile = mapper.profile(rows, max_rows=1_000, confidence_threshold=0.8)
profile.match_rate
profile.warning_counts
profile.to_dict() # JSON-ready
profile.explain() # compact report for logs or a CLI
CanonicalFieldEnum of all 62 canonical fields. Inherits from str for JSON compatibility:
from rolodexter import CanonicalField
assert CanonicalField.EMAIL == "email"
assert CanonicalField.PHONE.value == "phone"
first_name · last_name · full_name · middle_name · nickname · prefix · suffix · email · phone · home_phone · work_phone · fax · whatsapp · website · company · job_title · department · industry · address_line1 · address_line2 · city · state · postal_code · country · full_address · linkedin · twitter · facebook · instagram · github · youtube · tiktok · discord · telegram · lead_status · lifecycle_stage · email_opt_out · tags · source · utm_parameters · score · owner · birthday · age · created_at · updated_at · last_contacted · revenue · currency · message · subject · company_size · notes · metadata · gender · timezone · language_preference · referrer_url · source_id · source_service · subscribed · verified · unknown
custom = {
"fields": {
"first_name": ["fname", "given", "nombre"],
"loyalty_tier": ["tier", "vip_level", "membership"],
}
}
mapper = ContactMapper(patterns=custom)
Custom pattern data is validated when the registry is constructed. fields
must be an object whose values are lists of non-empty aliases; malformed files
raise PatternLoadError with the failing section instead of partially building
an index.
rolodexter/
├── pyproject.toml # Python/PyPI package metadata
├── src/rolodexter/ # Python package source
├── tests/ # Python tests
├── packages/js/package.json # JavaScript/NPM package metadata
├── packages/js/src/ # TypeScript package source
├── packages/js/test/ # JavaScript package tests
└── scripts/ # Cross-language release/parity probes and sweep
src/rolodexter/
├── __init__.py # Public API
├── __main__.py # CLI: rolodexter map / explain / fields
├── core.py # ContactMapper, PatternRegistry, strategies, normalizers
├── _phone.py # E.164 phone parser (wraps libphonenumber)
├── i18n.py # On-demand i18n generator (40 languages, cached)
└── patterns.json # Master alias table (600+ aliases, 62 canonical fields)
git clone https://github.com/LunarWerxs/RoloDexter.git
cd rolodexter
pip install -e ".[dev]"
pytest
The public API of both packages is pinned by golden snapshots in
scripts/api_golden/: one text file per export, plus a hash per export and
member that also covers every type it depends on. npm run check:parity
(run by CI) fails when either surface changes and labels each difference:
a removed name is BREAKING, a changed one POTENTIALLY_BREAKING, a new one
POTENTIALLY_NON_BREAKING, with a suggested version bump. After a deliberate
API change, review the report and accept it:
python scripts/api_snapshot.py # report; exits 1 on any change
python scripts/api_snapshot.py --update-goldens # rewrite only the affected goldens
The Python goldens are rendered with the Python version CI pins (3.14):
inspect.signature and dataclass-generated __init__ text can differ between
versions, so run the snapshot (and npm pack, whose prepack runs it) on
3.14, or pass --lang js to check only the Node half.
Is RoloDexter free? Yes. RoloDexter is MIT-licensed and free to use in commercial and open-source projects, both the Python package on PyPI and the JavaScript/TypeScript package on npm. There is no paid tier, account, or API key involved: it installs as a normal dependency and runs entirely inside your own process.
Does it work offline?
Yes, for field mapping and value normalization, both run entirely locally
with no network calls. The one exception is the optional i18n alias
generator (rolodexter[i18n-generate]), which calls Google Translate to
build language caches; that step is explicit, one-time, and the caches it
produces are then loaded offline with no further network access.
What are the system requirements?
The Python package needs Python 3.10 or newer (tested through 3.14) plus
its two core dependencies, phonenumbers and nameparser; optional extras
add rapidfuzz for fuzzy matching, pandas for DataFrame support, or
translation libraries for i18n generation. The JavaScript/TypeScript package
needs Node.js 20 or newer. Neither requires a database or server.
How is it different from Zapier's or Make's field mapping? Zapier and Make connect two apps by having you manually map each field between them for every workflow you build, by hand, per pill. RoloDexter instead resolves headers automatically against a built-in table of 600+ aliases across 62 canonical fields, so a HubSpot export and a random CSV both land on the same schema without you wiring each column yourself.
How is it different from an ETL platform like Talend or Alteryx? Talend and Alteryx are full ETL platforms with schema mapping as one feature among many; Talend Data Fabric alone is priced for enterprise budgets, commonly six figures a year. RoloDexter is a free, MIT-licensed library that does one job, contact field mapping and value normalization, embedded directly in your own Python or Node code with no platform to manage.
How is it different from using phonenumbers or nameparser directly?
phonenumbers and nameparser, two libraries RoloDexter depends on, each
solve one field type: phone parsing and name parsing. Neither tells you
which of fifty CSV columns is a phone number, or that mobilephone and
mobile both mean phone. RoloDexter runs header identification across all
62 canonical fields first, then hands the matched values to libraries like
these for normalization.
What data formats does it support?
CSV, JSON, and JSON Lines files through the CLI (rolodexter map), plus
plain Python dicts, lists, and streaming iterables through the library API,
and pandas DataFrames with the pandas extra installed. Output can be
written as CSV, JSON, or streamed JSONL, and a --schema-out/--schema-in
lockfile replays a resolved header plan on later imports.
Is my data sent anywhere? No. Field mapping and value normalization run entirely inside your own Python or Node process; RoloDexter makes no network calls and has no server component. The only exception is the optional, explicitly-invoked i18n alias generator, which sends field names, not your contact data, to Google Translate to build a local language cache.
MIT, see LICENSE.
Made by LunarWerx Studios, the team also behind RepoYeti, AgentHydra, and SageThumbs.