Skip to content

Commit 589c4c7

Browse files
authored
Merge pull request #707 from atlanhq/APP-8817
APP-8817: Fixed `get_client` bug and bumped to release `8.0.1`
2 parents e22558d + 1e2b0a9 commit 589c4c7

9 files changed

Lines changed: 329 additions & 42 deletions

File tree

.github/workflows/pyatlan-wolfi-base.yaml

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ on:
2626
description: 'Pyatlan version (leave empty to use version.txt ie: latest)'
2727
required: false
2828
type: string
29+
pyatlan_branch:
30+
description: 'Pyatlan git branch (overrides version - installs from git://github.com/atlanhq/atlan-python.git@branch)'
31+
required: false
32+
type: string
33+
release:
34+
types: [published]
2935

3036
permissions:
3137
contents: read
@@ -54,26 +60,54 @@ jobs:
5460
- name: Set build parameters
5561
id: set-params
5662
run: |
57-
# Set Python version (default to 3.13 if empty)
58-
if [ -z "${{ github.event.inputs.python_version }}" ]; then
63+
# Check if triggered by release or manual dispatch
64+
if [ "${{ github.event_name }}" = "release" ]; then
65+
# Release trigger: use latest Python and SDK version, release build type
66+
BUILD_TYPE="release"
5967
PYTHON_VERSION="3.13"
60-
else
61-
PYTHON_VERSION="${{ github.event.inputs.python_version }}"
62-
fi
63-
echo "PYTHON_VERSION=$PYTHON_VERSION" >> $GITHUB_ENV
64-
65-
# Set Pyatlan version (default to version.txt if empty)
66-
if [ -z "${{ github.event.inputs.pyatlan_version }}" ]; then
6768
PYATLAN_VERSION=$(cat pyatlan/version.txt)
68-
echo "Using pyatlan version from version.txt: $PYATLAN_VERSION"
69+
PYATLAN_BRANCH=""
70+
INSTALL_FROM_GIT="false"
71+
echo "Release trigger detected - using latest versions"
6972
else
70-
PYATLAN_VERSION="${{ github.event.inputs.pyatlan_version }}"
71-
echo "Using specified pyatlan version: $PYATLAN_VERSION"
73+
# Manual dispatch: use provided inputs with defaults
74+
BUILD_TYPE="${{ github.event.inputs.build_type }}"
75+
76+
# Set Python version (default to 3.13 if empty)
77+
if [ -z "${{ github.event.inputs.python_version }}" ]; then
78+
PYTHON_VERSION="3.13"
79+
else
80+
PYTHON_VERSION="${{ github.event.inputs.python_version }}"
81+
fi
82+
83+
# Check if branch is provided (overrides version)
84+
if [ -n "${{ github.event.inputs.pyatlan_branch }}" ]; then
85+
PYATLAN_BRANCH="${{ github.event.inputs.pyatlan_branch }}"
86+
PYATLAN_VERSION="branch-${PYATLAN_BRANCH}"
87+
INSTALL_FROM_GIT="true"
88+
echo "Using pyatlan branch: $PYATLAN_BRANCH"
89+
else
90+
# Set Pyatlan version (default to version.txt if empty)
91+
if [ -z "${{ github.event.inputs.pyatlan_version }}" ]; then
92+
PYATLAN_VERSION=$(cat pyatlan/version.txt)
93+
echo "Using pyatlan version from version.txt: $PYATLAN_VERSION"
94+
else
95+
PYATLAN_VERSION="${{ github.event.inputs.pyatlan_version }}"
96+
echo "Using specified pyatlan version: $PYATLAN_VERSION"
97+
fi
98+
PYATLAN_BRANCH=""
99+
INSTALL_FROM_GIT="false"
100+
fi
72101
fi
102+
103+
echo "BUILD_TYPE=$BUILD_TYPE" >> $GITHUB_ENV
104+
echo "PYTHON_VERSION=$PYTHON_VERSION" >> $GITHUB_ENV
73105
echo "PYATLAN_VERSION=$PYATLAN_VERSION" >> $GITHUB_ENV
106+
echo "PYATLAN_BRANCH=$PYATLAN_BRANCH" >> $GITHUB_ENV
107+
echo "INSTALL_FROM_GIT=$INSTALL_FROM_GIT" >> $GITHUB_ENV
74108
75109
# Set platforms based on build type
76-
if [ "${{ github.event.inputs.build_type }}" = "dev" ]; then
110+
if [ "$BUILD_TYPE" = "dev" ]; then
77111
PLATFORMS="linux/amd64"
78112
else
79113
PLATFORMS="linux/amd64,linux/arm64"
@@ -85,16 +119,23 @@ jobs:
85119
echo "COMMIT_HASH=$COMMIT_HASH" >> $GITHUB_ENV
86120
87121
echo "Build parameters:"
88-
echo " - Build Type: ${{ github.event.inputs.build_type }}"
122+
echo " - Trigger: ${{ github.event_name }}"
123+
echo " - Build Type: $BUILD_TYPE"
89124
echo " - Python Version: $PYTHON_VERSION"
90125
echo " - Pyatlan Version: $PYATLAN_VERSION"
126+
if [ "$INSTALL_FROM_GIT" = "true" ]; then
127+
echo " - Pyatlan Branch: $PYATLAN_BRANCH"
128+
echo " - Install Method: Git (development build)"
129+
else
130+
echo " - Install Method: PyPI (stable release)"
131+
fi
91132
echo " - Platforms: $PLATFORMS"
92133
echo " - Commit Hash: $COMMIT_HASH"
93134
94135
- name: Generate image tags
95136
id: generate-tags
96137
run: |
97-
BUILD_TYPE="${{ github.event.inputs.build_type }}"
138+
BUILD_TYPE="${{ env.BUILD_TYPE }}"
98139
PYTHON_VERSION="${{ env.PYTHON_VERSION }}"
99140
PYATLAN_VERSION="${{ env.PYATLAN_VERSION }}"
100141
COMMIT_HASH="${{ env.COMMIT_HASH }}"
@@ -112,7 +153,7 @@ jobs:
112153
echo "Generated image tag: ghcr.io/atlanhq/pyatlan-wolfi-base:$IMAGE_TAG"
113154
114155
- name: Wait for PyPI availability
115-
if: github.event.inputs.pyatlan_version != ''
156+
if: env.INSTALL_FROM_GIT == 'false' && (github.event.inputs.pyatlan_version != '' || github.event_name == 'release')
116157
uses: nick-fields/retry@v3
117158
with:
118159
max_attempts: 5
@@ -139,6 +180,8 @@ jobs:
139180
build-args: |
140181
PYTHON_VERSION=${{ env.PYTHON_VERSION }}
141182
PYATLAN_VERSION=${{ env.PYATLAN_VERSION }}
183+
PYATLAN_BRANCH=${{ env.PYATLAN_BRANCH }}
184+
INSTALL_FROM_GIT=${{ env.INSTALL_FROM_GIT }}
142185
cache-from: type=gha
143186
cache-to: type=gha,mode=max
144187

