urlps 0.2.1__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 (42) hide show
  1. urlps-0.2.1/LICENSE +21 -0
  2. urlps-0.2.1/PKG-INFO +280 -0
  3. urlps-0.2.1/README.md +240 -0
  4. urlps-0.2.1/pyproject.toml +117 -0
  5. urlps-0.2.1/setup.cfg +4 -0
  6. urlps-0.2.1/tests/test_builder.py +109 -0
  7. urlps-0.2.1/tests/test_constants_env_overrides.py +51 -0
  8. urlps-0.2.1/tests/test_exceptions.py +135 -0
  9. urlps-0.2.1/tests/test_facade.py +30 -0
  10. urlps-0.2.1/tests/test_ipv6_edgecases_more.py +47 -0
  11. urlps-0.2.1/tests/test_limits.py +90 -0
  12. urlps-0.2.1/tests/test_parser.py +190 -0
  13. urlps-0.2.1/tests/test_relative.py +270 -0
  14. urlps-0.2.1/tests/test_rfc3986_compliance.py +447 -0
  15. urlps-0.2.1/tests/test_rfc3986_extended.py +584 -0
  16. urlps-0.2.1/tests/test_security_fuzzing.py +395 -0
  17. urlps-0.2.1/tests/test_security_ip_formats.py +247 -0
  18. urlps-0.2.1/tests/test_security_phishing.py +296 -0
  19. urlps-0.2.1/tests/test_security_round2.py +647 -0
  20. urlps-0.2.1/tests/test_url.py +273 -0
  21. urlps-0.2.1/tests/test_url_comparison.py +148 -0
  22. urlps-0.2.1/tests/test_validation.py +50 -0
  23. urlps-0.2.1/tests/test_validation_comprehensive.py +451 -0
  24. urlps-0.2.1/tests/test_validation_idna_ipv4.py +40 -0
  25. urlps-0.2.1/tests/test_validation_ipv6_encoding.py +40 -0
  26. urlps-0.2.1/urlps/__init__.py +224 -0
  27. urlps-0.2.1/urlps/_audit.py +99 -0
  28. urlps-0.2.1/urlps/_builder.py +381 -0
  29. urlps-0.2.1/urlps/_components.py +90 -0
  30. urlps-0.2.1/urlps/_parser.py +365 -0
  31. urlps-0.2.1/urlps/_patterns.py +39 -0
  32. urlps-0.2.1/urlps/_relative.py +54 -0
  33. urlps-0.2.1/urlps/_security.py +378 -0
  34. urlps-0.2.1/urlps/_validation.py +325 -0
  35. urlps-0.2.1/urlps/constants.py +133 -0
  36. urlps-0.2.1/urlps/exceptions.py +174 -0
  37. urlps-0.2.1/urlps/url.py +416 -0
  38. urlps-0.2.1/urlps.egg-info/PKG-INFO +280 -0
  39. urlps-0.2.1/urlps.egg-info/SOURCES.txt +40 -0
  40. urlps-0.2.1/urlps.egg-info/dependency_links.txt +1 -0
  41. urlps-0.2.1/urlps.egg-info/requires.txt +9 -0
  42. urlps-0.2.1/urlps.egg-info/top_level.txt +1 -0
