Skip to main content

teluri

Strict, typed, zero-dependency parser, builder and comparator for tel: URIs (RFC 3966), with a reader for the tel: links found in real web pages and apps.

Install

pip install teluri

Usage

Parse

from teluri import parse

uri = parse("tel:+1-201-555-0123;ext=1234")
print(uri.number)  # +12015550123
print(uri.is_global)  # True
print(uri.ext)  # 1234
print(uri.is_e164)  # True
print(uri)  # tel:+12015550123;ext=1234

local = parse("tel:863-1234;phone-context=+1-914-555")
print(local.number)  # 8631234
print(local.phone_context)  # +1914555
print(local.is_global)  # False

parse validates the whole RFC 3966 grammar and returns a frozen TelURI. Visual separators (-, ., (, )) are removed from the number, the extension and a numeric phone-context. Other parameters land in params, keyed by lower-case name, with percent-decoded values and None for a parameter without a value. str() gives a normalised form: separators removed, names in lower case, values re-encoded, and ext or isub first, then phone-context, then the rest sorted by name, as section 3 asks. The letter case of numbers, contexts and values is kept, so two equivalent URIs may still print differently. Wrong types (parse(None), TelURI(123)) raise TypeError.

Errors

from teluri import TelURIError, parse

reasons = []
for bad in ["tel:863-1234", "tel:+1;ext=1;isub=2", "tel:+1;b;a"]:
    try:
        parse(bad, strict=True)
    except TelURIError as error:
        reasons.append(error.reason)
print(reasons)  # ['missing-context', 'ext-and-isub', 'param-order']

The ABNF accepts parameters in any order, so parse does too; strict=True also enforces the order rule of section 3, comparing names in lower case.

Compare

from teluri import equivalent, parse

print(equivalent("tel:+1-201-555-0123;EXT=1;a=X", "tel:+12015550123;a=x;ext=1"))  # True
domain = "tel:7042;phone-context=Example.COM."
print(equivalent(domain, "tel:7042;phone-context=example.com"))  # True
print(equivalent("tel:+1234", "tel:1234;phone-context=+1"))  # False
print(len({parse("tel:+1;b;a"), parse("tel:+1;A;B")}))  # 1

==, hash and equivalent follow section 4: global and local numbers never match, separators are ignored, a numeric context is compared digit by digit and a domain context as a host name (a final root dot is ignored), parameters are compared by name in any order, and the comparison is case-insensitive. Values, including isub, are compared as their percent-decoded octets with ASCII letters folded, so %2F equals / and %41 equals a, while %C3%A9 and %C3%89 stay different.

Build

from teluri import build

print(build("+1 (201) 555-0123", ext="1234"))  # tel:+12015550123;ext=1234
print(build("7042", phone_context="example.com"))  # tel:7042;phone-context=example.com
print(build("+1 201 555 0123", params={"note": "a;b c"}))  # tel:+12015550123;note=a%3Bb%20c

build takes human input: it removes spaces and separators from the numbers, encodes parameter values, refuses ext, isub and phone-context inside params, and always writes the canonical order. The result always parses back with strict=True.

from teluri import parse_link

print(parse_link("tel:+1 201 555 0123"))  # tel:+12015550123
print(parse_link("tel:1-408-555-5555", context="+1"))  # tel:14085555555;phone-context=+1
print(parse_link("tel:*31%23", context="example.com").href)  # tel:*31%23;phone-context=example.com