@@ -148,11 +191,18 @@ jobs:
148191
echo "" >> $GITHUB_STEP_SUMMARY
149192
echo "### Image Details:" >> $GITHUB_STEP_SUMMARY
150193
echo "- **Base Image:** Wolfi" >> $GITHUB_STEP_SUMMARY
151-
echo "- **Build Type:** ${{ github.event.inputs.build_type }}" >> $GITHUB_STEP_SUMMARY
194+
echo "- **Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
195+
echo "- **Build Type:** ${{ env.BUILD_TYPE }}" >> $GITHUB_STEP_SUMMARY
152196
echo "- **Python Version:** ${{ env.PYTHON_VERSION }}" >> $GITHUB_STEP_SUMMARY
153197
echo "- **Pyatlan Version:** ${{ env.PYATLAN_VERSION }}" >> $GITHUB_STEP_SUMMARY
198+
if [ "${{ env.INSTALL_FROM_GIT }}" = "true" ]; then
199+
echo "- **Pyatlan Branch:** ${{ env.PYATLAN_BRANCH }}" >> $GITHUB_STEP_SUMMARY
200+
echo "- **Install Method:** Git (development build)" >> $GITHUB_STEP_SUMMARY
201+
else
202+
echo "- **Install Method:** PyPI (stable release)" >> $GITHUB_STEP_SUMMARY
203+
fi
154204
echo "- **Platforms:** ${{ env.PLATFORMS }}" >> $GITHUB_STEP_SUMMARY
155-
if [ "${{ github.event.inputs.build_type }}" = "dev" ]; then
205+
if [ "${{ env.BUILD_TYPE }}" = "dev" ]; then
156206
echo "- **Commit Hash:** ${{ env.COMMIT_HASH }}" >> $GITHUB_STEP_SUMMARY
157207
fi
158208
echo "" >> $GITHUB_STEP_SUMMARY

