Skip to content

Commit b10850c

Browse files
samypr100ewdurbin
andauthored
Add uv parser (#162)
* feat: add uv parser * bump: 1.0.1 to 1.0.2 --------- Co-authored-by: Ee Durbin <ewdurbin@gmail.com>
1 parent 4649c7c commit b10850c

File tree

4 files changed

+95
-1
lines changed

4 files changed

+95
-1
lines changed

linehaul/ua/parser.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,26 @@ def TwineUserAgent(*, version, impl_name, impl_version):
197197
}
198198

199199

200+
@_parser.register
201+
@ua_parser
202+
def UvUserAgent(user_agent):
203+
# We're only concerned about uv user agents.
204+
if not user_agent.startswith("uv/"):
205+
raise UnableToParse
206+
207+
# This format was brand new in uv 0.1.22, so we'll need to restrict it
208+
# to only versions of uv newer than that.
209+
version_str = user_agent.split()[0].split("/", 1)[1]
210+
version = packaging.version.parse(version_str)
211+
if version not in SpecifierSet(">=0.1.22", prereleases=True):
212+
raise UnableToParse
213+
214+
try:
215+
return json.loads(user_agent.split(maxsplit=1)[1])
216+
except (json.JSONDecodeError, UnicodeDecodeError, IndexError):
217+
raise UnableToParse from None
218+
219+
200220
# TODO: We should probably consider not parsing this specially, and moving it to
201221
# just the same as we treat browsers, since we don't really know anything
202222
# about it-- including whether or not the version of Python mentioned is

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "linehaul"
3-
version = "1.0.1"
3+
version = "1.0.2"
44
description = "User-Agent parsing for PyPI analytics"
55

66
readme = "README.md"

tests/unit/ua/fixtures/uv.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# uv >=0.1.22 format
2+
3+
# OSX Example
4+
- 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}'
5+
result:
6+
installer:
7+
name: uv
8+
version: '0.1.22'
9+
python: 3.12.2
10+
implementation:
11+
name: CPython
12+
version: 3.12.2
13+
distro:
14+
name: macOS
15+
version: 14.4
16+
system:
17+
name: Darwin
18+
release: 23.2.0
19+
cpu: arm64
20+
21+
# Linux (Ubuntu) Example
22+
- 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}'
23+
result:
24+
installer:
25+
name: uv
26+
version: '0.1.22'
27+
python: 3.12.2
28+
implementation:
29+
name: CPython
30+
version: 3.12.2
31+
distro:
32+
name: Ubuntu
33+
version: 22.04
34+
id: jammy
35+
libc:
36+
lib: glibc
37+
version: 2.35
38+
system:
39+
name: Linux
40+
release: 6.5.0-1016-azure
41+
cpu: x86_64
42+
ci: true
43+
44+
# Windows Example
45+
- 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}'
46+
result:
47+
installer:
48+
name: uv
49+
version: '0.1.22'
50+
python: 3.12.2
51+
implementation:
52+
name: CPython
53+
version: 3.12.2
54+
system:
55+
name: Windows
56+
release: 2022Server
57+
cpu: AMD64
58+
ci: true

tests/unit/ua/test_parser.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,22 @@ def test_valid_data(
160160
assert parser.Pip1_4UserAgent(ua) == expected
161161

162162

163+
class TestUvUserAgent:
164+
@given(st.text().filter(lambda i: not i.startswith("uv/")))
165+
def test_not_uv(self, ua):
166+
with pytest.raises(parser.UnableToParse):
167+
parser.UvUserAgent(ua)
168+
169+
@given(st_version(max_version="0.1.21"))
170+
def test_invalid_version(self, version):
171+
with pytest.raises(parser.UnableToParse):
172+
parser.UvUserAgent(f"""uv/{version} {{"installer":{{"name":"uv","version":"{version}"}}}}""")
173+
174+
@given(st.text(max_size=100).filter(lambda i: not _is_valid_json(i)))
175+
def test_invalid_json(self, json_blob):
176+
with pytest.raises(parser.UnableToParse):
177+
parser.UvUserAgent(f"uv/0.1.22 {json_blob}")
178+
163179
class TestParse:
164180
@given(st.text())
165181
def test_unknown_user_agent(self, user_agent):

0 commit comments

Comments
 (0)