urlps-0.2.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 urlp maintainers
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.
urlps-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,280 @@
1
+ Metadata-Version: 2.4
2
+ Name: urlps
3
+ Version: 0.2.1
4
+ Summary: Lightweight URL parsing and building helpers (RFC 3986-like).
5
+ Author: urlps maintainers
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/micro/urlps
8
+ Project-URL: Repository, https://github.com/micro/urlps
9
+ Project-URL: Documentation, https://github.com/micro/urlps#readme
10
+ Project-URL: Changelog, https://github.com/micro/urlps/blob/main/CHANGELOG.md
11
+ Project-URL: Issues, https://github.com/micro/urlps/issues
12
+ Project-URL: Bug Tracker, https://github.com/micro/urlps/issues
13
+ Keywords: url,parser,builder,rfc3986,validation,idna,ipv6,punycode,ipv4
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Natural Language :: English
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: Implementation :: CPython
25
+ Classifier: Topic :: Internet :: WWW/HTTP
26
+ Classifier: Topic :: Software Development :: Libraries
27
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.10
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Provides-Extra: idna
33
+ Requires-Dist: idna>=3.11; extra == "idna"
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=7.4; extra == "dev"
36
+ Requires-Dist: mypy>=1.19; extra == "dev"
37
+ Requires-Dist: bandit>=1.9; extra == "dev"
38
+ Requires-Dist: idna>=3.11; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # urlps
42
+
43
+ Lightweight, secure URL parsing and building library with RFC 3986 compliance. Features comprehensive security protections including SSRF prevention, DNS rebinding detection, path traversal protection, and homograph attack detection.
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install urlps
49
+ ```
50
+
51
+ Development setup:
52
+ ```bash
53
+ python -m venv .venv
54
+ . .venv/Scripts/activate # Windows: .venv\Scripts\activate
55
+ pip install -e ".[dev]"
56
+ ```
57
+
58
+ ## Quick Start
59
+
60
+ ```python
61
+ from urlps import parse_url, build
62
+
63
+ # Secure by default - blocks SSRF, private IPs, localhost
64
+ url = parse_url("https://api.example.com/data?token=abc#section")
65
+ print(url.host) # api.example.com
66
+ print(url.query_params) # [("token", "abc")]
67
+
68
+ # Build URLs
69
+ url_str = build("https", "example.com", port=8443, path="/api", query="x=1")
70
+ # https://example.com:8443/api?x=1
71
+
72
+ # Immutable with functional updates
73
+ url = parse_url("https://example.com/path")
74
+ new_url = url.with_host("other.com").with_port(8080)
75
+ print(new_url) # https://other.com:8080/path
76
+ ```
77
+
78
+ ### Security
79
+
80
+ `parse_url()` blocks by default:
81
+ - Private IPs (192.168.x.x, 10.x.x.x, 172.16.x.x)
82
+ - Localhost and loopback addresses
83
+ - Link-local addresses (169.254.x.x)
84
+ - `.local` and `.internal` domains
85
+ - Path traversal patterns (`../`)
86
+ - Double-encoded characters
87
+ - Mixed Unicode scripts (homograph attacks)
88
+
89
+ Use `parse_url_unsafe()` for internal/development URLs:
90
+ ```python
91
+ from urlps import parse_url_unsafe
92
+
93
+ dev_url = parse_url_unsafe("http://localhost:3000/api")
94
+ internal = parse_url_unsafe("http://192.168.1.100/metrics")
95
+ ```
96
+
97
+ ## Core Features
98
+
99
+ ### Immutable URL Objects
100
+
101
+ ```python
102
+ url = parse_url("https://user:pass@example.com:8080/path?token=abc")
103
+ print(url.netloc) # user:pass@example.com:8080
104
+ print(url.effective_port) # 8080
105
+
106
+ # with_* methods return new URL objects
107
+ url2 = url.with_netloc("admin@example.com")
108
+ url3 = url.with_host("other.com").with_port(443).with_path("/api")
109
+ url4 = url.with_query_param("new", "value")
110
+ url5 = url.without_query_param("token")
111
+ ```
112
+
113
+ ### Security Checks
114
+
115
+ ```python
116
+ from urlps import parse_url, InvalidURLError
117
+
118
+ # SSRF protection (enabled by default)
119
+ try:
120
+ parse_url("http://localhost/admin") # Blocked
121
+ except InvalidURLError as e:
122
+ print(f"Rejected: {e}")
123
+
124
+ # DNS rebinding detection (optional)
125
+ url = parse_url("https://api.example.com/", check_dns=True)
126
+
127
+ # URL canonicalization
128
+ url = parse_url("HTTP://EXAMPLE.COM:80/path?z=1&a=2")
129
+ canonical = url.canonicalize()
130
+ print(canonical.scheme) # "http"
131
+ print(canonical.host) # "example.com"
132
+ print(canonical.port) # None (default port removed)
133
+ print(canonical.query) # "a=2&z=1" (sorted)
134
+
135
+ # Password masking
136
+ url = parse_url("https://admin:secret123@api.example.com/")
137
+ print(url.as_string(mask_password=True)) # https://admin:***@api.example.com/
138
+ ```
139
+
140
+ ### Audit Logging
141
+
142
+ ```python
143
+ from urlps import set_audit_callback
144
+ import logging
145
+
146
+ def audit_url_parsing(raw_url, parsed_url, exception):
147
+ if exception:
148
+ logging.warning(f"Failed to parse URL: {exception}")
149
+ else:
150
+ logging.info(f"Parsed URL to host: {parsed_url.host}")
151
+
152
+ set_audit_callback(audit_url_parsing)
153
+ ```
154
+
155
+ ### Component Length Limits
156
+
157
+ Conservative limits to prevent DoS attacks:
158
+
159
+ | Component | Max Length |
160
+ |-----------|------------|
161
+ | URL (total) | 32 KB |
162
+ | Scheme | 16 chars |
163
+ | Host | 253 chars |
164
+ | Path | 4 KB |
165
+ | Query | 8 KB |
166
+ | Fragment | 1 KB |
167
+ | Userinfo | 128 chars |
168
+
169
+ ## Environment Variables
170
+
171
+ Override length limits via environment variables:
172
+
173
+ ```bash
174
+ # PowerShell
175
+ $env:URLPS_MAX_URL_LENGTH = "65536"
176
+ python -c "import urlps.constants as c; print(c.MAX_URL_LENGTH)"
177
+
178
+ # Bash
179
+ export URLPS_MAX_URL_LENGTH=65536
180
+ python -c 'import urlps.constants as c; print(c.MAX_URL_LENGTH)'
181
+ ```
182
+
183
+ Supported variables:
184
+ - `URLPS_MAX_URL_LENGTH`
185
+ - `URLPS_MAX_SCHEME_LENGTH`
186
+ - `URLPS_MAX_HOST_LENGTH`
187
+ - `URLPS_MAX_PATH_LENGTH`
188
+ - `URLPS_MAX_QUERY_LENGTH`
189
+ - `URLPS_MAX_FRAGMENT_LENGTH`
190
+ - `URLPS_MAX_USERINFO_LENGTH`
191
+ - `URLPS_MAX_IPV6_STRING_LENGTH`
192
+
193
+ ## API Reference
194
+
195
+ ### Main Functions
196
+
197
+ | Function | Description |
198
+ | --- | --- |
199
+ | `parse_url(url, *, allow_custom_scheme=False, check_dns=False)` | Parse URL with security checks enabled (recommended) |
200
+ | `parse_url_unsafe(url, *, allow_custom_scheme=False, strict=False)` | Parse URL without security checks (trusted input only) |
201
+ | `build(*scheme_and_host, port=None, path="/", query=None, fragment=None, userinfo=None)` | Build URL string from components |
202
+ | `compose_url(components)` | Build URL from components dict |
203
+
204
+ ### URL Methods
205
+
206
+ | Method | Description |
207
+ | --- | --- |
208
+ | `url.as_string(mask_password=False)` | Convert to string, optionally masking password |
209
+ | `url.canonicalize()` | Return canonicalized copy |
210
+ | `url.is_semantically_equal(other)` | Compare URLs by meaning after canonicalization |
211
+ | `url.same_origin(other)` | Check if URLs have same origin |
212
+ | `url.origin` | Return origin string (e.g., `https://example.com`) |
213
+ | `url.copy(**overrides)` | Create copy with optional component overrides |
214
+ | `url.with_*()` | Functional updates: `with_scheme`, `with_host`, `with_port`, `with_path`, `with_fragment`, `with_userinfo`, `with_netloc`, `with_query_param`, `without_query_param` |
215
+
216
+ ### Cache Management
217
+
218
+ ```python
219
+ from urlps import get_cache_info, clear_all_caches
220
+
221
+ # Get cache statistics
222
+ stats = get_cache_info()
223
+ print(stats['parser']['normalize_path']['hits'])
224
+
225
+ # Clear all caches (useful for long-running apps)
226
+ previous = clear_all_caches()
227
+ ```
228
+
229
+ ## Comparison with urllib.parse
230
+
231
+ | Feature | urllib.parse | urlps |
232
+ | --- | --- | --- |
233
+ | Basic URL parsing | ✓ | ✓ |
234
+ | RFC 3986 strict compliance | Partial | ✓ |
235
+ | SSRF protection | ✗ | ✓ |
236
+ | DNS rebinding detection | ✗ | ✓ |
237
+ | Path traversal detection | ✗ | ✓ |
238
+ | Homograph detection | ✗ | ✓ |
239
+ | Immutable URL objects | ✗ | ✓ |
240
+ | URL canonicalization | ✗ | ✓ |
241
+ | Password masking | ✗ | ✓ |
242
+ | Audit logging | ✗ | ✓ |
243
+ | Component length limits | ✗ | ✓ |
244
+
245
+ **Use urllib.parse when:** You need zero dependencies and basic parsing is sufficient.
246
+
247
+ **Use urlps when:** Security matters, you need RFC 3986 strict compliance, or you want immutable URL objects with ergonomic manipulation methods.
248
+
249
+ ## Exceptions
250
+
251
+ ```python
252
+ from urlps import InvalidURLError, HostValidationError, parse_url
253
+
254
+ try:
255
+ url = parse_url(user_input)
256
+ except HostValidationError:
257
+ print("Invalid hostname")
258
+ except InvalidURLError:
259
+ print("Invalid URL")
260
+ ```
261
+
262
+ Exception hierarchy:
263
+ - `InvalidURLError` — Base exception for all URL errors
264
+ - `URLParseError` — Parsing errors
265
+ - `URLBuildError` — Building errors
266
+ - `HostValidationError` / `PortValidationError` — Component validation errors
267
+ - `QueryParsingError`, `FragmentEncodingError`, `UserInfoParsingError`, `UnsupportedSchemeError` — Specific errors
268
+
269
+ ## Running Tests
270
+
271
+ ```bash
272
+ pytest
273
+ pytest -v -k "test_parse" # Run specific tests
274
+ pytest -m ipv6 # Run IPv6 tests
275
+ pytest -m idna # Run IDNA tests
276
+ ```
277
+
278
+ ## License
279
+
280
+ MIT
urlps-0.2.1/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # urlps
2
+
3
+ Lightweight, secure URL parsing and building library with RFC 3986 compliance. Features comprehensive security protections including SSRF prevention, DNS rebinding detection, path traversal protection, and homograph attack detection.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install urlps
9
+ ```
10
+
11
+ Development setup:
12
+ ```bash
13
+ python -m venv .venv
14
+ . .venv/Scripts/activate # Windows: .venv\Scripts\activate
15
+ pip install -e ".[dev]"
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```python
21
+ from urlps import parse_url, build
22
+
23
+ # Secure by default - blocks SSRF, private IPs, localhost
24
+ url = parse_url("https://api.example.com/data?token=abc#section")
25
+ print(url.host) # api.example.com
26
+ print(url.query_params) # [("token", "abc")]
27
+
28
+ # Build URLs
29
+ url_str = build("https", "example.com", port=8443, path="/api", query="x=1")
30
+ # https://example.com:8443/api?x=1
31
+
32
+ # Immutable with functional updates
33
+ url = parse_url("https://example.com/path")
34
+ new_url = url.with_host("other.com").with_port(8080)
35
+ print(new_url) # https://other.com:8080/path
36
+ ```
37
+
38
+ ### Security
39
+
40
+ `parse_url()` blocks by default:
41
+ - Private IPs (192.168.x.x, 10.x.x.x, 172.16.x.x)
42
+ - Localhost and loopback addresses
43
+ - Link-local addresses (169.254.x.x)
44
+ - `.local` and `.internal` domains
45
+ - Path traversal patterns (`../`)
46
+ - Double-encoded characters
47
+ - Mixed Unicode scripts (homograph attacks)
48
+
49
+ Use `parse_url_unsafe()` for internal/development URLs:
50
+ ```python
51
+ from urlps import parse_url_unsafe
52
+
53
+ dev_url = parse_url_unsafe("http://localhost:3000/api")
54
+ internal = parse_url_unsafe("http://192.168.1.100/metrics")
55
+ ```
56
+
57
+ ## Core Features
58
+
59
+ ### Immutable URL Objects
60
+
61
+ ```python
62
+ url = parse_url("https://user:pass@example.com:8080/path?token=abc")
63
+ print(url.netloc) # user:pass@example.com:8080
64
+ print(url.effective_port) # 8080
65
+
66
+ # with_* methods return new URL objects
67
+ url2 = url.with_netloc("admin@example.com")
68
+ url3 = url.with_host("other.com").with_port(443).with_path("/api")
69
+ url4 = url.with_query_param("new", "value")
70
+ url5 = url.without_query_param("token")
71
+ ```
72
+
73
+ ### Security Checks
74
+
75
+ ```python
76
+ from urlps import parse_url, InvalidURLError
77
+
78
+ # SSRF protection (enabled by default)
79
+ try:
80
+ parse_url("http://localhost/admin") # Blocked
81
+ except InvalidURLError as e:
82
+ print(f"Rejected: {e}")
83
+
84
+ # DNS rebinding detection (optional)
85
+ url = parse_url("https://api.example.com/", check_dns=True)
86
+
87
+ # URL canonicalization
88
+ url = parse_url("HTTP://EXAMPLE.COM:80/path?z=1&a=2")
89
+ canonical = url.canonicalize()
90
+ print(canonical.scheme) # "http"
91
+ print(canonical.host) # "example.com"
92
+ print(canonical.port) # None (default port removed)
93
+ print(canonical.query) # "a=2&z=1" (sorted)
94
+
95
+ # Password masking
96
+ url = parse_url("https://admin:secret123@api.example.com/")
97
+ print(url.as_string(mask_password=True)) # https://admin:***@api.example.com/
98
+ ```
99
+
100
+ ### Audit Logging
101
+
102
+ ```python
103
+ from urlps import set_audit_callback
104
+ import logging
105
+
106
+ def audit_url_parsing(raw_url, parsed_url, exception):
107
+ if exception:
108
+ logging.warning(f"Failed to parse URL: {exception}")
109
+ else:
110
+ logging.info(f"Parsed URL to host: {parsed_url.host}")
111
+
112
+ set_audit_callback(audit_url_parsing)
113
+ ```
114
+
115
+ ### Component Length Limits
116
+
117
+ Conservative limits to prevent DoS attacks:
118
+
119
+ | Component | Max Length |
120
+ |-----------|------------|
121
+ | URL (total) | 32 KB |
122
+ | Scheme | 16 chars |
123
+ | Host | 253 chars |
124
+ | Path | 4 KB |
125
+ | Query | 8 KB |
126
+ | Fragment | 1 KB |
127
+ | Userinfo | 128 chars |
128
+
129
+ ## Environment Variables
130
+
131
+ Override length limits via environment variables:
132
+
133
+ ```bash
134
+ # PowerShell
135
+ $env:URLPS_MAX_URL_LENGTH = "65536"
136
+ python -c "import urlps.constants as c; print(c.MAX_URL_LENGTH)"
137
+
138
+ # Bash
139
+ export URLPS_MAX_URL_LENGTH=65536
140
+ python -c 'import urlps.constants as c; print(c.MAX_URL_LENGTH)'
141
+ ```
142
+
143
+ Supported variables:
144
+ - `URLPS_MAX_URL_LENGTH`
145
+ - `URLPS_MAX_SCHEME_LENGTH`
146
+ - `URLPS_MAX_HOST_LENGTH`
147
+ - `URLPS_MAX_PATH_LENGTH`
148
+ - `URLPS_MAX_QUERY_LENGTH`
149
+ - `URLPS_MAX_FRAGMENT_LENGTH`
150
+ - `URLPS_MAX_USERINFO_LENGTH`
151
+ - `URLPS_MAX_IPV6_STRING_LENGTH`
152
+
153
+ ## API Reference
154
+
155
+ ### Main Functions
156
+
157
+ | Function | Description |
158
+ | --- | --- |
159
+ | `parse_url(url, *, allow_custom_scheme=False, check_dns=False)` | Parse URL with security checks enabled (recommended) |
160
+ | `parse_url_unsafe(url, *, allow_custom_scheme=False, strict=False)` | Parse URL without security checks (trusted input only) |
161
+ | `build(*scheme_and_host, port=None, path="/", query=None, fragment=None, userinfo=None)` | Build URL string from components |
162
+ | `compose_url(components)` | Build URL from components dict |
163
+
164
+ ### URL Methods
165
+
166
+ | Method | Description |
167
+ | --- | --- |
168
+ | `url.as_string(mask_password=False)` | Convert to string, optionally masking password |
169
+ | `url.canonicalize()` | Return canonicalized copy |
170
+ | `url.is_semantically_equal(other)` | Compare URLs by meaning after canonicalization |
171
+ | `url.same_origin(other)` | Check if URLs have same origin |
172
+ | `url.origin` | Return origin string (e.g., `https://example.com`) |
173
+ | `url.copy(**overrides)` | Create copy with optional component overrides |
174
+ | `url.with_*()` | Functional updates: `with_scheme`, `with_host`, `with_port`, `with_path`, `with_fragment`, `with_userinfo`, `with_netloc`, `with_query_param`, `without_query_param` |
175
+
176
+ ### Cache Management
177
+
178
+ ```python
179
+ from urlps import get_cache_info, clear_all_caches
180
+
181
+ # Get cache statistics
182
+ stats = get_cache_info()
183
+ print(stats['parser']['normalize_path']['hits'])
184
+
185
+ # Clear all caches (useful for long-running apps)
186
+ previous = clear_all_caches()
187
+ ```
188
+
189
+ ## Comparison with urllib.parse
190
+
191
+ | Feature | urllib.parse | urlps |
192
+ | --- | --- | --- |
193
+ | Basic URL parsing | ✓ | ✓ |
194
+ | RFC 3986 strict compliance | Partial | ✓ |
195
+ | SSRF protection | ✗ | ✓ |
196
+ | DNS rebinding detection | ✗ | ✓ |
197
+ | Path traversal detection | ✗ | ✓ |
198
+ | Homograph detection | ✗ | ✓ |
199
+ | Immutable URL objects | ✗ | ✓ |
200
+ | URL canonicalization | ✗ | ✓ |
201
+ | Password masking | ✗ | ✓ |
202
+ | Audit logging | ✗ | ✓ |
203
+ | Component length limits | ✗ | ✓ |
204
+
205
+ **Use urllib.parse when:** You need zero dependencies and basic parsing is sufficient.
206
+
207
+ **Use urlps when:** Security matters, you need RFC 3986 strict compliance, or you want immutable URL objects with ergonomic manipulation methods.
208
+
209
+ ## Exceptions
210
+
211
+ ```python
212
+ from urlps import InvalidURLError, HostValidationError, parse_url
213
+
214
+ try:
215
+ url = parse_url(user_input)
216
+ except HostValidationError:
217
+ print("Invalid hostname")
218
+ except InvalidURLError:
219
+ print("Invalid URL")
220
+ ```
221
+
222
+ Exception hierarchy:
223
+ - `InvalidURLError` — Base exception for all URL errors
224
+ - `URLParseError` — Parsing errors
225
+ - `URLBuildError` — Building errors
226
+ - `HostValidationError` / `PortValidationError` — Component validation errors
227
+ - `QueryParsingError`, `FragmentEncodingError`, `UserInfoParsingError`, `UnsupportedSchemeError` — Specific errors
228
+
229
+ ## Running Tests
230
+
231
+ ```bash
232
+ pytest
233
+ pytest -v -k "test_parse" # Run specific tests
234
+ pytest -m ipv6 # Run IPv6 tests
235
+ pytest -m idna # Run IDNA tests
236
+ ```
237
+
238
+ ## License
239
+
240
+ MIT
@@ -0,0 +1,117 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "urlps"
7
+ version = "0.2.1"
8
+ description = "Lightweight URL parsing and building helpers (RFC 3986-like)."
9
+ readme = { file = "README.md", content-type = "text/markdown" }
10
+ requires-python = ">=3.10"
11
+ authors = [
12
+ { name = "urlps maintainers" },
13
+ ]
14
+ license = "MIT"
15
+ license-files = ["LICENSE"]
16
+ keywords = ["url", "parser", "builder", "rfc3986", "validation", "idna", "ipv6", "punycode", "ipv4"]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "Natural Language :: English",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: Implementation :: CPython",
29
+ "Topic :: Internet :: WWW/HTTP",
30
+ "Topic :: Software Development :: Libraries",
31
+ "Topic :: Software Development :: Libraries :: Python Modules",
32
+ "Typing :: Typed",
33
+ ]
34
+ dependencies = []
35
+
36
+ [project.optional-dependencies]
37
+ idna = ["idna>=3.11"]
38
+ dev = ["pytest>=7.4", "mypy>=1.19", "bandit>=1.9", "idna>=3.11"]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/micro/urlps"
42
+ Repository = "https://github.com/micro/urlps"
43
+ Documentation = "https://github.com/micro/urlps#readme"
44
+ Changelog = "https://github.com/micro/urlps/blob/main/CHANGELOG.md"
45
+ Issues = "https://github.com/micro/urlps/issues"
46
+ "Bug Tracker" = "https://github.com/micro/urlps/issues"
47
+
48
+ [tool.setuptools.packages.find]
49
+ include = ["urlps*"]
50
+ exclude = ["tests*"]
51
+
52
+ [tool.black]
53
+ line-length = 120
54
+ target-version = ["py310", "py311", "py312"]
55
+ include = '\.pyi?$'
56
+ extend-exclude = '''
57
+ /(
58
+ # directories
59
+ \.eggs
60
+ | \.git
61
+ | \.hg
62
+ | \.mypy_cache
63
+ | \.tox
64
+ | \.venv
65
+ | build
66
+ | dist
67
+ )/
68
+ '''
69
+
70
+ [tool.isort]
71
+ profile = "black"
72
+ line_length = 120
73
+ multi_line_mode = 3
74
+ include_trailing_comma = true
75
+ force_grid_wrap = 0
76
+ use_parentheses = true
77
+ ensure_newline_before_comments = true
78
+ skip_glob = [".git", "__pycache__", ".venv"]
79
+
80
+ [tool.mypy]
81
+ python_version = "3.9"
82
+ warn_return_any = true
83
+ warn_unused_configs = true
84
+ disallow_untyped_defs = false
85
+ disallow_untyped_calls = false
86
+ warn_unused_ignores = true
87
+ warn_redundant_casts = true
88
+ warn_no_return = true
89
+
90
+ [tool.pytest.ini_options]
91
+ testpaths = ["tests"]
92
+ python_files = ["test_*.py"]
93
+ python_classes = ["Test*"]
94
+ python_functions = ["test_*"]
95
+ addopts = ["-v", "--strict-markers", "--tb=short"]
96
+ markers = [
97
+ "slow: marks tests as slow",
98
+ "integration: marks tests as integration tests",
99
+ "ipv6: marks IPv6-related tests",
100
+ "idna: marks IDNA-related tests",
101
+ "rfc3986: marks RFC 3986 compliance tests",
102
+ ]
103
+
104
+ [tool.coverage.run]
105
+ source = ["urlps"]
106
+ branch = true
107
+
108
+ [tool.coverage.report]
109
+ exclude_lines = [
110
+ "pragma: no cover",
111
+ "def __repr__",
112
+ "raise AssertionError",
113
+ "raise NotImplementedError",
114
+ "if __name__ == .__main__.:",
115
+ "if TYPE_CHECKING:",
116
+ ]
117
+
urlps-0.2.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+