Dockerfile.wolfi

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,41 @@ FROM cgr.dev/chainguard/wolfi-base
33
# Build arguments for configurable versions
44
ARG PYTHON_VERSION=3.11
55
ARG PYATLAN_VERSION=latest
6+
ARG PYATLAN_BRANCH=""
7+
ARG INSTALL_FROM_GIT=false
68

79
WORKDIR /app
810

9-
# Install Python
10-
RUN apk add python-${PYTHON_VERSION} && \
11+
# Install Python and git (needed for git-based installations)
12+
RUN apk add python-${PYTHON_VERSION} git && \
1113
chown -R nonroot:nonroot /app/
1214

1315
# Install uv
1416
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
1517

16-
# Install pyatlan from PyPI using uv pip install (as root for --system)
18+
# Install pyatlan - either from PyPI or git branch
1719
RUN --mount=type=cache,target=/root/.cache/uv \
18-
if [ "$PYATLAN_VERSION" = "latest" ]; then \
20+
if [ "$INSTALL_FROM_GIT" = "true" ]; then \
21+
echo "Installing pyatlan from git branch: $PYATLAN_BRANCH"; \
22+
uv pip install --system "git+https://github.com/atlanhq/atlan-python.git@$PYATLAN_BRANCH"; \
23+
elif [ "$PYATLAN_VERSION" = "latest" ]; then \
24+
echo "Installing latest pyatlan from PyPI"; \
1925
uv pip install --system pyatlan; \
2026
else \
27+
echo "Installing pyatlan==$PYATLAN_VERSION from PyPI"; \
2128
uv pip install --system pyatlan==$PYATLAN_VERSION; \
2229
fi
2330

31+
# Add build information as labels
32+
RUN if [ "$INSTALL_FROM_GIT" = "true" ]; then \
33+
echo "LABEL pyatlan.source=git" >> /tmp/labels && \
34+
echo "LABEL pyatlan.branch=$PYATLAN_BRANCH" >> /tmp/labels; \
35+
else \
36+
echo "LABEL pyatlan.source=pypi" >> /tmp/labels && \
37+
echo "LABEL pyatlan.version=$PYATLAN_VERSION" >> /tmp/labels; \
38+
fi && \
39+
echo "LABEL python.version=$PYTHON_VERSION" >> /tmp/labels
40+
2441
USER nonroot
2542

2643
# Set environment variables

