Skip to main content

teluri

Lianes8 Certified

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 RFC 3966 syntax with the policies in the guide 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)

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. Diagnostics contain the reason, never the input. A closed output pipe exits 1 quietly.

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 only global syntax, at most 15 digits and a nonzero first digit; it does not check country allocation or dialability. 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

Use teluri to preserve URI parameters, validate local contexts and compare URI identity. urllib.parse splits URL components; it does not validate telephone syntax. phonenumbers parses and formats phone numbers, including RFC 3966 forms, and supplies numbering-plan validation. Choose it when you need country or number validity checks. teluri keeps arbitrary URI parameters and local identifiers without a numbering database or runtime dependencies.

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.

See the guide and limits, release changes, and reference manifest.

License

MIT. See LICENSE.

Metadata

Release files for teluri 1.0.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 1.0.0
File Size Uploaded Provenance Actions
teluri-1.0.0.tar.gz 43.6 kB Without provenance Details for teluri-1.0.0.tar.gz

Built distribution (wheel)

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

Total release size: 61.4 kB

Release files / teluri-1.0.0.tar.gz

Download URL teluri-1.0.0.tar.gz
Size 43.6 kB
Tags Source
SHA-256 checksum
How to use checksums
163a837314b69e189b8936eceb90cfb695c73887eae23499f54b9a67c99f4094
BLAKE2b-256 checksum
How to use checksums
91193ff0a2ea3bfb1cdb4adcd6fda9dd4bdae4de66dc040f63bd1e232dcae815
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

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

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

Security for teluri 1.0.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.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.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