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 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.
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)
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 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
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.
Links
- Website: https://lianes8studio.com
- GitHub: https://github.com/JaviLianes8
- LinkedIn: https://www.linkedin.com/in/jlianes/
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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.