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.
Files changed (53) hide show
  1. teluri-1.0.0/CHANGELOG.md +103 -0
  2. {teluri-0.2.0 → teluri-1.0.0}/CONTRIBUTING.md +21 -19
  3. teluri-1.0.0/GUIDE.md +139 -0
  4. {teluri-0.2.0 → teluri-1.0.0}/LICENSE +21 -21
  5. {teluri-0.2.0 → teluri-1.0.0}/PKG-INFO +23 -13
  6. {teluri-0.2.0 → teluri-1.0.0}/README.md +19 -11
  7. {teluri-0.2.0 → teluri-1.0.0}/pyproject.toml +8 -3
  8. teluri-1.0.0/requirements-dev.txt +3 -0
  9. teluri-1.0.0/scripts/check.ps1 +16 -0
  10. {teluri-0.2.0 → teluri-1.0.0}/scripts/fetch_upstream.py +5 -2
  11. {teluri-0.2.0 → teluri-1.0.0}/src/teluri/__init__.py +9 -1
  12. {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_build.py +8 -2
  13. {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_chars.py +11 -1
  14. {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_cli.py +43 -13
  15. {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_errors.py +13 -0
  16. {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_grammar.py +12 -13
  17. {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_link.py +15 -9
  18. {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_model.py +40 -11
  19. {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_parse.py +24 -6
  20. {teluri-0.2.0 → teluri-1.0.0}/tests/conftest.py +5 -2
  21. {teluri-0.2.0 → teluri-1.0.0}/tests/test_cli.py +2 -2
  22. teluri-1.0.0/tests/test_cli_contracts.py +97 -0
  23. teluri-1.0.0/tests/test_compatibility.py +48 -0
  24. teluri-1.0.0/tests/test_contract_api.py +116 -0
  25. teluri-1.0.0/tests/test_contract_edges.py +61 -0
  26. {teluri-0.2.0 → teluri-1.0.0}/tests/test_docs.py +2 -2
  27. teluri-1.0.0/tests/test_e2e_wheel.py +186 -0
  28. teluri-1.0.0/tests/test_error_chaining.py +125 -0
  29. teluri-1.0.0/tests/test_properties.py +75 -0
  30. {teluri-0.2.0 → teluri-1.0.0}/tests/test_review.py +11 -5
  31. teluri-1.0.0/tests/test_review_contracts.py +86 -0
  32. teluri-1.0.0/tests/test_safety.py +155 -0
  33. teluri-1.0.0/tests/test_spec.py +127 -0
  34. teluri-1.0.0/tests/test_string_inputs.py +189 -0
  35. {teluri-0.2.0 → teluri-1.0.0}/tests/test_typing.py +14 -15
  36. teluri-1.0.0/tests/test_upgrade.py +64 -0
  37. teluri-1.0.0/tests/upstream/MANIFEST.json +110 -0
  38. teluri-0.2.0/CHANGELOG.md +0 -42
  39. teluri-0.2.0/tests/test_e2e_wheel.py +0 -97
  40. teluri-0.2.0/tests/upstream/MANIFEST.json +0 -47
  41. {teluri-0.2.0 → teluri-1.0.0}/.gitignore +0 -0
  42. {teluri-0.2.0 → teluri-1.0.0}/src/teluri/__main__.py +0 -0
  43. {teluri-0.2.0 → teluri-1.0.0}/src/teluri/_params.py +0 -0
  44. {teluri-0.2.0 → teluri-1.0.0}/src/teluri/py.typed +0 -0
  45. {teluri-0.2.0 → teluri-1.0.0}/tests/data/examples.json +0 -0
  46. {teluri-0.2.0 → teluri-1.0.0}/tests/test_build.py +0 -0
  47. {teluri-0.2.0 → teluri-1.0.0}/tests/test_contract.py +0 -0
  48. {teluri-0.2.0 → teluri-1.0.0}/tests/test_errors.py +0 -0
  49. {teluri-0.2.0 → teluri-1.0.0}/tests/test_link.py +0 -0
  50. {teluri-0.2.0 → teluri-1.0.0}/tests/test_model.py +0 -0
  51. {teluri-0.2.0 → teluri-1.0.0}/tests/test_parse.py +0 -0
  52. {teluri-0.2.0 → teluri-1.0.0}/tests/test_rfc_examples.py +0 -0
  53. {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
- ## Setup
6
-
7
- ```
8
- pip install -r requirements-dev.txt
9
- ```
10
-
11
- ## Before you open a pull request
12
-
13
- - Keep public API in `src/teluri/__init__.py`; internals live in `_`-prefixed modules.
14
- - Add a test for the behaviour you change. Tests inject time, disk and network; they
15
- never touch the real thing.
16
- - Run `scripts/check.ps1` (format, lint, types, tests) and make sure it passes.
17
- - One logical change per commit, imperative subject line.
18
-
19
- Open an issue first if the change is large or shifts the public API.
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.2.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 :: 4 - Beta
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
+ [![Lianes8 Certified](https://lianes8studio.com/certified/lianes8-certified-badge.svg)](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 whole RFC 3966 grammar and returns a frozen `TelURI`. Visual
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): tel:863-1234
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 the E.164 limits only: a global number of at most 15 digits whose
171
- country code does not start with 0. `unknown_mandatory` lists `m-` parameters; section
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
- RFC 3966 `tel:` URIs are their own grammar: global numbers with a leading `+`, local
193
- numbers that must carry a `phone-context`, visual separators, `ext` and `isub`, and a
194
- comparison rule that ignores separators, case and parameter order. The standard library
195
- does not model any of it, general URI libraries stop at RFC 3986, and `phonenumbers` is a
196
- number-validation database rather than a URI codec. teluri is a small, strict, typed one,
197
- with the browser and phone dialect handled explicitly instead of by accident.
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
+ [![Lianes8 Certified](https://lianes8studio.com/certified/lianes8-certified-badge.svg)](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 whole RFC 3966 grammar and returns a frozen `TelURI`. Visual
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): tel:863-1234
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 the E.164 limits only: a global number of at most 15 digits whose
135
- country code does not start with 0. `unknown_mandatory` lists `m-` parameters; section
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
- RFC 3966 `tel:` URIs are their own grammar: global numbers with a leading `+`, local
157
- numbers that must carry a `phone-context`, visual separators, `ext` and `isub`, and a
158
- comparison rule that ignores separators, case and parameter order. The standard library
159
- does not model any of it, general URI libraries stop at RFC 3986, and `phonenumbers` is a
160
- number-validation database rather than a URI codec. teluri is a small, strict, typed one,
161
- with the browser and phone dialect handled explicitly instead of by accident.
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.2.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 :: 4 - Beta",
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,3 @@
1
+ # Development dependencies: the package in editable mode plus its dev extras
2
+ # (ruff, mypy, pytest, build, twine). Install with: pip install -r requirements-dev.txt
3
+ -e .[dev]
@@ -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
- FOLDER = Path(__file__).resolve().parents[1] / "tests" / "upstream"
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((FOLDER / "MANIFEST.json").read_text(encoding="utf-8"))
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.2.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"]