Skip to main content

Action0-Url

CI PyPI

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

action0_url-0.1.0.tar.gz (82.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

action0_url-0.1.0-py3-none-any.whl (14.8 kB view details)

Uploaded Python 3

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

Hashes for action0_url-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c202e5eb7465793282d2d8342fc2128bdd2497d4cfd9dd596aef5d1d86a2c33e
MD5 c0f31bbe6a90c0e4440a05fa3b86ef3f
BLAKE2b-256 c96cc77be6980c1e677f51e99711289d02c8252afd0ca8e95adc41a8458b306d

See more details on using hashes here.

Provenance

The following attestation bundles were made for action0_url-0.1.0.tar.gz:

Publisher: release.yml on LaughInJar/action0-url

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for action0_url-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b10d76c81cc6862345b94a55c6df4e6ba1ab0c098cc1af41c07850cb83a4667d
MD5 8fbaedee96e51c1f762e708cc9453d97
BLAKE2b-256 a5be84ebbe2caae13d91b828025cf1ddc964574f640cc4a1fd3dc5bc4eb49a34

See more details on using hashes here.

Provenance

The following attestation bundles were made for action0_url-0.1.0-py3-none-any.whl:

Publisher: release.yml on LaughInJar/action0-url

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page