HISTORY.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
## 8.0.1 (August 25, 2025)
2+
3+
### New Features
4+
5+
- Added `get_client_async()` for initializing `AsyncAtlanClient` when using packages.
6+
- Added optional parameter `set_pkg_headers()` to set package headers on the client (defaults to `False`).
7+
8+
### Bug Fixes
9+
10+
- Fixed `httpcore.LocalProtocolError` exception in `get_client()` that occurred when `ATLAN_API_KEY` environment variable was not configured (commonly encountered during package impersonation with empty `API` key strings).
11+
12+
### QOL Improvements
13+
14+
- Added `pyatlan-wolfi-base` Docker image workflow with enhanced capabilities:
15+
- **Automatic release builds**: Workflow now triggers automatically on GitHub releases using latest Python (3.13) and SDK versions
16+
- **Git branch support**: Added ability to install SDK from any git branch for development/testing purposes via `pyatlan_branch` parameter
17+
- **Smart installation logic**: Automatically chooses between PyPI (stable) or git (development) installation methods
18+
- **Enhanced tagging**: Branch builds tagged as `branch-{branchname}-{python}-{commit}` for easy identification
19+
- **Build metadata**: Images include labels tracking installation source, version/branch, and Python version
20+
- **Conditional PyPI checks**: Skips PyPI availability checks when installing from git branches
21+
- **Improved logging**: Shows installation method, branch info, and trigger source in build outputs
22+
123
## 8.0.0 (August 20, 2025)
224

325
### New Features

