Action0-Url
Easy URL parsing, manipulation and unparsing wrapper around urllib.parse's methods.
Requires Python 3.11 or newer.
Full documentation including the API reference: https://laughinjar.github.io/action0-url/
Installation
pip install action0-url # or: uv add action0-url
Usage
Either with an existing url:
from action0.url import Url
url = Url("https://www.example.com/path/filename.json?foo=bar")
url.query.set("foo", "baz")
print(url.as_str())
# https://www.example.com/path/filename.json?foo=baz
url.query.add("a", "b")
print(url.as_str())
# https://www.example.com/path/filename.json?foo=baz&a=b
url.query.add("foo", "123")
print(url.as_str())
# https://www.example.com/path/filename.json?foo=baz&a=b&foo=123
url.hostname = "action0.com"
url.port = 8443
url.path = "/public/index.html"
url.username = "user"
url.password = "pass"
url.fragment = "fragment"
# instead of url.as_str() you can also use str(url) or just print(url)
print(url)
Or construct one (and manipulate):
from action0.url import Url
url = Url(
scheme="https",
hostname="example.com",
path="index.html",
query={"a": "b", "foo": ["bar", "baz"]},
port=1234
)
print(url)
# https://example.com:1234/index.html?a=b&foo=bar&foo=baz
url.username="myuser"
url.port=None
print(url)
# 'https://myuser@example.com/index.html?a=b&foo=bar&foo=baz'
Working with query parameters
url.query is a Params instance which supports single and multiple
values per parameter name. It behaves like a dict (a MutableMapping)
where subscription works with a single value per name — the last one —
while add(), get_all() and friends handle multiple values:
from action0.url import Url
url = Url("https://example.com/?b=2&a=1&a=3")
url.query["c"] = "4" # replace/set values, like Params.set()
url.query.add("c", ["x", 5]) # non-strings are coerced, bools become "true"/"false"
url.query.remove("a", "1")
print(url.query["a"]) # the single (last) value; use get() for a default
# 3
print(url.query.get_all("c"))
# ['4', 'x', '5']
print("b" in url.query, len(url.query))
# True 3
print(url.query.as_str())
# b=2&a=3&c=4&c=x&c=5
print(url.query.as_str(sort=True))
# a=3&b=2&c=4&c=5&c=x
url.query.update({"b": 9}, token="abc") # replaces values of existing names
print(url.query.as_str())
# b=9&a=3&c=4&c=x&c=5&token=abc
url.query.sort() # persistent, unlike as_str(sort=True)
print(url.query.as_str())
# a=3&b=9&c=4&c=5&c=x&token=abc
Blank values are kept, so parsing and re-rendering is lossless:
Params("a=&b=1").as_str() is "a=&b=1" again.
Params can also be used on its own, e.g. with a ; separator as used
for path parameters:
from action0.url import Params
params = Params({"foo": "bar", "a": ["b", "c"]}, separator=";")
print(params.as_str())
# foo=bar;a=b;a=c
Building and comparing URLs
from action0.url import Url
# append path segments with "/" (always returns a new Url)
api = Url("https://example.com").origin() / "api" / "v2"
print(api / "users")
# https://example.com/api/v2/users
# resolve links like a browser does
print(Url("https://example.com/docs/intro.html").join("chapter2.html"))
# https://example.com/docs/chapter2.html
# derive variants without touching the original
url = Url("https://user:secret@example.com:8443/index.html")
print(url.copy(scheme="http", port=None))
# http://user:secret@example.com/index.html
# the authority ("hostname:port") and the origin as readable shortcuts
print(url.authority, "|", url.origin())
# example.com:8443 | https://example.com:8443
# equality compares the parts; query parameter order doesn't matter
print(Url("https://example.com?a=1&b=2") == Url("https://example.com?b=2&a=1"))
# True
# repr() never leaks the password (str() / as_str() keep it)
print(repr(url))
# Url(https://user:***@example.com:8443/index.html)
Paths, normalization and encoding
from action0.url import Url
url = Url("https://example.com/docs/guide/intro.html?lang=en")
print(url.name, "|", url.suffix, "|", url.parent)
# intro.html | .html | https://example.com/docs/guide?lang=en
url.name = "outro.html"
print(url)
# https://example.com/docs/guide/outro.html?lang=en
# RFC 3986 style normalization: casing, default ports, dot segments
print(Url("https://example.com:443/a/./b/../c").normalize())
# https://example.com/a/c
# parts are stored decoded, rendering percent-encodes them again ...
url = Url("https://example.com/a%20b", fragment="§ 2")
print(url.path, "|", url)
# /a b | https://example.com/a%20b#%C2%A7%202
# ... and non-ASCII hostnames become punycode
print(Url(scheme="https", hostname="bücher.example"))
# https://xn--bcher-kva.example
print(url.is_absolute(), Url(path="/a").is_relative())
# True True
For debugging and stdlib interoperability there are also url.as_dict()
(all parts as a plain dictionary) and url.as_parse_result() (the
urllib.parse.ParseResult named tuple).
Development
The project is managed with uv; uv run
creates and syncs the virtual environment automatically:
uv run pytest # run the tests (incl. the docstring examples as doctests)
uv run ruff check # lint
uv run ruff format # format
uv run mypy # type-check (also: uv run pyright, uv run ty check)
# build the docs (Sphinx; deployed to GitHub Pages on push to main)
uv run --group docs sphinx-build -W docs docs/_build/html
Releasing
The version lives only in src/action0/url/__init__.py (__version__).
To release: bump it, merge to main, then tag the release commit and push
the tag — the release workflow re-runs all checks, verifies the tag
matches __version__, builds sdist + wheel and publishes to PyPI via
trusted publishing:
git tag v0.1.0
git push origin v0.1.0
AI-assisted development
In the spirit of transparency: most of this project's code, tests and
documentation are written by Claude Code,
Anthropic's coding agent — under human direction and review. The designs
are specified, discussed and iterated by a human, and every change is
reviewed before it lands in main or in a release. AI-authored commits
carry a Co-Authored-By: Claude ... trailer.
About action0
This is just the namespace I like to use for my personal projects. I quite like namespaces.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file action0_url-0.1.0.tar.gz.
File metadata
- Download URL: action0_url-0.1.0.tar.gz
- Upload date:
- Size: 82.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c202e5eb7465793282d2d8342fc2128bdd2497d4cfd9dd596aef5d1d86a2c33e
|
|
| MD5 |
c0f31bbe6a90c0e4440a05fa3b86ef3f
|
|
| BLAKE2b-256 |
c96cc77be6980c1e677f51e99711289d02c8252afd0ca8e95adc41a8458b306d
|
Provenance
The following attestation bundles were made for action0_url-0.1.0.tar.gz:
Publisher:
release.yml on LaughInJar/action0-url
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
action0_url-0.1.0.tar.gz -
Subject digest:
c202e5eb7465793282d2d8342fc2128bdd2497d4cfd9dd596aef5d1d86a2c33e - Sigstore transparency entry: 2379849883
- Sigstore integration time:
-
Permalink:
LaughInJar/action0-url@949f92f8949859b506059eb8fd6b0c526258fef6 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/LaughInJar
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@949f92f8949859b506059eb8fd6b0c526258fef6 -
Trigger Event:
push
-
Statement type:
File details
Details for the file action0_url-0.1.0-py3-none-any.whl.
File metadata
- Download URL: action0_url-0.1.0-py3-none-any.whl
- Upload date:
- Size: 14.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b10d76c81cc6862345b94a55c6df4e6ba1ab0c098cc1af41c07850cb83a4667d
|
|
| MD5 |
8fbaedee96e51c1f762e708cc9453d97
|
|
| BLAKE2b-256 |
a5be84ebbe2caae13d91b828025cf1ddc964574f640cc4a1fd3dc5bc4eb49a34
|
Provenance
The following attestation bundles were made for action0_url-0.1.0-py3-none-any.whl:
Publisher:
release.yml on LaughInJar/action0-url
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
action0_url-0.1.0-py3-none-any.whl -
Subject digest:
b10d76c81cc6862345b94a55c6df4e6ba1ab0c098cc1af41c07850cb83a4667d - Sigstore transparency entry: 2379849916
- Sigstore integration time:
-
Permalink:
LaughInJar/action0-url@949f92f8949859b506059eb8fd6b0c526258fef6 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/LaughInJar
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@949f92f8949859b506059eb8fd6b0c526258fef6 -
Trigger Event:
push
-
Statement type: