Skip to content

Commit 2749463

Browse files
dcjclaude
andcommitted
Add user_agent parameter for client self-identification in VTN logs
BaseClient accepts an optional user_agent kwarg, composes a layered User-Agent string (python-oa3-client/<ver> openadr3/<ver> <custom>), and passes it through to the upstream OpenADRClient. Bumps openadr3 dependency to >=0.3.0 for upstream user_agent support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 18ce003 commit 2749463

4 files changed

Lines changed: 60 additions & 2 deletions

File tree

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,25 @@ bl_token = base64.b64encode(b"bl_client:1001").decode()
6565
ven_token = base64.b64encode(b"ven_client:999").decode()
6666
```
6767

68+
## User-Agent
69+
70+
Clients send a composed User-Agent header for server-side log identification:
71+
72+
```
73+
python-oa3-client/0.2.1 openadr3/0.3.0 (node=a1b2c3d4e5f6)
74+
```
75+
76+
Add your own identifier with the `user_agent` parameter:
77+
78+
```python
79+
ven = VenClient(
80+
url=vtn_url,
81+
token=token,
82+
user_agent="my-thermostat/1.0 (contact@example.com)",
83+
)
84+
# UA: python-oa3-client/0.2.1 openadr3/0.3.0 (node=...) my-thermostat/1.0 (contact@example.com)
85+
```
86+
6887
## mDNS/DNS-SD Discovery
6988

7089
Requires: `pip install python-oa3-client[mdns]`

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ classifiers = [
2525
"Topic :: Software Development :: Libraries",
2626
]
2727
dependencies = [
28-
"openadr3>=0.2.0",
28+
"openadr3>=0.3.0",
2929
]
3030

3131
[project.optional-dependencies]

src/openadr3_client/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44

55
import logging
66
import threading
7+
from importlib.metadata import version as _pkg_version
78
from typing import Any
89

10+
from openadr3.api import (
11+
DEFAULT_USER_AGENT as _UPSTREAM_UA,
12+
)
913
from openadr3.api import (
1014
OpenADRClient,
1115
create_bl_client,
@@ -17,6 +21,8 @@
1721

1822
log = logging.getLogger(__name__)
1923

24+
DEFAULT_USER_AGENT = f"python-oa3-client/{_pkg_version('python-oa3-client')} {_UPSTREAM_UA}"
25+
2026

2127
class BaseClient:
2228
"""Lifecycle-managed OpenADR 3 client with __getattr__ delegation.
@@ -38,6 +44,7 @@ def __init__(
3844
validate: bool = False,
3945
discovery: str | DiscoveryMode = "never",
4046
discovery_timeout: float = 3.0,
47+
user_agent: str | None = None,
4148
) -> None:
4249
if not token and not (client_id and client_secret):
4350
raise ValueError("Provide either token or both client_id and client_secret")
@@ -57,6 +64,7 @@ def __init__(
5764
self.spec_version = spec_version
5865
self.spec_path = spec_path
5966
self.validate = validate
67+
self.user_agent = f"{DEFAULT_USER_AGENT} {user_agent}" if user_agent else DEFAULT_USER_AGENT
6068

6169
self._resolved_url: str | None = None
6270
self._api: OpenADRClient | None = None
@@ -98,6 +106,7 @@ def start(self) -> BaseClient:
98106
token=self.token,
99107
spec_path=self.spec_path,
100108
validate=self.validate,
109+
user_agent=self.user_agent,
101110
)
102111
log.info(
103112
"%s started: type=%s url=%s",

tests/test_base.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from openadr3_client.base import BaseClient
7+
from openadr3_client.base import DEFAULT_USER_AGENT, BaseClient
88

99

1010
class TestBaseClientLifecycle:
@@ -121,3 +121,33 @@ def test_raises_attribute_error_unknown(self, mock_create):
121121
c.start()
122122
with pytest.raises(AttributeError):
123123
c.totally_fake_method()
124+
125+
126+
class TestBaseClientUserAgent:
127+
def test_default_user_agent(self):
128+
c = BaseClient(url="http://test", token="tok")
129+
assert c.user_agent == DEFAULT_USER_AGENT
130+
assert "python-oa3-client/" in c.user_agent
131+
assert "openadr3/" in c.user_agent
132+
133+
def test_custom_user_agent_appended(self):
134+
c = BaseClient(url="http://test", token="tok", user_agent="my-app/1.0")
135+
assert c.user_agent.startswith("python-oa3-client/")
136+
assert c.user_agent.endswith("my-app/1.0")
137+
assert "openadr3/" in c.user_agent
138+
139+
@patch("openadr3_client.base.create_ven_client")
140+
def test_user_agent_passed_to_factory(self, mock_create):
141+
mock_create.return_value = MagicMock()
142+
c = BaseClient(url="http://test", token="tok", user_agent="my-app/2.0")
143+
c.start()
144+
call_kwargs = mock_create.call_args[1]
145+
assert call_kwargs["user_agent"] == c.user_agent
146+
147+
@patch("openadr3_client.base.create_ven_client")
148+
def test_default_user_agent_passed_to_factory(self, mock_create):
149+
mock_create.return_value = MagicMock()
150+
c = BaseClient(url="http://test", token="tok")
151+
c.start()
152+
call_kwargs = mock_create.call_args[1]
153+
assert call_kwargs["user_agent"] == DEFAULT_USER_AGENT

0 commit comments

Comments
 (0)