pyatlan/client/aio/client.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,7 @@ async def from_token_guid( # type: ignore[override]
174174

175175
# Step 1: Initialize base client and get Atlan-Argo credentials
176176
# Note: Using empty api_key as we're bootstrapping authentication
177-
client = cls(base_url=final_base_url)
178-
# Explicitly set api_key to empty string to avoid
179-
# httpx.LocalProtocolError: Illegal header value b'Bearer '
180-
client.api_key = ""
177+
client = cls(base_url=final_base_url, api_key="")
181178
client_info = ImpersonateUser.get_client_info(
182179
client_id=client_id, client_secret=client_secret
183180
)
@@ -873,6 +870,11 @@ async def _upload_file(self, api, file=None, filename=None):
873870
self._api_logger(api, path)
874871
return await self._call_api_internal(api, path, params, binary_data=post_data)
875872

873+
def update_headers(self, header: dict[str, str]):
874+
"""Update headers for the async session."""
875+
if self._async_session:
876+
self._async_session.headers.update(header)
877+
876878
async def aclose(self):
877879
"""Close async resources"""
878880
if self._async_session:

pyatlan/client/atlan.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,11 @@ class Config:
167167

168168
def __init__(self, **data):
169169
super().__init__(**data)
170-
self._request_params = {
171-
"headers": {
172-
"authorization": f"Bearer {self.api_key}",
173-
}
174-
}
170+
self._request_params = (
171+
{"headers": {"authorization": f"Bearer {self.api_key}"}}
172+
if self.api_key and self.api_key.strip()
173+
else {"headers": {}}
174+
)
175175
# Configure httpx client with the provided retry settings
176176
self._session = httpx.Client(
177177
transport=RetryTransport(retry=self.retry),
@@ -377,10 +377,7 @@ def from_token_guid(
377377

378378
# Step 1: Initialize base client and get Atlan-Argo credentials
379379
# Note: Using empty api_key as we're bootstrapping authentication
380-
client = AtlanClient(base_url=final_base_url)
381-
# Explicitly set api_key to empty string to avoid
382-
# httpx.LocalProtocolError: Illegal header value b'Bearer '
383-
client.api_key = ""
380+
client = AtlanClient(base_url=final_base_url, api_key="")
384381
client_info = ImpersonateUser.get_client_info(
385382
client_id=client_id, client_secret=client_secret
386383
)

pyatlan/pkg/utils.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44
import logging
55
import os
66
import sys
7-
from typing import Any, Dict, List, Mapping, Optional, Sequence, Union
7+
from typing import Any, Dict, List, Mapping, Optional, Sequence, TypeVar, Union
88

99
from pydantic.v1 import parse_obj_as, parse_raw_as
1010

11+
from pyatlan.client.aio import AsyncAtlanClient
1112
from pyatlan.client.atlan import AtlanClient
1213
from pyatlan.pkg.models import RuntimeConfig
1314

1415
LOGGER = logging.getLogger(__name__)
1516

17+
# Type variable for client types
18+
ClientType = TypeVar("ClientType", AtlanClient, AsyncAtlanClient)
19+
1620
# Try to import OpenTelemetry libraries
1721
try:
1822
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( # type:ignore
@@ -64,13 +68,16 @@ def _is_valid_type(self, value: Any) -> bool:
6468
OTEL_IMPORTS_AVAILABLE = False
6569

6670

67-
def get_client(impersonate_user_id: str) -> AtlanClient:
71+
def get_client(
72+
impersonate_user_id: str, set_pkg_headers: Optional[bool] = False
73+
) -> AtlanClient:
6874
"""
6975
Set up the default Atlan client, based on environment variables.
7076
This will use an API token if found in ATLAN_API_KEY, and will fallback to attempting to impersonate a user if
7177
ATLAN_API_KEY is empty.
7278
7379
:param impersonate_user_id: unique identifier (GUID) of a user or API token to impersonate
80+
:param set_pkg_headers: whether to set package headers on the client (default is False)
7481
:returns: an initialized client
7582
"""
7683
base_url = os.environ.get("ATLAN_BASE_URL", "INTERNAL")
@@ -94,6 +101,46 @@ def get_client(impersonate_user_id: str) -> AtlanClient:
94101
client = AtlanClient(base_url=base_url, api_key=api_key)
95102
if user_id:
96103
client._user_id = user_id
104+
if set_pkg_headers:
105+
client = set_package_headers(client)
106+
return client
107+
108+
109+
async def get_client_async(
110+
impersonate_user_id: str, set_pkg_headers: Optional[bool] = False
111+
) -> AsyncAtlanClient:
112+
"""
113+
Set up the default async Atlan client, based on environment variables.
114+
This will use an API token if found in ATLAN_API_KEY, and will fallback to attempting to impersonate a user if
115+
ATLAN_API_KEY is empty.
116+
117+
:param impersonate_user_id: unique identifier (GUID) of a user or API token to impersonate
118+
:param set_pkg_headers: whether to set package headers on the client (default is False)
119+
:returns: an initialized async client
120+
"""
121+
base_url = os.environ.get("ATLAN_BASE_URL", "INTERNAL")
122+
api_token = os.environ.get("ATLAN_API_KEY", "")
123+
user_id = os.environ.get("ATLAN_USER_ID", impersonate_user_id)
124+
125+
if api_token:
126+
LOGGER.info("Using provided API token for authentication.")
127+
api_key = api_token
128+
elif user_id:
129+
LOGGER.info("No API token found, attempting to impersonate user: %s", user_id)
130+
client = AsyncAtlanClient(base_url=base_url, api_key="")
131+
api_key = await client.impersonate.user(user_id=user_id)
132+
else:
133+
LOGGER.info(
134+
"No API token or impersonation user, attempting short-lived escalation."
135+
)
136+
client = AsyncAtlanClient(base_url=base_url, api_key="")
137+
api_key = await client.impersonate.escalate()
138+
139+
client = AsyncAtlanClient(base_url=base_url, api_key=api_key)
140+
if user_id:
141+
client._user_id = user_id
142+
if set_pkg_headers:
143+
client = set_package_headers(client)
97144
return client
98145

99146

@@ -110,12 +157,12 @@ def set_package_ops(run_time_config: RuntimeConfig) -> AtlanClient:
110157
return client
111158

112159

113-
def set_package_headers(client: AtlanClient) -> AtlanClient:
160+
def set_package_headers(client: ClientType) -> ClientType:
114161
"""
115-
Configure the AtlanClient with package headers from environment variables.
162+
Configure the AtlanClient or AsyncAtlanClient with package headers from environment variables.
116163
117-
:param client: AtlanClient instance to configure
118-
:returns: updated AtlanClient instance.
164+
:param client: AtlanClient or AsyncAtlanClient instance to configure
165+
:returns: updated client instance of the same type.
119166
"""
120167

121168
if (agent := os.environ.get("X_ATLAN_AGENT")) and (

pyatlan/version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
8.0.0
1+
8.0.1

0 commit comments

Comments
 (0)