Skip to content

Commit 5ca7edc

Browse files
committed
[ci/qa] GH workflow: v9 unit + label-triggered v9 integration; fix all qa-checks (ruff format, ruff check, mypy)
Made-with: Cursor
1 parent 7460201 commit 5ca7edc

45 files changed

Lines changed: 496 additions & 183 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/pyatlan-pr.yaml

Lines changed: 173 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
name: Pyatlan Pull Request Build
22

33
# This workflow runs both sync and async integration tests intelligently:
4-
# - Sync integration tests: Always run on every PR
5-
# - Async integration tests: Only run when:
6-
# 1. Changes detected in pyatlan/*/aio/ or tests/*/aio/ paths
7-
# 2. PR has the "run-async-tests" label (manual trigger)
8-
# This prevents adding 12+ minutes to every PR while ensuring async tests run when needed.
4+
# - Legacy sync integration tests: Always run on every PR with code changes
5+
# - Legacy async integration tests: Only run when AIO changes detected or "run-async-tests" label
6+
# - V9 unit tests: Always run on every PR with code changes
7+
# - V9 integration tests (sync + async): Only run when PR has the "run_pyatlan_v9_integration_test" label
98

109
on:
1110
pull_request:
@@ -117,6 +116,28 @@ jobs:
117116
echo "⏭️ No AIO changes detected and no manual trigger label found"
118117
fi
119118
119+
check-v9-integration-label:
120+
runs-on: ubuntu-latest
121+
outputs:
122+
run-v9-integration: ${{ steps.check-label.outputs.run-v9-integration }}
123+
steps:
124+
- name: Check for v9 integration test label
125+
id: check-label
126+
run: |
127+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
128+
echo "run-v9-integration=true" >> $GITHUB_OUTPUT
129+
echo "Manual trigger: running v9 integration tests"
130+
exit 0
131+
fi
132+
133+
if echo '${{ toJson(github.event.pull_request.labels.*.name) }}' | grep -q "run_pyatlan_v9_integration_test"; then
134+
echo "run-v9-integration=true" >> $GITHUB_OUTPUT
135+
echo "Found 'run_pyatlan_v9_integration_test' label"
136+
else
137+
echo "run-v9-integration=false" >> $GITHUB_OUTPUT
138+
echo "No 'run_pyatlan_v9_integration_test' label found, skipping v9 integration tests"
139+
fi
140+
120141
qa-checks-and-unit-tests:
121142
needs: [check-code-changes, vulnerability-scan]
122143
if: needs.check-code-changes.outputs.has-code-changes == 'true'
@@ -260,3 +281,150 @@ jobs:
260281
# Run the async integration test file using `pytest-timer` plugin
261282
# to display only the durations of the 10 slowest tests with `pytest-sugar`
262283
command: uv run pytest ${{ matrix.test_file }} -p name_of_plugin --timer-top-n 10 --force-sugar -vv
284+
285+
# =========================================================================
286+
# V9 (msgspec) Jobs
287+
# =========================================================================
288+
289+
v9-unit-tests:
290+
needs: [check-code-changes, vulnerability-scan]
291+
if: needs.check-code-changes.outputs.has-code-changes == 'true'
292+
runs-on: ubuntu-latest
293+
strategy:
294+
matrix:
295+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
296+
297+
steps:
298+
- name: Checkout code
299+
uses: actions/checkout@v4
300+
301+
- name: Set up Python
302+
uses: actions/setup-python@v5
303+
with:
304+
python-version: ${{ matrix.python-version }}
305+
306+
- name: Install uv
307+
uses: astral-sh/setup-uv@v6
308+
309+
- name: Install dependencies
310+
run: uv sync --group dev
311+
312+
- name: Run v9 unit tests
313+
env:
314+
ATLAN_API_KEY: ${{ secrets.ATLAN_API_KEY }}
315+
ATLAN_BASE_URL: ${{ secrets.ATLAN_BASE_URL }}
316+
run: uv run pytest tests_v9/unit --force-sugar -vv
317+
318+
v9-prepare-integration-tests:
319+
needs: [check-code-changes, vulnerability-scan, check-v9-integration-label]
320+
if: >-
321+
needs.check-code-changes.outputs.has-code-changes == 'true' &&
322+
needs.check-v9-integration-label.outputs.run-v9-integration == 'true'
323+
runs-on: ubuntu-latest
324+
outputs:
325+
v9-files: ${{ steps.distribute-v9-files.outputs.v9-files }}
326+
v9-aio-files: ${{ steps.distribute-v9-aio-files.outputs.v9-aio-files }}
327+
328+
steps:
329+
- name: Checkout code
330+
uses: actions/checkout@v4
331+
332+
- name: Prepare v9 sync integration tests distribution
333+
id: distribute-v9-files
334+
run: |
335+
files=$(find tests_v9/integration -maxdepth 1 \( -name "test_*.py" -o -name "*_test.py" \) | sort | tr '\n' ' ')
336+
if [ -n "$files" ]; then
337+
json_files=$(echo "${files[@]}" | jq -R -c 'split(" ")[:-1]')
338+
else
339+
json_files="[]"
340+
fi
341+
echo "v9-files=$json_files" >> $GITHUB_OUTPUT
342+
echo "V9 sync integration test files: $json_files"
343+
344+
- name: Prepare v9 async integration tests distribution
345+
id: distribute-v9-aio-files
346+
run: |
347+
if [ -d "tests_v9/integration/aio" ]; then
348+
aio_files=$(find tests_v9/integration/aio -name "test_*.py" | sort | tr '\n' ' ')
349+
if [ -n "$aio_files" ]; then
350+
json_aio_files=$(echo "${aio_files[@]}" | jq -R -c 'split(" ")[:-1]')
351+
else
352+
json_aio_files="[]"
353+
fi
354+
else
355+
json_aio_files="[]"
356+
fi
357+
echo "v9-aio-files=$json_aio_files" >> $GITHUB_OUTPUT
358+
echo "V9 async integration test files: $json_aio_files"
359+
360+
v9-integration-tests:
361+
needs: [v9-prepare-integration-tests]
362+
if: needs.v9-prepare-integration-tests.outputs.v9-files != '[]'
363+
runs-on: ubuntu-latest
364+
strategy:
365+
fail-fast: false
366+
matrix:
367+
test_file: ${{fromJson(needs.v9-prepare-integration-tests.outputs.v9-files)}}
368+
concurrency:
369+
group: v9-${{ matrix.test_file }}
370+
371+
steps:
372+
- name: Checkout code
373+
uses: actions/checkout@v4
374+
375+
- name: Set up Python 3.11
376+
uses: actions/setup-python@v5
377+
with:
378+
python-version: "3.11"
379+
380+
- name: Install uv
381+
uses: astral-sh/setup-uv@v6
382+
383+
- name: Install dependencies
384+
run: uv sync --group dev
385+
386+
- name: Run v9 integration test
387+
env:
388+
ATLAN_API_KEY: ${{ secrets.ATLAN_API_KEY }}
389+
ATLAN_BASE_URL: ${{ secrets.ATLAN_BASE_URL }}
390+
uses: nick-fields/retry@v3
391+
with:
392+
max_attempts: 3
393+
timeout_minutes: 10
394+
command: uv run pytest ${{ matrix.test_file }} -p name_of_plugin --timer-top-n 10 --force-sugar -vv
395+
396+
v9-async-integration-tests:
397+
needs: [v9-prepare-integration-tests]
398+
if: needs.v9-prepare-integration-tests.outputs.v9-aio-files != '[]'
399+
runs-on: ubuntu-latest
400+
strategy:
401+
fail-fast: false
402+
matrix:
403+
test_file: ${{fromJson(needs.v9-prepare-integration-tests.outputs.v9-aio-files)}}
404+
concurrency:
405+
group: v9-async-${{ matrix.test_file }}
406+
407+
steps:
408+
- name: Checkout code
409+
uses: actions/checkout@v4
410+
411+
- name: Set up Python 3.11
412+
uses: actions/setup-python@v5
413+
with:
414+
python-version: "3.11"
415+
416+
- name: Install uv
417+
uses: astral-sh/setup-uv@v6
418+
419+
- name: Install dependencies
420+
run: uv sync --group dev
421+
422+
- name: Run v9 async integration test
423+
env:
424+
ATLAN_API_KEY: ${{ secrets.ATLAN_API_KEY }}
425+
ATLAN_BASE_URL: ${{ secrets.ATLAN_BASE_URL }}
426+
uses: nick-fields/retry@v3
427+
with:
428+
max_attempts: 3
429+
timeout_minutes: 15
430+
command: uv run pytest ${{ matrix.test_file }} -p name_of_plugin --timer-top-n 10 --force-sugar -vv

