teluri 0.2.0__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- teluri-1.0.0/CHANGELOG.md +103 -0
- {teluri-0.2.0 → teluri-1.0.0}/CONTRIBUTING.md +21 -19
- teluri-1.0.0/GUIDE.md +139 -0
- {teluri-0.2.0 → teluri-1.0.0}/LICENSE +21 -21
- {teluri-0.2.0 → teluri-1.0.0}/PKG-INFO +23 -13
- {teluri-0.2.0 → teluri-1.0.0}/README.md +19 -11
- {teluri-0.2.0 → teluri-1.0.0}/pyproject.toml +8 -3
- teluri-1.0.0/requirements-dev.txt +3 -0
- teluri-1.0.0/scripts/check.ps1 +16 -0
- {teluri-0.2.0 → teluri-1.0.0}/scripts/fetch_upstream.py +5 -2
- {teluri-0.2.0 → teluri-1.0.0}/src/teluri/__init__.py +9 -1
- {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_build.py +8 -2
- {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_chars.py +11 -1
- {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_cli.py +43 -13
- {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_errors.py +13 -0
- {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_grammar.py +12 -13
- {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_link.py +15 -9
- {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_model.py +40 -11
- {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_parse.py +24 -6
- {teluri-0.2.0 → teluri-1.0.0}/tests/conftest.py +5 -2
- {teluri-0.2.0 → teluri-1.0.0}/tests/test_cli.py +2 -2
- teluri-1.0.0/tests/test_cli_contracts.py +97 -0
- teluri-1.0.0/tests/test_compatibility.py +48 -0
- teluri-1.0.0/tests/test_contract_api.py +116 -0
- teluri-1.0.0/tests/test_contract_edges.py +61 -0
- {teluri-0.2.0 → teluri-1.0.0}/tests/test_docs.py +2 -2
- teluri-1.0.0/tests/test_e2e_wheel.py +186 -0
- teluri-1.0.0/tests/test_error_chaining.py +125 -0
- teluri-1.0.0/tests/test_properties.py +75 -0
- {teluri-0.2.0 → teluri-1.0.0}/tests/test_review.py +11 -5
- teluri-1.0.0/tests/test_review_contracts.py +86 -0
- teluri-1.0.0/tests/test_safety.py +155 -0
- teluri-1.0.0/tests/test_spec.py +127 -0
- teluri-1.0.0/tests/test_string_inputs.py +189 -0
- {teluri-0.2.0 → teluri-1.0.0}/tests/test_typing.py +14 -15
- teluri-1.0.0/tests/test_upgrade.py +64 -0
- teluri-1.0.0/tests/upstream/MANIFEST.json +110 -0
- teluri-0.2.0/CHANGELOG.md +0 -42
- teluri-0.2.0/tests/test_e2e_wheel.py +0 -97
- teluri-0.2.0/tests/upstream/MANIFEST.json +0 -47
- {teluri-0.2.0 → teluri-1.0.0}/.gitignore +0 -0
- {teluri-0.2.0 → teluri-1.0.0}/src/teluri/__main__.py +0 -0
- {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_params.py +0 -0
- {teluri-0.2.0 → teluri-1.0.0}/src/teluri/py.typed +0 -0
- {teluri-0.2.0 → teluri-1.0.0}/tests/data/examples.json +0 -0
- {teluri-0.2.0 → teluri-1.0.0}/tests/test_build.py +0 -0
- {teluri-0.2.0 → teluri-1.0.0}/tests/test_contract.py +0 -0
- {teluri-0.2.0 → teluri-1.0.0}/tests/test_errors.py +0 -0
- {teluri-0.2.0 → teluri-1.0.0}/tests/test_link.py +0 -0
- {teluri-0.2.0 → teluri-1.0.0}/tests/test_model.py +0 -0
- {teluri-0.2.0 → teluri-1.0.0}/tests/test_parse.py +0 -0
- {teluri-0.2.0 → teluri-1.0.0}/tests/test_rfc_examples.py +0 -0
- {teluri-0.2.0 → teluri-1.0.0}/tests/test_rfc_upstream.py +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [1.0.0] - 2026-11-01
|
|
10
|
+
|
|
11
|
+
### Stable
|
|
12
|
+
|
|
13
|
+
- The seven exported names, their signatures and defaults: `Reason`, `TelURI`,
|
|
14
|
+
`TelURIError`, `build`, `equivalent`, `parse` and `parse_link`.
|
|
15
|
+
- Validated immutable URI fields, canonical serialization, RFC 3966 comparison,
|
|
16
|
+
the explicit link dialect, typed error reasons and command-line modes.
|
|
17
|
+
- Python 3.10 through 3.14; no runtime dependencies; `py.typed` included.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Library errors and CLI diagnostics no longer echo input, parameter names or values.
|
|
22
|
+
CLI usage errors also omit arbitrary arguments. Error reasons remain unchanged;
|
|
23
|
+
caller-created `TelURIError(reason, message)` still preserves its message and args.
|
|
24
|
+
- `parse` requires a boolean `strict`; `parse_link` rejects a non-string context with
|
|
25
|
+
`TypeError` rather than leaking `AttributeError`. `build` checks strings before
|
|
26
|
+
stripping whitespace and rejects false-valued non-mappings in `params`.
|
|
27
|
+
- Wrong-type diagnostics use fixed descriptions rather than user-controlled type names.
|
|
28
|
+
`equivalent` names the offending argument (`a must be a str or TelURI`, or `b ...`)
|
|
29
|
+
instead of reporting `uri must be a str` for either one.
|
|
30
|
+
- CLI output failures caused by a closed pipe return 1 without a traceback.
|
|
31
|
+
- String subclasses are normalized to plain strings before validation and storage,
|
|
32
|
+
including mapping keys and values, so overridden operations cannot bypass validation.
|
|
33
|
+
- CLI stdin decoding keeps its configured encoding and replaces malformed bytes with
|
|
34
|
+
characters that fail URI validation. Invalid lines receive bounded diagnostics and
|
|
35
|
+
valid neighboring lines are processed, even with strict input decoding configured.
|
|
36
|
+
- A parameter value or link number that is not valid UTF-8 raises `TelURIError` without
|
|
37
|
+
the underlying `UnicodeDecodeError` as `__context__`, so the undecodable input no longer
|
|
38
|
+
stays reachable from the error. No library error is chained to another exception.
|
|
39
|
+
- Duplicate parameter detection uses a set rather than scanning an ever-growing list.
|
|
40
|
+
- `TelURI` and `TelURIError` identify their public module as `teluri`; old pickles still
|
|
41
|
+
load, and new pickles also load in 0.2.0. The accidental `annotations` attribute is removed.
|
|
42
|
+
- The release helper checks version availability and a clean working tree, preserves
|
|
43
|
+
any existing `dist` directory and stops if a quality or build check fails.
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- Runnable public docstrings, a guide with explicit grammar policies, strict consumer
|
|
48
|
+
typing checks, seeded properties, refreshed source checks and wheel upgrade tests.
|
|
49
|
+
- Source distributions include the development requirements and check script referenced
|
|
50
|
+
by their contributor instructions.
|
|
51
|
+
- Contracts for long domain contexts, multiple href hashes, reflected equality,
|
|
52
|
+
single-pass link decoding, populated CLI JSON fields and stdin whitespace handling.
|
|
53
|
+
- Contracts for string subclasses that override indexing or hide invalid numbers and
|
|
54
|
+
extensions through iteration, and malformed stdin under `ignore` and
|
|
55
|
+
`surrogateescape` decoding policies while preserving valid neighboring lines.
|
|
56
|
+
- Contracts preserving underlying numbers in strict parsing with overridden partitioning,
|
|
57
|
+
and constructor numbers and extensions with overridden indexing, including canonical URIs.
|
|
58
|
+
- Contracts rejecting invalid false-valued extension subclasses whose iteration hides
|
|
59
|
+
their text, and preserving valid false-valued extensions, subaddresses and contexts.
|
|
60
|
+
- Contracts rejecting invalid false-valued number subclasses and preserving valid ones,
|
|
61
|
+
plus bounded constructor type errors that omit private optional-field input.
|
|
62
|
+
- Contracts pinning the exact constructor type-error messages for `number`, the optional
|
|
63
|
+
text fields and `params`, so no part of a private input representation can be appended.
|
|
64
|
+
- Contracts requiring every syntax reason and every wrong-type error to carry no
|
|
65
|
+
`__cause__` or `__context__` and no input in its formatted traceback, plus a source
|
|
66
|
+
check that no raise chains with `from` or raises a new error inside a handler.
|
|
67
|
+
- The consumer typing check runs inline with no config file or cache, so it writes
|
|
68
|
+
nothing to disk and does not depend on the pytest temporary directory.
|
|
69
|
+
|
|
70
|
+
## [0.2.0]
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- `TelURIError.reason` from the closed set `Reason`; errors survive pickling.
|
|
75
|
+
- Full RFC 3966 grammar: ASCII digits only, domain names per the ABNF, `pvalue` and
|
|
76
|
+
percent-encoding checks, `isub` per erratum 203, `ext` and `isub` mutually exclusive.
|
|
77
|
+
- `parse(..., strict=True)` enforces the section 3 parameter order.
|
|
78
|
+
- `equivalent(a, b)`, and `==` and `hash` on `TelURI`, follow the section 4 rules.
|
|
79
|
+
- `TelURI.isub`, `is_e164`, `unknown_mandatory` and `href`.
|
|
80
|
+
- `parse_link` reads real-world `tel:` links (spaces, encoded numbers, missing context).
|
|
81
|
+
- `build` accepts `isub`, strips spaces from printed numbers and encodes values.
|
|
82
|
+
- `teluri` command line with `--json`, `--strict`, `--link` and `--context`.
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
|
|
86
|
+
- `str(TelURI)` writes the canonical form: section 3 order, lower-case parameter names.
|
|
87
|
+
- `TelURI(number, ext, isub, phone_context, params)`: `is_global` is now derived from the
|
|
88
|
+
number, every field is validated on construction (wrong types raise `TypeError`), and
|
|
89
|
+
`params` is read-only and hashable.
|
|
90
|
+
- `params` values are percent-decoded; `isub` has its own field.
|
|
91
|
+
- `build` percent-encodes `params` values, so callers that passed encoded values must now
|
|
92
|
+
pass plain text.
|
|
93
|
+
- `TelURIError(reason, message)` replaces the single message argument.
|
|
94
|
+
|
|
95
|
+
## [0.1.0]
|
|
96
|
+
|
|
97
|
+
- First release: parse and build tel: URIs per RFC 3966.
|
|
98
|
+
- `parse` reads global numbers (leading `+`) and local numbers with a
|
|
99
|
+
`phone-context`, strips visual separators, and keeps other parameters verbatim.
|
|
100
|
+
- `build` composes a tel: URI from a number, optional `ext`, optional
|
|
101
|
+
`phone-context` and extra parameters, normalizing visual separators.
|
|
102
|
+
- `TelURI` is a frozen, typed result whose `str()` round-trips through `build`.
|
|
103
|
+
- `TelURIError` signals malformed input.
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
# Contributing
|
|
2
|
-
|
|
3
|
-
Thanks for taking the time. This project keeps a small, predictable shape.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for taking the time. This project keeps a small, predictable shape.
|
|
4
|
+
Run the commands below from the repository root or an unpacked source distribution;
|
|
5
|
+
both include `requirements-dev.txt` and `scripts/check.ps1`.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
pip install -r requirements-dev.txt
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Before you open a pull request
|
|
14
|
+
|
|
15
|
+
- Keep public API in `src/teluri/__init__.py`; internals live in `_`-prefixed modules.
|
|
16
|
+
- Add a test for the behaviour you change. Tests inject time, disk and network; they
|
|
17
|
+
never touch the real thing.
|
|
18
|
+
- Run `scripts/check.ps1` (format, lint, types, tests) and make sure it passes.
|
|
19
|
+
- One logical change per commit, imperative subject line.
|
|
20
|
+
|
|
21
|
+
Open an issue first if the change is large or shifts the public API.
|
teluri-1.0.0/GUIDE.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# teluri guide
|
|
2
|
+
|
|
3
|
+
## Stable surface
|
|
4
|
+
|
|
5
|
+
Import only from `teluri`. `__all__` contains `Reason`, `TelURI`, `TelURIError`,
|
|
6
|
+
`build`, `equivalent`, `parse` and `parse_link`. `__version__` is the release version.
|
|
7
|
+
Parameter names, positional arguments, keyword-only arguments and defaults are stable:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from teluri import TelURI, build, equivalent, parse, parse_link
|
|
11
|
+
|
|
12
|
+
uri = TelURI("7042", phone_context="example.com", params={"m-service": "desk"})
|
|
13
|
+
print(uri.unknown_mandatory) # ('m-service',)
|
|
14
|
+
print(build("+1 201", ext="3")) # tel:+1201;ext=3
|
|
15
|
+
print(equivalent(parse("tel:+1-201"), "TEL:+1201")) # True
|
|
16
|
+
print(parse_link("tel:7042", context="example.com")) # tel:7042;phone-context=example.com
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`TelURI(number, ext=None, isub=None, phone_context=None, params=<empty mapping>)`
|
|
20
|
+
validates on construction. It snapshots the mapping, normalizes the number, extension
|
|
21
|
+
and numeric context, and stores generic parameter names in sorted lower case. Its public
|
|
22
|
+
fields are frozen; changing a source mapping later does not change the URI. The mapping
|
|
23
|
+
interface is read-only. Private attributes are outside the immutability contract.
|
|
24
|
+
`dataclasses.replace` validates again. Copying and pickling preserve values.
|
|
25
|
+
|
|
26
|
+
`parse(uri, *, strict=False)` accepts parameters in any order. `strict=True` also
|
|
27
|
+
requires `ext` or `isub` first, then `phone-context`, then generic parameters sorted
|
|
28
|
+
by lower-case name. Duplicate names are always invalid.
|
|
29
|
+
|
|
30
|
+
`build(number, *, ext=None, isub=None, phone_context=None, params=None)` removes
|
|
31
|
+
Unicode whitespace from numbers, extensions and numeric contexts. It percent-encodes
|
|
32
|
+
plain-text generic parameter values as UTF-8. Pass `isub` already percent-encoded.
|
|
33
|
+
Reserved names in `params` are rejected; use the corresponding fields instead.
|
|
34
|
+
|
|
35
|
+
`parse_link(href, *, context=None)` strips surrounding whitespace, decodes the number
|
|
36
|
+
as UTF-8 and removes its Unicode whitespace. A local number without a context uses
|
|
37
|
+
the supplied context. An existing context wins; a global number ignores the default.
|
|
38
|
+
Any supplied default is still validated. Encoded semicolons cannot inject parameters.
|
|
39
|
+
Parameter syntax is the same as `parse`; parameter order remains permissive.
|
|
40
|
+
|
|
41
|
+
`equivalent(a, b)` accepts strings or `TelURI` objects. Invalid strings raise the same
|
|
42
|
+
errors as `parse`. `==` and `hash` use the same identity: separator-free numbers,
|
|
43
|
+
case-insensitive ASCII octets, the same parameter names and values, and context identity.
|
|
44
|
+
A domain's final root dot is ignored. Local and global numbers are never equivalent.
|
|
45
|
+
A local context is an identifier, not a prefix to concatenate with a local number.
|
|
46
|
+
|
|
47
|
+
## Syntax policies and limits
|
|
48
|
+
|
|
49
|
+
The basis is [RFC 3966](https://www.rfc-editor.org/rfc/rfc3966.html), sections 3 to 5,
|
|
50
|
+
with [verified erratum 203](https://www.rfc-editor.org/errata/eid203) for `isub`.
|
|
51
|
+
The following are deliberate library policies, also present in 0.2.0:
|
|
52
|
+
|
|
53
|
+
- Generic parameter values must decode as UTF-8. An `isub` can contain any encoded
|
|
54
|
+
octet and retains its encoded spelling. Empty values are invalid; `None` means a flag.
|
|
55
|
+
- An extension must contain a digit, even though the RFC's ABNF also permits a string
|
|
56
|
+
of separators. Global syntax accepts any positive digit count, including a leading 0.
|
|
57
|
+
- Parameter values follow the ABNF character set, including literal `+`. Comparison
|
|
58
|
+
folds decoded ASCII bytes, including reserved characters represented by percent escapes.
|
|
59
|
+
- Domain contexts follow the RFC's label syntax without DNS resolution or length limits.
|
|
60
|
+
- Registered parameters such as `rn`, `cic`, `tgrp` and `isub-encoding` receive generic
|
|
61
|
+
syntax validation only. Their parameter-specific semantics belong to the caller.
|
|
62
|
+
- `unknown_mandatory` reports every `m-` name in sorted order. The caller decides which
|
|
63
|
+
it understands before using the URI. The parser does not dial or contact anything.
|
|
64
|
+
- `is_e164` is only a shape check: global, 1 to 15 digits, nonzero first digit. It does
|
|
65
|
+
not validate assigned country codes, number lengths by country, or reachability.
|
|
66
|
+
- `href` escapes `#` for URL use, but does not HTML-escape `&` or quotes. Use an HTML
|
|
67
|
+
serializer when putting it into markup. Not every device can dial every valid URI.
|
|
68
|
+
|
|
69
|
+
There is no fixed input length cap. Work is linear in input length plus parameter
|
|
70
|
+
sorting, O(p log p); memory is proportional to input and output size. Applications
|
|
71
|
+
receiving untrusted traffic should choose limits appropriate to their own workload.
|
|
72
|
+
|
|
73
|
+
## Errors
|
|
74
|
+
|
|
75
|
+
Malformed text raises `TelURIError`, a `ValueError`. `.reason` is one of the twelve
|
|
76
|
+
`Literal` strings in `Reason`; see the complete list in the
|
|
77
|
+
[README](https://github.com/JaviLianes8/teluri/blob/main/README.md#reasons).
|
|
78
|
+
`.message` provides a short description; `.args` is `(reason, message)`.
|
|
79
|
+
Library-generated diagnostics never include input. The wording is not a stable parser
|
|
80
|
+
interface: branch on `.reason`. Directly creating an exception preserves a caller's
|
|
81
|
+
message without redacting it. `Reason` is a typing alias, not a runtime validator.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from teluri import TelURIError, parse
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
parse("tel:7042")
|
|
88
|
+
except TelURIError as error:
|
|
89
|
+
print(error.reason) # missing-context
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Wrong argument types raise `TypeError`: URI text, number and optional string fields,
|
|
93
|
+
the boolean `strict`, the default link context, and mapping keys. Invalid generic
|
|
94
|
+
parameter values, including non-strings, raise `TelURIError` with `param-value`.
|
|
95
|
+
`None` is accepted only where shown in the signatures.
|
|
96
|
+
String subclasses are accepted using their underlying text, without invoking overridden
|
|
97
|
+
string operations. Validation and stored fields use plain strings, including parameter
|
|
98
|
+
names and values.
|
|
99
|
+
|
|
100
|
+
## Command line
|
|
101
|
+
|
|
102
|
+
Both `teluri` and `python -m teluri` accept one URI per argument, or one URI per stdin
|
|
103
|
+
line when there are no arguments. Empty lines are skipped. Each valid URI produces a
|
|
104
|
+
canonical line on stdout; each invalid URI produces `invalid (reason)` on stderr.
|
|
105
|
+
`--json` emits one ASCII JSON object per valid URI with `uri`, `number`, `global`,
|
|
106
|
+
`e164`, `ext`, `isub`, `phone_context` and `params`. `--strict`, `--link` and
|
|
107
|
+
`--context` match the API options. `--context` requires `--link`; `--link` and
|
|
108
|
+
`--strict` cannot be combined. Use `--` before URIs beginning with `-`.
|
|
109
|
+
|
|
110
|
+
Exit codes: 0 for success or help, 1 if any URI is invalid or an output pipe closes,
|
|
111
|
+
2 for invalid command-line usage. Inputs are processed independently and in order.
|
|
112
|
+
Diagnostics are ASCII, bounded per input, and omit the supplied data.
|
|
113
|
+
Stdin uses its configured encoding. Malformed byte sequences become replacement
|
|
114
|
+
characters, which fail URI validation on that line; valid neighboring lines still run.
|
|
115
|
+
Only line endings are removed in the default and `--strict` modes. `--link` removes
|
|
116
|
+
surrounding whitespace, but a whitespace-only line is invalid in every mode.
|
|
117
|
+
|
|
118
|
+
## Upgrading from 0.2.0
|
|
119
|
+
|
|
120
|
+
Install with `pip install --upgrade teluri`. The exported signatures, valid URI results,
|
|
121
|
+
and error reasons remain unchanged. Invalid-type handling is stricter, diagnostics no
|
|
122
|
+
longer echo data, and pipe failures exit quietly. Import classes from `teluri`; their
|
|
123
|
+
public module identity now also controls new pickles. Existing 0.2.0 pickles remain
|
|
124
|
+
readable. See every change in the
|
|
125
|
+
[1.0.0 changelog](https://github.com/JaviLianes8/teluri/blob/main/CHANGELOG.md).
|
|
126
|
+
|
|
127
|
+
## Reproducing checks
|
|
128
|
+
|
|
129
|
+
Install `.[dev]`, then run `ruff format --check .`, `ruff check .`,
|
|
130
|
+
`mypy --strict src tests` and `pytest --basetemp=<your-scratch-directory>`.
|
|
131
|
+
Tests use a 300-second per-test timeout. Set `TELURI_UPSTREAM` to a local scratch
|
|
132
|
+
directory and run `python scripts/fetch_upstream.py` to enable fetched-source checks.
|
|
133
|
+
Sources are hash-checked against the manifest and are never distributed.
|
|
134
|
+
Set `TELURI_E2E=1` for wheel builds and clean-environment tests, and `TELURI_PYPI=1`
|
|
135
|
+
to include installing and upgrading the published 0.2.0 release. Set `TEMP` and `TMP`
|
|
136
|
+
to your scratch directory so build and venv tools use it too. The optional reference
|
|
137
|
+
checks use `phonenumbers` and `pypdf`, installed separately, and otherwise skip cleanly.
|
|
138
|
+
`TELURI_OLD_PYTHON` can point to a separate environment containing PyPI 0.2.0 to run
|
|
139
|
+
the seeded compatibility comparison without rebuilding the distributions.
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Javier Lianes
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Javier Lianes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: teluri
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Strict, typed, zero-dependency parser, builder and comparator for tel: URIs (RFC 3966), with a reader for real-world tel: links
|
|
5
5
|
Project-URL: Homepage, https://lianes8studio.com
|
|
6
6
|
Project-URL: Repository, https://github.com/JaviLianes8/teluri
|
|
7
|
+
Project-URL: Source, https://github.com/JaviLianes8/teluri
|
|
7
8
|
Project-URL: Issues, https://github.com/JaviLianes8/teluri/issues
|
|
8
9
|
Project-URL: Changelog, https://github.com/JaviLianes8/teluri/blob/main/CHANGELOG.md
|
|
9
10
|
Project-URL: LinkedIn, https://www.linkedin.com/in/jlianes/
|
|
@@ -12,7 +13,7 @@ Author-email: Javier Lianes <javier@lianes8studio.com>
|
|
|
12
13
|
License-Expression: MIT
|
|
13
14
|
License-File: LICENSE
|
|
14
15
|
Keywords: e164,href,link,parser,phone,phone-number,rfc3966,sip,tel,telephone,uri
|
|
15
|
-
Classifier: Development Status ::
|
|
16
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
16
17
|
Classifier: Intended Audience :: Developers
|
|
17
18
|
Classifier: Operating System :: OS Independent
|
|
18
19
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -30,12 +31,15 @@ Provides-Extra: dev
|
|
|
30
31
|
Requires-Dist: build; extra == 'dev'
|
|
31
32
|
Requires-Dist: mypy; extra == 'dev'
|
|
32
33
|
Requires-Dist: pytest; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-timeout; extra == 'dev'
|
|
33
35
|
Requires-Dist: ruff; extra == 'dev'
|
|
34
36
|
Requires-Dist: twine; extra == 'dev'
|
|
35
37
|
Description-Content-Type: text/markdown
|
|
36
38
|
|
|
37
39
|
# teluri
|
|
38
40
|
|
|
41
|
+
[](https://lianes8studio.com/certified)
|
|
42
|
+
|
|
39
43
|
Strict, typed, zero-dependency parser, builder and comparator for `tel:` URIs
|
|
40
44
|
(RFC 3966), with a reader for the `tel:` links found in real web pages and apps.
|
|
41
45
|
|
|
@@ -65,7 +69,7 @@ print(local.phone_context) # +1914555
|
|
|
65
69
|
print(local.is_global) # False
|
|
66
70
|
```
|
|
67
71
|
|
|
68
|
-
`parse` validates the
|
|
72
|
+
`parse` validates RFC 3966 syntax with the policies in the [guide](https://github.com/JaviLianes8/teluri/blob/main/GUIDE.md) and returns a frozen `TelURI`. Visual
|
|
69
73
|
separators (`-`, `.`, `(`, `)`) are removed from the number, the extension and a numeric
|
|
70
74
|
`phone-context`. Other parameters land in `params`, keyed by lower-case name, with
|
|
71
75
|
percent-decoded values and `None` for a parameter without a value. `str()` gives a
|
|
@@ -147,13 +151,14 @@ its Phone app does not dial a link containing `*` or `#`.
|
|
|
147
151
|
```
|
|
148
152
|
$ teluri "tel:+1-201-555-0123;Ext=9" "tel:863-1234"
|
|
149
153
|
tel:+12015550123;ext=9
|
|
150
|
-
invalid (missing-context)
|
|
154
|
+
invalid (missing-context)
|
|
151
155
|
```
|
|
152
156
|
|
|
153
157
|
One URI per argument, or one per line on standard input. `--json` prints every field,
|
|
154
158
|
`--strict` enforces the parameter order, `--link` reads links as above (not with
|
|
155
159
|
`--strict`) and `--context` gives local links their context. Output is ASCII, so any
|
|
156
160
|
console can print it. The exit status is 1 when any input is invalid, 2 for bad usage.
|
|
161
|
+
Diagnostics contain the reason, never the input. A closed output pipe exits 1 quietly.
|
|
157
162
|
|
|
158
163
|
## API
|
|
159
164
|
|
|
@@ -167,8 +172,8 @@ console can print it. The exit status is 1 when any input is invalid, 2 for bad
|
|
|
167
172
|
| `TelURIError` | `ValueError` with a `reason` from `Reason` |
|
|
168
173
|
| `Reason` | The closed set of reasons, listed below |
|
|
169
174
|
|
|
170
|
-
`is_e164` checks
|
|
171
|
-
|
|
175
|
+
`is_e164` checks only global syntax, at most 15 digits and a nonzero first digit;
|
|
176
|
+
it does not check country allocation or dialability. `unknown_mandatory` lists `m-` parameters; section
|
|
172
177
|
5.4 says not to use a URI whose mandatory parameters you do not understand. `isub` is
|
|
173
178
|
kept in its encoded form because a subaddress may carry any octet.
|
|
174
179
|
|
|
@@ -189,12 +194,13 @@ kept in its encoded form because a subaddress may carry any octet.
|
|
|
189
194
|
|
|
190
195
|
## Why
|
|
191
196
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
Use teluri to preserve URI parameters, validate local contexts and compare URI identity.
|
|
198
|
+
[urllib.parse](https://docs.python.org/3/library/urllib.parse.html) splits URL components;
|
|
199
|
+
it does not validate telephone syntax.
|
|
200
|
+
[phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) parses and formats
|
|
201
|
+
phone numbers, including RFC 3966 forms, and supplies numbering-plan validation.
|
|
202
|
+
Choose it when you need country or number validity checks. teluri keeps arbitrary URI
|
|
203
|
+
parameters and local identifiers without a numbering database or runtime dependencies.
|
|
198
204
|
|
|
199
205
|
## Sources
|
|
200
206
|
|
|
@@ -203,9 +209,13 @@ RFC 3966 and its errata, the IANA tel URI parameter registry, RFC 4694, 4715, 47
|
|
|
203
209
|
`python scripts/fetch_upstream.py` to fetch the texts and enable the tests that compare
|
|
204
210
|
against them.
|
|
205
211
|
|
|
212
|
+
See the [guide and limits](https://github.com/JaviLianes8/teluri/blob/main/GUIDE.md),
|
|
213
|
+
[release changes](https://github.com/JaviLianes8/teluri/blob/main/CHANGELOG.md), and
|
|
214
|
+
[reference manifest](https://github.com/JaviLianes8/teluri/blob/main/tests/upstream/MANIFEST.json).
|
|
215
|
+
|
|
206
216
|
## License
|
|
207
217
|
|
|
208
|
-
MIT. See [LICENSE](LICENSE).
|
|
218
|
+
MIT. See [LICENSE](https://github.com/JaviLianes8/teluri/blob/main/LICENSE).
|
|
209
219
|
|
|
210
220
|
## Links
|
|
211
221
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# teluri
|
|
2
2
|
|
|
3
|
+
[](https://lianes8studio.com/certified)
|
|
4
|
+
|
|
3
5
|
Strict, typed, zero-dependency parser, builder and comparator for `tel:` URIs
|
|
4
6
|
(RFC 3966), with a reader for the `tel:` links found in real web pages and apps.
|
|
5
7
|
|
|
@@ -29,7 +31,7 @@ print(local.phone_context) # +1914555
|
|
|
29
31
|
print(local.is_global) # False
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
`parse` validates the
|
|
34
|
+
`parse` validates RFC 3966 syntax with the policies in the [guide](https://github.com/JaviLianes8/teluri/blob/main/GUIDE.md) and returns a frozen `TelURI`. Visual
|
|
33
35
|
separators (`-`, `.`, `(`, `)`) are removed from the number, the extension and a numeric
|
|
34
36
|
`phone-context`. Other parameters land in `params`, keyed by lower-case name, with
|
|
35
37
|
percent-decoded values and `None` for a parameter without a value. `str()` gives a
|
|
@@ -111,13 +113,14 @@ its Phone app does not dial a link containing `*` or `#`.
|
|
|
111
113
|
```
|
|
112
114
|
$ teluri "tel:+1-201-555-0123;Ext=9" "tel:863-1234"
|
|
113
115
|
tel:+12015550123;ext=9
|
|
114
|
-
invalid (missing-context)
|
|
116
|
+
invalid (missing-context)
|
|
115
117
|
```
|
|
116
118
|
|
|
117
119
|
One URI per argument, or one per line on standard input. `--json` prints every field,
|
|
118
120
|
`--strict` enforces the parameter order, `--link` reads links as above (not with
|
|
119
121
|
`--strict`) and `--context` gives local links their context. Output is ASCII, so any
|
|
120
122
|
console can print it. The exit status is 1 when any input is invalid, 2 for bad usage.
|
|
123
|
+
Diagnostics contain the reason, never the input. A closed output pipe exits 1 quietly.
|
|
121
124
|
|
|
122
125
|
## API
|
|
123
126
|
|
|
@@ -131,8 +134,8 @@ console can print it. The exit status is 1 when any input is invalid, 2 for bad
|
|
|
131
134
|
| `TelURIError` | `ValueError` with a `reason` from `Reason` |
|
|
132
135
|
| `Reason` | The closed set of reasons, listed below |
|
|
133
136
|
|
|
134
|
-
`is_e164` checks
|
|
135
|
-
|
|
137
|
+
`is_e164` checks only global syntax, at most 15 digits and a nonzero first digit;
|
|
138
|
+
it does not check country allocation or dialability. `unknown_mandatory` lists `m-` parameters; section
|
|
136
139
|
5.4 says not to use a URI whose mandatory parameters you do not understand. `isub` is
|
|
137
140
|
kept in its encoded form because a subaddress may carry any octet.
|
|
138
141
|
|
|
@@ -153,12 +156,13 @@ kept in its encoded form because a subaddress may carry any octet.
|
|
|
153
156
|
|
|
154
157
|
## Why
|
|
155
158
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
Use teluri to preserve URI parameters, validate local contexts and compare URI identity.
|
|
160
|
+
[urllib.parse](https://docs.python.org/3/library/urllib.parse.html) splits URL components;
|
|
161
|
+
it does not validate telephone syntax.
|
|
162
|
+
[phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) parses and formats
|
|
163
|
+
phone numbers, including RFC 3966 forms, and supplies numbering-plan validation.
|
|
164
|
+
Choose it when you need country or number validity checks. teluri keeps arbitrary URI
|
|
165
|
+
parameters and local identifiers without a numbering database or runtime dependencies.
|
|
162
166
|
|
|
163
167
|
## Sources
|
|
164
168
|
|
|
@@ -167,9 +171,13 @@ RFC 3966 and its errata, the IANA tel URI parameter registry, RFC 4694, 4715, 47
|
|
|
167
171
|
`python scripts/fetch_upstream.py` to fetch the texts and enable the tests that compare
|
|
168
172
|
against them.
|
|
169
173
|
|
|
174
|
+
See the [guide and limits](https://github.com/JaviLianes8/teluri/blob/main/GUIDE.md),
|
|
175
|
+
[release changes](https://github.com/JaviLianes8/teluri/blob/main/CHANGELOG.md), and
|
|
176
|
+
[reference manifest](https://github.com/JaviLianes8/teluri/blob/main/tests/upstream/MANIFEST.json).
|
|
177
|
+
|
|
170
178
|
## License
|
|
171
179
|
|
|
172
|
-
MIT. See [LICENSE](LICENSE).
|
|
180
|
+
MIT. See [LICENSE](https://github.com/JaviLianes8/teluri/blob/main/LICENSE).
|
|
173
181
|
|
|
174
182
|
## Links
|
|
175
183
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "teluri"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "1.0.0"
|
|
8
8
|
description = "Strict, typed, zero-dependency parser, builder and comparator for tel: URIs (RFC 3966), with a reader for real-world tel: links"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -13,7 +13,7 @@ license-files = ["LICENSE"]
|
|
|
13
13
|
authors = [{ name = "Javier Lianes", email = "javier@lianes8studio.com" }]
|
|
14
14
|
keywords = ["tel", "uri", "rfc3966", "telephone", "phone", "phone-number", "e164", "href", "link", "sip", "parser"]
|
|
15
15
|
classifiers = [
|
|
16
|
-
"Development Status ::
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
17
|
"Intended Audience :: Developers",
|
|
18
18
|
"Operating System :: OS Independent",
|
|
19
19
|
"Programming Language :: Python :: 3",
|
|
@@ -30,11 +30,12 @@ classifiers = [
|
|
|
30
30
|
dependencies = []
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
|
-
dev = ["ruff", "mypy", "pytest", "build", "twine"]
|
|
33
|
+
dev = ["ruff", "mypy", "pytest", "pytest-timeout", "build", "twine"]
|
|
34
34
|
|
|
35
35
|
[project.urls]
|
|
36
36
|
Homepage = "https://lianes8studio.com"
|
|
37
37
|
Repository = "https://github.com/JaviLianes8/teluri"
|
|
38
|
+
Source = "https://github.com/JaviLianes8/teluri"
|
|
38
39
|
Issues = "https://github.com/JaviLianes8/teluri/issues"
|
|
39
40
|
Changelog = "https://github.com/JaviLianes8/teluri/blob/main/CHANGELOG.md"
|
|
40
41
|
LinkedIn = "https://www.linkedin.com/in/jlianes/"
|
|
@@ -51,9 +52,12 @@ include = [
|
|
|
51
52
|
"tests/data/examples.json",
|
|
52
53
|
"tests/upstream/MANIFEST.json",
|
|
53
54
|
"scripts/fetch_upstream.py",
|
|
55
|
+
"scripts/check.ps1",
|
|
56
|
+
"requirements-dev.txt",
|
|
54
57
|
"CHANGELOG.md",
|
|
55
58
|
"CONTRIBUTING.md",
|
|
56
59
|
"README.md",
|
|
60
|
+
"GUIDE.md",
|
|
57
61
|
"LICENSE",
|
|
58
62
|
"pyproject.toml",
|
|
59
63
|
]
|
|
@@ -76,3 +80,4 @@ strict = true
|
|
|
76
80
|
testpaths = ["tests"]
|
|
77
81
|
pythonpath = ["src"]
|
|
78
82
|
addopts = "-q"
|
|
83
|
+
timeout = 300
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Local quality gate. Run before every commit. No CI runs in the cloud.
|
|
2
|
+
# $ErrorActionPreference does NOT stop on a native command's non-zero exit, so each tool's
|
|
3
|
+
# exit code is checked explicitly: a red gate must fail, never pass silently.
|
|
4
|
+
$ErrorActionPreference = "Stop"
|
|
5
|
+
|
|
6
|
+
function Invoke-Step($label, [scriptblock]$cmd) {
|
|
7
|
+
Write-Host "== $label =="
|
|
8
|
+
& $cmd
|
|
9
|
+
if ($LASTEXITCODE -ne 0) { Write-Host "FAILED: $label (exit $LASTEXITCODE)"; exit $LASTEXITCODE }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
Invoke-Step "ruff format" { ruff format --check . }
|
|
13
|
+
Invoke-Step "ruff lint" { ruff check . }
|
|
14
|
+
Invoke-Step "mypy" { mypy --strict src tests }
|
|
15
|
+
Invoke-Step "pytest" { python -m pytest }
|
|
16
|
+
Write-Host "gate green"
|
|
@@ -10,15 +10,18 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import hashlib
|
|
12
12
|
import json
|
|
13
|
+
import os
|
|
13
14
|
import sys
|
|
14
15
|
import urllib.request
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
MANIFEST = Path(__file__).resolve().parents[1] / "tests" / "upstream" / "MANIFEST.json"
|
|
19
|
+
FOLDER = Path(os.environ.get("TELURI_UPSTREAM", str(MANIFEST.parent)))
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
def main() -> int:
|
|
21
|
-
manifest = json.loads(
|
|
23
|
+
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
|
24
|
+
FOLDER.mkdir(parents=True, exist_ok=True)
|
|
22
25
|
failed = 0
|
|
23
26
|
for name, entry in manifest.items():
|
|
24
27
|
with urllib.request.urlopen(entry["url"], timeout=60) as response:
|
|
@@ -2,12 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from typing import get_type_hints as _get_type_hints
|
|
6
|
+
|
|
5
7
|
from ._build import build
|
|
6
8
|
from ._errors import Reason, TelURIError
|
|
7
9
|
from ._link import parse_link
|
|
8
10
|
from ._model import TelURI
|
|
9
11
|
from ._parse import equivalent, parse
|
|
10
12
|
|
|
11
|
-
__version__ = "0.
|
|
13
|
+
__version__ = "1.0.0"
|
|
14
|
+
|
|
15
|
+
TelURI.__annotations__ = _get_type_hints(TelURI)
|
|
16
|
+
TelURI.__module__ = __name__
|
|
17
|
+
TelURIError.__module__ = __name__
|
|
18
|
+
|
|
19
|
+
del annotations
|
|
12
20
|
|
|
13
21
|
__all__ = ["Reason", "TelURI", "TelURIError", "build", "equivalent", "parse", "parse_link"]
|