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.
Links in the wild
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 atel:URInumber: the number is empty or has a character the grammar does not allowext: the extension has no digit, or a character other than digits and separatorsisub: the subaddress is empty or not1*paramchar(erratum 203)ext-and-isub: both are present, which section 5.3 forbidsphone-context: the context is neither a global number prefix nor a domain namemissing-context: a local number withoutphone-contextglobal-context: a global number withphone-contextparam-name: a parameter name is empty, malformed, or reserved for a fieldparam-value: a value is empty, malformed, badly percent-encoded or not UTF-8duplicate-param: a parameter appears twice (names compare case-insensitively)param-order: withstrict=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.
Links
- Website: https://lianes8studio.com
- GitHub: https://github.com/JaviLianes8
- LinkedIn: https://www.linkedin.com/in/jlianes/
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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| teluri-0.2.0.tar.gz | 24.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.