pyatlan/client/aio/batch.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -410,11 +410,10 @@ def _track_response(self, response: AssetMutationResponse, sent: list[Asset]):
410410

411411
@staticmethod
412412
def __track(tracker: List[Asset], candidate: Asset):
413-
if (
414-
isinstance(candidate, AtlasGlossaryTerm)
415-
or getattr(candidate, "type_name", None) == "AtlasGlossaryTerm"
416-
):
417-
asset = cast(Asset, type(candidate).ref_by_guid(candidate.guid))
413+
if isinstance(candidate, AtlasGlossaryTerm):
414+
# trim_to_required for AtlasGlossaryTerm requires anchor
415+
# which is not include in AssetMutationResponse
416+
asset = cast(Asset, AtlasGlossaryTerm.ref_by_guid(candidate.guid))
418417
else:
419418
asset = candidate.trim_to_required()
420419
asset.name = candidate.name

pyatlan/client/aio/client.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757
CONNECTION_RETRY,
5858
VERSION,
5959
AtlanClient,
60-
_is_msgspec_struct,
6160
get_python_version,
6261
)
6362
from pyatlan.client.common import ImpersonateUser
@@ -489,12 +488,6 @@ async def _create_params(
489488
params["data"] = await async_request.json()
490489
elif api.consumes == APPLICATION_ENCODED_FORM:
491490
params["data"] = request_obj
492-
elif _is_msgspec_struct(request_obj):
493-
from pyatlan_v9.model.core import AtlanRequest as V9AtlanRequest
494-
495-
params["data"] = V9AtlanRequest(
496-
instance=request_obj, client=self
497-
).json()
498491
else:
499492
params["data"] = json.dumps(request_obj)
500493
return params

pyatlan/client/asset.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2570,11 +2570,10 @@ def _track_response(self, response: AssetMutationResponse, sent: list[Asset]):
25702570

25712571
@staticmethod
25722572
def __track(tracker: List[Asset], candidate: Asset):
2573-
if (
2574-
isinstance(candidate, AtlasGlossaryTerm)
2575-
or getattr(candidate, "type_name", None) == "AtlasGlossaryTerm"
2576-
):
2577-
asset = cast(Asset, type(candidate).ref_by_guid(candidate.guid))
2573+
if isinstance(candidate, AtlasGlossaryTerm):
2574+
# trim_to_required for AtlasGlossaryTerm requires anchor
2575+
# which is not include in AssetMutationResponse
2576+
asset = cast(Asset, AtlasGlossaryTerm.ref_by_guid(candidate.guid))
25782577
else:
25792578
asset = candidate.trim_to_required()
25802579
asset.name = candidate.name
@@ -2674,8 +2673,8 @@ def _build_category_dict(self, stub_dict: Dict[str, AtlasGlossaryCategory]):
26742673
full_parent = self._categories.get(parent_guid, stub_dict[parent_guid])
26752674
children: List[AtlasGlossaryCategory] = (
26762675
[]
2677-
if not full_parent.children_categories
2678-
else list(full_parent.children_categories)
2676+
if full_parent.children_categories is None
2677+
else full_parent.children_categories.copy()
26792678
)
26802679
if category not in children:
26812680
children.append(category)

pyatlan/client/atlan.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,16 +96,6 @@
9696
request_id_var = ContextVar("request_id", default=None)
9797

9898

99-
def _is_msgspec_struct(obj: Any) -> bool:
100-
"""Check if *obj* is a msgspec.Struct instance (without importing msgspec at module level)."""
101-
try:
102-
import msgspec
103-
104-
return isinstance(obj, msgspec.Struct)
105-
except ImportError:
106-
return False
107-
108-
10999
def get_adapter() -> logging.LoggerAdapter:
110100
"""
111101
This function creates a LoggerAdapter that will provide the requestid from the ContextVar request_id_var
@@ -872,12 +862,6 @@ def _create_params(
872862
params["data"] = AtlanRequest(instance=request_obj, client=self).json()
873863
elif api.consumes == APPLICATION_ENCODED_FORM:
874864
params["data"] = request_obj
875-
elif _is_msgspec_struct(request_obj):
876-
from pyatlan_v9.model.core import AtlanRequest as V9AtlanRequest
877-
878-
params["data"] = V9AtlanRequest(
879-
instance=request_obj, client=self
880-
).json()
881865
else:
882866
params["data"] = json.dumps(request_obj)
883867
return params

0 commit comments

Comments
 (0)