Skip to content

Commit 3969ddb

Browse files
authored
Merge pull request #847 from atlanhq/BLDX-728
BLDX-728 | update set_package_headers to accept explicit header values
2 parents 70a085d + d43aba8 commit 3969ddb

4 files changed

Lines changed: 191 additions & 42 deletions

File tree

pyatlan/pkg/utils.py

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import sys
77
from typing import Any, Dict, List, Mapping, Optional, Sequence, TypeVar, Union
88

9-
from pydantic.v1 import parse_obj_as, parse_raw_as
9+
from pydantic.v1 import BaseModel, parse_obj_as, parse_raw_as
1010

1111
from pyatlan.client.aio import AsyncAtlanClient
1212
from pyatlan.client.atlan import AtlanClient
@@ -17,6 +17,39 @@
1717
# Type variable for client types
1818
ClientType = TypeVar("ClientType", AtlanClient, AsyncAtlanClient)
1919

20+
21+
class PackageHeaders(BaseModel):
22+
"""Typed container for Atlan package HTTP headers."""
23+
24+
agent: str = "workflow"
25+
workflow_id: Optional[str] = None
26+
app_name: Optional[str] = None
27+
run_id: Optional[str] = None
28+
29+
class Config:
30+
allow_mutation = False
31+
32+
def to_headers(self) -> Dict[str, str]:
33+
return {
34+
"x-atlan-agent": self.agent,
35+
"x-atlan-agent-id": self.workflow_id or "",
36+
"x-atlan-agent-package-name": self.app_name or "",
37+
"x-atlan-agent-app-name": self.app_name or "",
38+
"x-atlan-agent-workflow-id": self.run_id or "",
39+
}
40+
41+
@classmethod
42+
def from_env(cls) -> Optional["PackageHeaders"]:
43+
workflow_id = os.environ.get("X_ATLAN_AGENT_ID")
44+
if not workflow_id:
45+
return None
46+
return cls(
47+
workflow_id=workflow_id,
48+
app_name=os.environ.get("X_ATLAN_AGENT_PACKAGE_NAME", ""),
49+
run_id=os.environ.get("X_ATLAN_AGENT_WORKFLOW_ID", ""),
50+
)
51+
52+
2053
# Try to import OpenTelemetry libraries
2154
try:
2255
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( # type:ignore
@@ -186,33 +219,33 @@ def set_package_ops(run_time_config: RuntimeConfig) -> AtlanClient:
186219
:returns: an intialized AtlanClient that should be used for any calls to the SDK
187220
"""
188221
client = get_client(run_time_config.user_id or "")
189-
if run_time_config.agent == "workflow":
190-
client = set_package_headers(client)
222+
if run_time_config.agent:
223+
client = set_package_headers(
224+
client,
225+
headers=PackageHeaders(
226+
agent=run_time_config.agent,
227+
workflow_id=run_time_config.agent_id,
228+
app_name=run_time_config.agent_pkg,
229+
run_id=run_time_config.agent_wfl,
230+
),
231+
)
191232
return client
192233

193234

194-
def set_package_headers(client: ClientType) -> ClientType:
235+
def set_package_headers(
236+
client: ClientType,
237+
headers: Optional[PackageHeaders] = None,
238+
) -> ClientType:
195239
"""
196-
Configure the AtlanClient or AsyncAtlanClient with package headers from environment variables.
240+
Configure the AtlanClient or AsyncAtlanClient with package headers.
197241
198242
:param client: AtlanClient or AsyncAtlanClient instance to configure
243+
:param headers: PackageHeaders instance; if None, reads from environment variables
199244
:returns: updated client instance of the same type.
200245
"""
201-
202-
if (agent := os.environ.get("X_ATLAN_AGENT")) and (
203-
agent_id := os.environ.get("X_ATLAN_AGENT_ID")
204-
):
205-
headers: Dict[str, str] = {
206-
"x-atlan-agent": agent,
207-
"x-atlan-agent-id": agent_id,
208-
"x-atlan-agent-package-name": os.environ.get(
209-
"X_ATLAN_AGENT_PACKAGE_NAME", ""
210-
),
211-
"x-atlan-agent-workflow-id": os.environ.get(
212-
"X_ATLAN_AGENT_WORKFLOW_ID", ""
213-
),
214-
}
215-
client.update_headers(headers)
246+
pkg_headers = headers or PackageHeaders.from_env()
247+
if pkg_headers and pkg_headers.workflow_id:
248+
client.update_headers(pkg_headers.to_headers())
216249
return client
217250

218251

pyatlan_v9/pkg/utils.py

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,42 @@
55
import os
66
from typing import Optional
77

8+
import msgspec
9+
810
from pyatlan_v9.client.atlan import AtlanClient
911

1012
LOGGER = logging.getLogger(__name__)
1113

1214

15+
class PackageHeaders(msgspec.Struct, frozen=True, kw_only=True):
16+
"""Typed container for Atlan package HTTP headers."""
17+
18+
agent: str = "workflow"
19+
workflow_id: Optional[str] = None
20+
app_name: Optional[str] = None
21+
run_id: Optional[str] = None
22+
23+
def to_headers(self) -> dict[str, str]:
24+
return {
25+
"x-atlan-agent": self.agent,
26+
"x-atlan-agent-id": self.workflow_id or "",
27+
"x-atlan-agent-package-name": self.app_name or "",
28+
"x-atlan-agent-app-name": self.app_name or "",
29+
"x-atlan-agent-workflow-id": self.run_id or "",
30+
}
31+
32+
@classmethod
33+
def from_env(cls) -> Optional["PackageHeaders"]:
34+
workflow_id = os.environ.get("X_ATLAN_AGENT_ID")
35+
if not workflow_id:
36+
return None
37+
return cls(
38+
workflow_id=workflow_id,
39+
app_name=os.environ.get("X_ATLAN_AGENT_PACKAGE_NAME", ""),
40+
run_id=os.environ.get("X_ATLAN_AGENT_WORKFLOW_ID", ""),
41+
)
42+
43+
1344
def get_client(
1445
impersonate_user_id: Optional[str] = None, set_pkg_headers: Optional[bool] = False
1546
) -> AtlanClient:
@@ -68,25 +99,18 @@ def get_client(
6899
return client
69100

70101

71-
def set_package_headers(client: AtlanClient) -> AtlanClient:
102+
def set_package_headers(
103+
client: AtlanClient,
104+
headers: Optional[PackageHeaders] = None,
105+
) -> AtlanClient:
72106
"""
73-
Configure the AtlanClient with package headers from environment variables.
107+
Configure the AtlanClient with package headers.
74108
75109
:param client: AtlanClient instance to configure
110+
:param headers: PackageHeaders instance; if None, reads from environment variables
76111
:returns: updated client instance
77112
"""
78-
if (agent := os.environ.get("X_ATLAN_AGENT")) and (
79-
agent_id := os.environ.get("X_ATLAN_AGENT_ID")
80-
):
81-
headers = {
82-
"x-atlan-agent": agent,
83-
"x-atlan-agent-id": agent_id,
84-
"x-atlan-agent-package-name": os.environ.get(
85-
"X_ATLAN_AGENT_PACKAGE_NAME", ""
86-
),
87-
"x-atlan-agent-workflow-id": os.environ.get(
88-
"X_ATLAN_AGENT_WORKFLOW_ID", ""
89-
),
90-
}
91-
client.update_headers(headers)
113+
pkg_headers = headers or PackageHeaders.from_env()
114+
if pkg_headers and pkg_headers.workflow_id:
115+
client.update_headers(pkg_headers.to_headers())
92116
return client

tests/integration/custom_package_test.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from pyatlan.client.impersonate import ImpersonationClient
1010
from pyatlan.pkg.models import CustomPackage, generate
1111
from pyatlan.pkg.ui import UIConfig, UIStep
12-
from pyatlan.pkg.utils import get_client, set_package_headers
12+
from pyatlan.pkg.utils import PackageHeaders, get_client, set_package_headers
1313
from pyatlan.pkg.widgets import TextInput
1414

1515

@@ -18,7 +18,6 @@ def mock_pkg_env():
1818
with patch.dict(
1919
os.environ,
2020
{
21-
"X_ATLAN_AGENT": "agent_value",
2221
"X_ATLAN_AGENT_ID": "agent_id_value",
2322
"X_ATLAN_AGENT_PACKAGE_NAME": "package_name_value",
2423
"X_ATLAN_AGENT_WORKFLOW_ID": "workflow_id_value",
@@ -100,15 +99,62 @@ def test_set_package_headers(client: AtlanClient, mock_pkg_env):
10099
mock_client = MagicMock(spec=client)
101100
updated_client = set_package_headers(mock_client)
102101
expected_headers = {
103-
"x-atlan-agent": "agent_value",
102+
"x-atlan-agent": "workflow",
104103
"x-atlan-agent-id": "agent_id_value",
105104
"x-atlan-agent-package-name": "package_name_value",
105+
"x-atlan-agent-app-name": "package_name_value",
106106
"x-atlan-agent-workflow-id": "workflow_id_value",
107107
}
108108
mock_client.update_headers.assert_called_once_with(expected_headers)
109109
assert updated_client == mock_client
110110

111111

112+
def test_set_package_headers_explicit_values(client: AtlanClient):
113+
mock_client = MagicMock(spec=client)
114+
headers = PackageHeaders(
115+
agent="custom-agent",
116+
workflow_id="wf-123",
117+
app_name="my-app",
118+
run_id="run-456",
119+
)
120+
updated_client = set_package_headers(mock_client, headers=headers)
121+
expected_headers = {
122+
"x-atlan-agent": "custom-agent",
123+
"x-atlan-agent-id": "wf-123",
124+
"x-atlan-agent-package-name": "my-app",
125+
"x-atlan-agent-app-name": "my-app",
126+
"x-atlan-agent-workflow-id": "run-456",
127+
}
128+
mock_client.update_headers.assert_called_once_with(expected_headers)
129+
assert updated_client == mock_client
130+
131+
132+
def test_set_package_headers_explicit_overrides_env(client: AtlanClient, mock_pkg_env):
133+
mock_client = MagicMock(spec=client)
134+
headers = PackageHeaders(
135+
workflow_id="override-wf-id",
136+
app_name="override-app",
137+
)
138+
updated_client = set_package_headers(mock_client, headers=headers)
139+
expected_headers = {
140+
"x-atlan-agent": "workflow",
141+
"x-atlan-agent-id": "override-wf-id",
142+
"x-atlan-agent-package-name": "override-app",
143+
"x-atlan-agent-app-name": "override-app",
144+
"x-atlan-agent-workflow-id": "",
145+
}
146+
mock_client.update_headers.assert_called_once_with(expected_headers)
147+
assert updated_client == mock_client
148+
149+
150+
def test_set_package_headers_no_workflow_id(client: AtlanClient):
151+
mock_client = MagicMock(spec=client)
152+
with patch.dict(os.environ, {}, clear=True):
153+
updated_client = set_package_headers(mock_client)
154+
mock_client.update_headers.assert_not_called()
155+
assert updated_client == mock_client
156+
157+
112158
@patch.object(ImpersonationClient, "user", return_value="some-api-key")
113159
def test_get_client_user_id_handling(
114160
mock_impersonate_client,

tests_v9/integration/custom_package_test.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@
1010
from pyatlan.pkg.widgets import TextInput
1111
from pyatlan_v9.client.atlan import AtlanClient
1212
from pyatlan_v9.client.impersonate import V9ImpersonationClient
13-
from pyatlan_v9.pkg.utils import get_client, set_package_headers
13+
from pyatlan_v9.pkg.utils import PackageHeaders, get_client, set_package_headers
1414

1515

1616
@pytest.fixture
1717
def mock_pkg_env():
1818
with patch.dict(
1919
os.environ,
2020
{
21-
"X_ATLAN_AGENT": "agent_value",
2221
"X_ATLAN_AGENT_ID": "agent_id_value",
2322
"X_ATLAN_AGENT_PACKAGE_NAME": "package_name_value",
2423
"X_ATLAN_AGENT_WORKFLOW_ID": "workflow_id_value",
@@ -100,15 +99,62 @@ def test_set_package_headers(client: AtlanClient, mock_pkg_env):
10099
mock_client = MagicMock(spec=client)
101100
updated_client = set_package_headers(mock_client)
102101
expected_headers = {
103-
"x-atlan-agent": "agent_value",
102+
"x-atlan-agent": "workflow",
104103
"x-atlan-agent-id": "agent_id_value",
105104
"x-atlan-agent-package-name": "package_name_value",
105+
"x-atlan-agent-app-name": "package_name_value",
106106
"x-atlan-agent-workflow-id": "workflow_id_value",
107107
}
108108
mock_client.update_headers.assert_called_once_with(expected_headers)
109109
assert updated_client == mock_client
110110

111111

112+
def test_set_package_headers_explicit_values(client: AtlanClient):
113+
mock_client = MagicMock(spec=client)
114+
headers = PackageHeaders(
115+
agent="custom-agent",
116+
workflow_id="wf-123",
117+
app_name="my-app",
118+
run_id="run-456",
119+
)
120+
updated_client = set_package_headers(mock_client, headers=headers)
121+
expected_headers = {
122+
"x-atlan-agent": "custom-agent",
123+
"x-atlan-agent-id": "wf-123",
124+
"x-atlan-agent-package-name": "my-app",
125+
"x-atlan-agent-app-name": "my-app",
126+
"x-atlan-agent-workflow-id": "run-456",
127+
}
128+
mock_client.update_headers.assert_called_once_with(expected_headers)
129+
assert updated_client == mock_client
130+
131+
132+
def test_set_package_headers_explicit_overrides_env(client: AtlanClient, mock_pkg_env):
133+
mock_client = MagicMock(spec=client)
134+
headers = PackageHeaders(
135+
workflow_id="override-wf-id",
136+
app_name="override-app",
137+
)
138+
updated_client = set_package_headers(mock_client, headers=headers)
139+
expected_headers = {
140+
"x-atlan-agent": "workflow",
141+
"x-atlan-agent-id": "override-wf-id",
142+
"x-atlan-agent-package-name": "override-app",
143+
"x-atlan-agent-app-name": "override-app",
144+
"x-atlan-agent-workflow-id": "",
145+
}
146+
mock_client.update_headers.assert_called_once_with(expected_headers)
147+
assert updated_client == mock_client
148+
149+
150+
def test_set_package_headers_no_workflow_id(client: AtlanClient):
151+
mock_client = MagicMock(spec=client)
152+
with patch.dict(os.environ, {}, clear=True):
153+
updated_client = set_package_headers(mock_client)
154+
mock_client.update_headers.assert_not_called()
155+
assert updated_client == mock_client
156+
157+
112158
@patch.object(V9ImpersonationClient, "user", return_value="some-api-key")
113159
def test_get_client_user_id_handling(
114160
mock_impersonate_client,

0 commit comments

Comments
 (0)