Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,24 @@ jobs:
with:
python-version: "3.11"

- name: Install ruff
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff
pip install -e ".[dev]"

- name: Run ruff
run: |
ruff check .

- name: Run mypy
run: |
mypy csv2vcard

test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4
Expand Down
18 changes: 12 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ jobs:
exit 1
fi

- name: Verify __version__ matches tag
run: |
MODULE_VERSION=$(python -c "import re; print(re.search(r'__version__ = \"(.+?)\"', open('csv2vcard/_version.py').read()).group(1))")
if [ "$MODULE_VERSION" != "${{ steps.get_version.outputs.VERSION }}" ]; then
echo "Error: csv2vcard/_version.py ($MODULE_VERSION) doesn't match tag version (${{ steps.get_version.outputs.VERSION }})"
exit 1
fi

- name: Install build dependencies
run: |
python -m pip install --upgrade pip
Expand All @@ -57,15 +65,13 @@ jobs:
run: |
twine check dist/*

# Uses PyPI Trusted Publishing (OIDC via id-token: write) - no API token secret.
# Requires a trusted publisher for this repo/workflow configured on pypi.org.
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
twine upload dist/*
uses: pypa/gh-action-pypi-publish@release/v1

- name: Upload release assets
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: |
dist/*.whl
Expand Down
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Changelog

## 0.6.0

### Fixed

- **Contacts with the same name no longer overwrite each other.** Files are suffixed (`smith_john_2.vcf`, ...) instead of silently replaced.
- **Excel "CSV UTF-8" files work.** The byte order mark no longer corrupts the first column, which previously dropped every contact's last name.
- **Line breaks in a cell can no longer inject properties.** All values are escaped or sanitized, including `\r`.
- **Output follows the vCard specs:** CRLF line endings (also on Windows) and line folding at 75 octets.
- **vCard 4.0:**
- Inline `PHOTO`, `LOGO` and `KEY` use `data:` URIs (`ENCODING=b` is not valid in 4.0).
- Phone numbers are valid `tel:` URIs; local numbers without a country code are written as text.
- `CATEGORIES` and `NICKNAME` keep their list separators instead of collapsing into one value.
- **vCard 3.0:**
- The vCard 2.1 `CHARSET=UTF-8` parameter is no longer emitted.
- `data:` URI photos are converted properly.
- Time zone names use `VALUE=text`.
- **Invalid values are skipped with a warning** instead of being written: geo coordinates, and dates in 3.0.
- **`iter_contacts` streams rows** instead of loading the whole file first.
- **`test_csv2vcard` is no longer collected by pytest** as a test.

### Added

- **vCard 2.1 output** (`-V 2.1`) for legacy Outlook, car kits and feature phones, with quoted-printable encoding for non-ASCII text.
- **RFC 9554 properties in vCard 4.0:** `PRONOUNS` and `SOCIALPROFILE`, plus `LANG`. Columns: `pronouns`, `social_profile`, `language`.
- **Stable UIDs:** UIDs come from the name, organization and email, or from a `uid` / `contact_id` / `external_id` column. Re-importing a re-converted CSV updates contacts instead of duplicating them.
- **Multiple values per field:** numbered columns such as `email_2` or `Phone 3` for phones, emails, websites and social profiles.
- **`--keep-unmapped` / `keep_unmapped=True`:** writes columns that match no field as `X-` properties.
- **Organization-only rows** become company cards (`KIND:org` in 4.0, `X-ABSHOWAS:COMPANY` in 3.0).
- **`PRODID`** is written in 3.0 and 4.0.
- **More date formats:** `DD.MM.YYYY`, unambiguous slashed dates, and dates without a year (`--MM-DD`).
- **`csv2vcard --version`** works without a subcommand.
- **`--strict`** now also fails on malformed rows and undecodable bytes.

### Changed

- **Column headers match loosely:** case-insensitive, and spaces, hyphens and underscores are treated alike (`First Name` = `first_name`).
- **`mobile`, `cell` and `cellphone` columns now map to `phone_cell`** (`TEL;TYPE=CELL`) instead of the default work phone.
- **A `location` column is no longer treated as geo coordinates.**
- **Multi-value fields keep every matching column**, not just the first.
- **Python 3.10+ is required**; Python 3.9 is end-of-life. Python 3.14 is supported.

### Packaging and CI

- **Removed the stale `setup.py`**; `pyproject.toml` is the only build configuration. setuptools >= 77 is required.
- **CI runs mypy** and tests Python 3.10 to 3.14.
- **Releases publish through PyPI Trusted Publishing** (no API token); `action-gh-release` is updated to v2.
44 changes: 33 additions & 11 deletions DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
# csv2vcard

A Python library for converting CSV files to vCard format (3.0 and 4.0).
A Python library for converting CSV files to vCard format (2.1, 3.0 and 4.0).

Create vCards from a spreadsheet of contacts - useful for business cards, QR codes, CRM imports, or transferring contacts between systems.

## Features

- **vCard 3.0 and 4.0 support** - Generate either format
- **vCard 2.1, 3.0 and 4.0** - Standards-compliant output (CRLF line endings, line folding, escaping); 4.0 includes RFC 9554 properties such as pronouns and social profiles, 2.1 targets legacy Outlook, car kits and feature phones
- **Stable UIDs** - Converting the same CSV again produces the same UIDs, so re-imports update contacts instead of duplicating them
- **Custom CSV mapping** - Map any CSV column names to vCard fields
- **Batch processing** - Convert entire directories of CSV files
- **Single-file output** - Combine all contacts into one .vcf file
- **File splitting** - Split output by size (`--max-vcard-file-size`) or contact count (`--max-vcards-per-file`)
- **Multi-type fields** - Multiple phones (`phone_cell`, `phone_home`, `phone_work`, `phone_fax`), emails (`email_home`, `email_work`), and addresses (work + home)
- **Multiple values per field** - Extra emails, phones, websites and social profiles via numbered columns (`email_2`, `Phone 3`, ...)
- **Keep extra columns** - `--keep-unmapped` writes columns that match no field as `X-` properties instead of dropping them
- **Media embedding** - Embed photos, logos, and keys (base64 or URL)
- **Accent stripping** - Remove diacritics for compatibility (`--strip-accents`)
- **Auto-detect encoding** - Handles various file encodings
- **Auto-detect encoding** - Handles various file encodings, including Excel's UTF-8 with BOM
- **Command-line interface** - Convert files directly from terminal
- **Library API** - Use programmatically in your Python code
- **Type hints** - Full typing support for IDE autocomplete
Expand Down Expand Up @@ -48,6 +51,12 @@ csv2vcard convert contacts.csv
# Specify output directory and vCard version
csv2vcard convert contacts.csv -o ./vcards -V 4.0

# vCard 2.1 for legacy Outlook, car kits and feature phones
csv2vcard convert contacts.csv -V 2.1

# Keep columns that match no vCard field as X- properties
csv2vcard convert contacts.csv --keep-unmapped

# Convert all CSVs in a directory
csv2vcard convert ./csv_folder/

Expand Down Expand Up @@ -93,6 +102,7 @@ csv2vcard(
mapping_file="mapping.json", # Custom column names
strip_accents=True, # Remove diacritics
max_vcards_per_file=100, # Split into multiple files
keep_unmapped=True, # Keep unknown columns as X- properties
)

# Convert entire directory
Expand All @@ -106,13 +116,15 @@ test_csv2vcard()

Your CSV file should have column headers that match vCard fields. Use the default names or create a custom mapping.

Headers are matched case-insensitively and treat spaces, hyphens and underscores alike, so `First Name`, `first-name` and `first_name` are equivalent. Exports from Excel (including "CSV UTF-8" with a byte order mark) and Outlook-style headers such as `Business Street` or `Mobile Phone` work out of the box.

### Default Column Names

**Required:** `last_name`, `first_name`
**Required:** `last_name`, `first_name` (rows with only `org` become organization cards)

**Basic fields:**
```
last_name, first_name, middle_name, name_prefix, name_suffix, nickname, gender, birthday, anniversary, org, title, role, note
last_name, first_name, middle_name, name_prefix, name_suffix, nickname, gender, birthday, anniversary, pronouns, language, org, title, role, note, uid
```

**Contact fields (single):**
Expand Down Expand Up @@ -147,9 +159,15 @@ photo, logo, key

**Additional fields:**
```
categories, geo, tz
categories, geo, tz, social_profile
```

**Multiple values:** `phone*`, `email*`, `website` and `social_profile` accept numbered columns for extra values, e.g. `email`, `email_2`, `email_3` or `Phone 1`, `Phone 2`.

**Dates:** `birthday` and `anniversary` accept `YYYY-MM-DD`, `YYYYMMDD`, `DD.MM.YYYY`, `--MM-DD` (no year) and slashed dates when day and month can be told apart. Ambiguous dates such as `06/07/1990` are reported and kept as text in vCard 4.0.

**UIDs:** each vCard gets a UID derived from the name, organization and email, or from the `uid` column (`uid`, `contact_id`, `external_id`) when present.

### Example CSV

```csv
Expand Down Expand Up @@ -187,16 +205,18 @@ Arguments:
Options:
-d, --delimiter TEXT CSV field delimiter (default: ",")
-o, --output PATH Output directory (default: ./export/)
-V, --vcard-version TEXT vCard version: 3.0 or 4.0 (default: 3.0)
-V, --vcard-version TEXT vCard version: 2.1, 3.0 or 4.0 (default: 3.0)
-1, --single-vcard Export all contacts to a single .vcf file
-m, --mapping PATH Path to JSON mapping file
-e, --encoding TEXT CSV file encoding (auto-detected if not set)
-a, --strip-accents Remove accents/diacritics from contact fields
--max-vcard-file-size INT Split output by file size (bytes)
--max-vcards-per-file INT Split output by contact count
--strict Exit on validation errors
--keep-unmapped Keep unmapped columns as X- properties
--strict Fail on validation errors, malformed rows
and undecodable bytes
-v, --verbose Enable verbose output
--version Show version and exit
--version Show version and exit (also: csv2vcard --version)
--help Show help message
```

Expand All @@ -221,6 +241,7 @@ files = csv2vcard(
strip_accents=False, # Remove diacritics
max_file_size=None, # Split by file size (bytes)
max_vcards_per_file=None, # Split by contact count
keep_unmapped=False, # Keep unknown columns as X- properties
)
# Returns: List[Path] of created vCard files

Expand Down Expand Up @@ -251,13 +272,14 @@ contact = Contact(
contact = Contact.from_dict({"last_name": "Doe", "first_name": "John"})

# vCard versions
VCardVersion.V2_1 # vCard 2.1 (legacy)
VCardVersion.V3_0 # vCard 3.0 (RFC 2426)
VCardVersion.V4_0 # vCard 4.0 (RFC 6350)
VCardVersion.V4_0 # vCard 4.0 (RFC 6350 + RFC 9554)
```

## Requirements

- Python 3.9 or higher
- Python 3.10 or higher
- For CLI: `typer` (installed with `csv2vcard[cli]`)
- For encoding detection: `charset-normalizer` (installed with `csv2vcard[encoding]`)

Expand Down
Loading
Loading