Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add uv parser #162

Merged
merged 4 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions linehaul/ua/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,26 @@ def TwineUserAgent(*, version, impl_name, impl_version):
}


@_parser.register
@ua_parser
def UvUserAgent(user_agent):
# We're only concerned about uv user agents.
if not user_agent.startswith("uv/"):
raise UnableToParse

# This format was brand new in uv 0.1.22, so we'll need to restrict it
# to only versions of uv newer than that.
version_str = user_agent.split()[0].split("/", 1)[1]
version = packaging.version.parse(version_str)
if version not in SpecifierSet(">=0.1.22", prereleases=True):
raise UnableToParse

try:
return json.loads(user_agent.split(maxsplit=1)[1])
except (json.JSONDecodeError, UnicodeDecodeError, IndexError):
raise UnableToParse from None


# TODO: We should probably consider not parsing this specially, and moving it to
# just the same as we treat browsers, since we don't really know anything
# about it-- including whether or not the version of Python mentioned is
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "linehaul"
version = "1.0.1"
version = "1.0.2"
description = "User-Agent parsing for PyPI analytics"

readme = "README.md"
Expand Down
58 changes: 58 additions & 0 deletions tests/unit/ua/fixtures/uv.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# uv >=0.1.22 format

# OSX Example
- ua: 'uv/0.1.22 {"installer":{"name":"uv","version":"0.1.22"},"python":"3.12.2","implementation":{"name":"CPython","version":"3.12.2"},"distro":{"name":"macOS","version":"14.4","id":null,"libc":null},"system":{"name":"Darwin","release":"23.2.0"},"cpu":"arm64","openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}'
result:
installer:
name: uv
version: '0.1.22'
python: 3.12.2
implementation:
name: CPython
version: 3.12.2
distro:
name: macOS
version: 14.4
system:
name: Darwin
release: 23.2.0
cpu: arm64

# Linux (Ubuntu) Example
- ua: 'uv/0.1.22 {"installer":{"name":"uv","version":"0.1.22"},"python":"3.12.2","implementation":{"name":"CPython","version":"3.12.2"},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":{"lib":"glibc","version":"2.35"}},"system":{"name":"Linux","release":"6.5.0-1016-azure"},"cpu":"x86_64","openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}'
result:
installer:
name: uv
version: '0.1.22'
python: 3.12.2
implementation:
name: CPython
version: 3.12.2
distro:
name: Ubuntu
version: 22.04
id: jammy
libc:
lib: glibc
version: 2.35
system:
name: Linux
release: 6.5.0-1016-azure
cpu: x86_64
ci: true

# Windows Example
- ua: 'uv/0.1.22 {"installer":{"name":"uv","version":"0.1.22"},"python":"3.12.2","implementation":{"name":"CPython","version":"3.12.2"},"distro":null,"system":{"name":"Windows","release":"2022Server"},"cpu":"AMD64","openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}'
result:
installer:
name: uv
version: '0.1.22'
python: 3.12.2
implementation:
name: CPython
version: 3.12.2
system:
name: Windows
release: 2022Server
cpu: AMD64
ci: true
16 changes: 16 additions & 0 deletions tests/unit/ua/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,22 @@ def test_valid_data(
assert parser.Pip1_4UserAgent(ua) == expected


class TestUvUserAgent:
@given(st.text().filter(lambda i: not i.startswith("uv/")))
def test_not_uv(self, ua):
with pytest.raises(parser.UnableToParse):
parser.UvUserAgent(ua)

@given(st_version(max_version="0.1.21"))
def test_invalid_version(self, version):
with pytest.raises(parser.UnableToParse):
parser.UvUserAgent(f"""uv/{version} {{"installer":{{"name":"uv","version":"{version}"}}}}""")

@given(st.text(max_size=100).filter(lambda i: not _is_valid_json(i)))
def test_invalid_json(self, json_blob):
with pytest.raises(parser.UnableToParse):
parser.UvUserAgent(f"uv/0.1.22 {json_blob}")

class TestParse:
@given(st.text())
def test_unknown_user_agent(self, user_agent):
Expand Down