Web pages and apps often write tel: links that RFC 3966 does not allow: spaces as in printed numbers, percent-encoded numbers, and local numbers without a phone-context (Apple's Phone Links reference shows tel:1-408-555-5555). parse_link decodes and cleans the number, adds context (checked as a single phone-context descriptor) to a local number that has none, then validates the result as RFC 3966. In the other direction, TelURI.href writes # as %23: an HTML URL parser would otherwise read it as the start of a fragment. Apple also documents that its Phone app does not dial a link containing * or #.

Command line

$ teluri "tel:+1-201-555-0123;Ext=9" "tel:863-1234"
tel:+12015550123;ext=9
invalid (missing-context): tel:863-1234

One URI per argument, or one per line on standard input. --json prints every field, --strict enforces the parameter order, --link reads links as above (not with --strict) and --context gives local links their context. Output is ASCII, so any console can print it. The exit status is 1 when any input is invalid, 2 for bad usage.

API

Name What it does
parse(uri, *, strict=False) Parse and validate a tel: URI, return a TelURI
parse_link(href, *, context=None) Read a real-world tel: link, return a TelURI
build(number, *, ext, isub, phone_context, params) Canonical tel: URI from human input
equivalent(a, b) Section 4 comparison of two URIs (strings or TelURI)
TelURI Frozen, hashable result: number, ext, isub, phone_context, params, is_global, is_e164, unknown_mandatory, href
TelURIError ValueError with a reason from Reason
Reason The closed set of reasons, listed below

is_e164 checks the E.164 limits only: a global number of at most 15 digits whose country code does not start with 0. unknown_mandatory lists m- parameters; section 5.4 says not to use a URI whose mandatory parameters you do not understand. isub is kept in its encoded form because a subaddress may carry any octet.

Reasons

  • scheme: not a tel: URI
  • number: the number is empty or has a character the grammar does not allow
  • ext: the extension has no digit, or a character other than digits and separators
  • isub: the subaddress is empty or not 1*paramchar (erratum 203)
  • ext-and-isub: both are present, which section 5.3 forbids
  • phone-context: the context is neither a global number prefix nor a domain name
  • missing-context: a local number without phone-context
  • global-context: a global number with phone-context
  • param-name: a parameter name is empty, malformed, or reserved for a field
  • param-value: a value is empty, malformed, badly percent-encoded or not UTF-8
  • duplicate-param: a parameter appears twice (names compare case-insensitively)
  • param-order: with strict=True, parameters are not in the section 3 order

Why

RFC 3966 tel: URIs are their own grammar: global numbers with a leading +, local numbers that must carry a phone-context, visual separators, ext and isub, and a comparison rule that ignores separators, case and parameter order. The standard library does not model any of it, general URI libraries stop at RFC 3986, and phonenumbers is a number-validation database rather than a URI codec. teluri is a small, strict, typed one, with the browser and phone dialect handled explicitly instead of by accident.

Sources

RFC 3966 and its errata, the IANA tel URI parameter registry, RFC 4694, 4715, 4759 and 4904 for the registered parameters. Their examples are checked by the test suite; run python scripts/fetch_upstream.py to fetch the texts and enable the tests that compare against them.

License

MIT. See LICENSE.

Metadata

Release files for teluri 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for teluri 0.2.0
File Size Uploaded Provenance
teluri-0.2.0.tar.gz 24.9 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for teluri 0.2.0
File Interpreter ABI Platform
teluri-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 41.3 kB

Release files / teluri-0.2.0.tar.gz

Download URL teluri-0.2.0.tar.gz
Size 24.9 kB
Tags Source
SHA-256 checksum
How to use checksums
80122027ad3af5e5f7be6e675b71aa2304910e7eaf9b7278396f8bb605fb6707
BLAKE2b-256 checksum
How to use checksums
261a1293c863b3c0f5234d9f6fecb97dfad4544ead47d648b3e2c02245fd90be
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release files / teluri-0.2.0-py3-none-any.whl

Download URL teluri-0.2.0-py3-none-any.whl
Size 16.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d222528d31a71d5d1f1fe27fe707e084c5e20d3f8450f579483d58b1dd27327c
BLAKE2b-256 checksum
How to use checksums
d9e4de85e362a442f7ca9324a8e4290194cd021fbc2852205cfbb4b6fa83f14a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Security for teluri 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page