|
27 | 27 | AtlasGlossary, |
28 | 28 | AtlasGlossaryCategory, |
29 | 29 | AtlasGlossaryTerm, |
| 30 | + Connection, |
30 | 31 | Database, |
| 32 | + DataContract, |
31 | 33 | Schema, |
32 | 34 | Table, |
33 | 35 | ) |
34 | 36 | from pyatlan.model.audit import AuditSearchRequest |
| 37 | +from pyatlan.model.contract import DataContractSpec |
35 | 38 | from pyatlan.model.core import Announcement |
36 | | -from pyatlan.model.enums import AnnouncementType, AtlanConnectorType, SortOrder, UTMTags |
| 39 | +from pyatlan.model.enums import ( |
| 40 | + AnnouncementType, |
| 41 | + AtlanConnectorType, |
| 42 | + DataContractStatus, |
| 43 | + EntityStatus, |
| 44 | + SortOrder, |
| 45 | + UTMTags, |
| 46 | +) |
37 | 47 | from pyatlan.model.fluent_search import CompoundQuery, FluentSearch |
38 | 48 | from pyatlan.model.search import ( |
39 | 49 | DSL, |
@@ -1691,3 +1701,215 @@ async def doit(asset: Asset): |
1691 | 1701 |
|
1692 | 1702 | processed_count = await client.asset.process_assets(search=search, func=doit) |
1693 | 1703 | assert processed_count == expected_count |
| 1704 | + |
| 1705 | + |
| 1706 | +# --------------------------------------------------------------------------- |
| 1707 | +# Async contract tests — client.contracts (generate_initial_spec, delete, |
| 1708 | +# delete_latest_version) |
| 1709 | +# --------------------------------------------------------------------------- |
| 1710 | + |
| 1711 | + |
| 1712 | +@pytest_asyncio.fixture(scope="module") |
| 1713 | +async def async_spec_contract( |
| 1714 | + client: AsyncAtlanClient, |
| 1715 | + table: Table, |
| 1716 | + connection: Connection, |
| 1717 | +): |
| 1718 | + """Contract created via DataContractSpec model using the async client.""" |
| 1719 | + assert table and table.qualified_name and table.type_name |
| 1720 | + |
| 1721 | + spec = DataContractSpec( |
| 1722 | + kind="DataContract", |
| 1723 | + template_version="0.0.2", |
| 1724 | + status=DataContractStatus.DRAFT, |
| 1725 | + type=table.type_name, |
| 1726 | + dataset=table.name or table.qualified_name.split("/")[-1], |
| 1727 | + data_source=connection.name, |
| 1728 | + description="Automated testing of the Python SDK - async spec-based.", |
| 1729 | + ) |
| 1730 | + contract = DataContract.creator( |
| 1731 | + asset_qualified_name=table.qualified_name, |
| 1732 | + contract_spec=spec, |
| 1733 | + ) |
| 1734 | + response = await client.asset.save(contract) |
| 1735 | + created = response.assets_created(asset_type=DataContract) |
| 1736 | + updated = response.assets_updated(asset_type=DataContract) |
| 1737 | + result = (created or updated)[0] |
| 1738 | + yield result |
| 1739 | + await delete_asset_async(client, guid=result.guid, asset_type=DataContract) |
| 1740 | + |
| 1741 | + |
| 1742 | +@pytest.mark.order(before="test_async_contract_from_spec") |
| 1743 | +async def test_async_generate_initial_spec(client: AsyncAtlanClient, table: Table): |
| 1744 | + """client.contracts.generate_initial_spec() returns parseable YAML (async).""" |
| 1745 | + assert table and table.qualified_name |
| 1746 | + |
| 1747 | + yaml_spec = await client.contracts.generate_initial_spec(table) |
| 1748 | + |
| 1749 | + assert yaml_spec, "Expected a non-empty YAML string from generate_initial_spec" |
| 1750 | + spec = DataContractSpec.from_yaml(yaml_spec) |
| 1751 | + assert spec.kind == "DataContract" |
| 1752 | + assert spec.type |
| 1753 | + assert spec.dataset |
| 1754 | + |
| 1755 | + |
| 1756 | +async def test_async_contract_from_spec( |
| 1757 | + client: AsyncAtlanClient, table: Table, async_spec_contract: DataContract |
| 1758 | +): |
| 1759 | + """Async: DataContract created from DataContractSpec has the right asset linkage.""" |
| 1760 | + assert async_spec_contract and async_spec_contract.guid |
| 1761 | + assert async_spec_contract.qualified_name |
| 1762 | + assert table.qualified_name |
| 1763 | + assert async_spec_contract.qualified_name.startswith(table.qualified_name) |
| 1764 | + assert "/contract" in async_spec_contract.qualified_name |
| 1765 | + |
| 1766 | + fetched = await client.asset.get_by_guid( |
| 1767 | + async_spec_contract.guid, asset_type=DataContract, ignore_relationships=False |
| 1768 | + ) |
| 1769 | + assert fetched |
| 1770 | + assert fetched.data_contract_asset_guid == table.guid |
| 1771 | + assert fetched.data_contract_spec or fetched.data_contract_json |
| 1772 | + assert fetched.data_contract_version and fetched.data_contract_version >= 1 |
| 1773 | + |
| 1774 | + # hasContract / dataContractLatest / dataContractLatestCertified |
| 1775 | + table_state = await client.asset.get_by_guid( |
| 1776 | + table.guid, asset_type=Table, ignore_relationships=False |
| 1777 | + ) |
| 1778 | + assert table_state.has_contract is True |
| 1779 | + assert table_state.data_contract_latest is not None |
| 1780 | + assert table_state.data_contract_latest.guid == async_spec_contract.guid |
| 1781 | + assert table_state.data_contract_latest_certified is None # DRAFT not certified |
| 1782 | + |
| 1783 | + |
| 1784 | +@pytest_asyncio.fixture(scope="module") |
| 1785 | +async def async_multi_version_contract( |
| 1786 | + client: AsyncAtlanClient, |
| 1787 | + table: Table, |
| 1788 | + connection: Connection, |
| 1789 | +): |
| 1790 | + """V1 DRAFT → V1 VERIFIED → V2 DRAFT lifecycle (async).""" |
| 1791 | + import asyncio |
| 1792 | + |
| 1793 | + assert table and table.qualified_name and table.type_name |
| 1794 | + asset_qn = table.qualified_name |
| 1795 | + dataset_name = table.name or asset_qn.split("/")[-1] |
| 1796 | + |
| 1797 | + table_with_rels = await client.asset.get_by_guid( |
| 1798 | + table.guid, asset_type=Table, ignore_relationships=False |
| 1799 | + ) |
| 1800 | + if table_with_rels.data_contract_latest: |
| 1801 | + try: |
| 1802 | + await client.contracts.delete(table_with_rels.data_contract_latest.guid) |
| 1803 | + await asyncio.sleep(2) |
| 1804 | + except Exception: |
| 1805 | + pass |
| 1806 | + |
| 1807 | + async def _save_spec(status_str: str, description: str) -> DataContract: |
| 1808 | + spec = DataContractSpec( |
| 1809 | + kind="DataContract", |
| 1810 | + template_version="0.0.2", |
| 1811 | + status=status_str, |
| 1812 | + type=table.type_name, |
| 1813 | + dataset=dataset_name, |
| 1814 | + data_source=connection.name, |
| 1815 | + description=description, |
| 1816 | + ) |
| 1817 | + contract = DataContract.creator( |
| 1818 | + asset_qualified_name=asset_qn, |
| 1819 | + contract_spec=spec, |
| 1820 | + ) |
| 1821 | + response = await client.asset.save(contract) |
| 1822 | + created = response.assets_created(asset_type=DataContract) |
| 1823 | + updated = response.assets_updated(asset_type=DataContract) |
| 1824 | + return (created or updated)[0] |
| 1825 | + |
| 1826 | + v1 = await _save_spec("draft", "E2E test - DRAFT v1") |
| 1827 | + await asyncio.sleep(3) |
| 1828 | + await _save_spec("VERIFIED", "E2E test - VERIFIED v1") |
| 1829 | + await asyncio.sleep(2) |
| 1830 | + v2 = await _save_spec("draft", "E2E test - DRAFT v2") |
| 1831 | + |
| 1832 | + yield {"v1_guid": v1.guid, "v2_guid": v2.guid} |
| 1833 | + |
| 1834 | + try: |
| 1835 | + await client.contracts.delete(v2.guid) |
| 1836 | + except Exception: |
| 1837 | + pass |
| 1838 | + |
| 1839 | + |
| 1840 | +@pytest.mark.order(before="test_async_delete_all_contract_versions") |
| 1841 | +@pytest.mark.xfail( |
| 1842 | + strict=False, |
| 1843 | + reason=( |
| 1844 | + "delete_latest_version triggers an Atlas NPE on dq-dev: " |
| 1845 | + "getRelationshipEdgeLabel() is null for contract version-chain attributes " |
| 1846 | + "not yet deployed in this environment. Remove xfail once backend is updated." |
| 1847 | + ), |
| 1848 | +) |
| 1849 | +async def test_async_delete_latest_version_restores_previous( |
| 1850 | + client: AsyncAtlanClient, table: Table, async_multi_version_contract: dict |
| 1851 | +): |
| 1852 | + """Async: deleting latest DRAFT restores VERIFIED v1 as dataContractLatest.""" |
| 1853 | + v2_guid = async_multi_version_contract["v2_guid"] |
| 1854 | + assert v2_guid |
| 1855 | + |
| 1856 | + response = await client.contracts.delete_latest_version(v2_guid) |
| 1857 | + |
| 1858 | + assert response |
| 1859 | + deleted = response.assets_deleted(asset_type=DataContract) |
| 1860 | + assert deleted and len(deleted) == 1 |
| 1861 | + assert deleted[0].guid == v2_guid |
| 1862 | + assert deleted[0].status == EntityStatus.DELETED |
| 1863 | + |
| 1864 | + table_after = await client.asset.get_by_guid( |
| 1865 | + table.guid, asset_type=Table, ignore_relationships=False |
| 1866 | + ) |
| 1867 | + assert table_after.has_contract is True |
| 1868 | + assert table_after.data_contract_latest is not None |
| 1869 | + assert table_after.data_contract_latest.guid != v2_guid |
| 1870 | + assert table_after.data_contract_latest_certified is not None |
| 1871 | + assert table_after.data_contract_latest_certified.guid != v2_guid |
| 1872 | + |
| 1873 | + |
| 1874 | +@pytest.mark.order(before="test_async_contract_from_spec") |
| 1875 | +async def test_async_delete_all_contract_versions( |
| 1876 | + client: AsyncAtlanClient, table: Table, connection: Connection |
| 1877 | +): |
| 1878 | + """Async: client.contracts.delete() purges all versions and clears asset attrs.""" |
| 1879 | + assert table and table.qualified_name and table.type_name |
| 1880 | + dataset_name = table.name or table.qualified_name.split("/")[-1] |
| 1881 | + |
| 1882 | + spec = DataContractSpec( |
| 1883 | + kind="DataContract", |
| 1884 | + template_version="0.0.2", |
| 1885 | + status=DataContractStatus.DRAFT, |
| 1886 | + type=table.type_name, |
| 1887 | + dataset=dataset_name, |
| 1888 | + data_source=connection.name, |
| 1889 | + description="Automated testing - async delete-all scenario.", |
| 1890 | + ) |
| 1891 | + contract = DataContract.creator( |
| 1892 | + asset_qualified_name=table.qualified_name, |
| 1893 | + contract_spec=spec, |
| 1894 | + ) |
| 1895 | + response = await client.asset.save(contract) |
| 1896 | + created = response.assets_created(asset_type=DataContract) |
| 1897 | + updated = response.assets_updated(asset_type=DataContract) |
| 1898 | + saved = (created or updated)[0] |
| 1899 | + assert saved and saved.guid |
| 1900 | + |
| 1901 | + del_response = await client.contracts.delete(saved.guid) |
| 1902 | + |
| 1903 | + assert del_response |
| 1904 | + deleted = del_response.assets_deleted(asset_type=DataContract) |
| 1905 | + assert deleted and len(deleted) >= 1 |
| 1906 | + assert saved.guid in {a.guid for a in deleted} |
| 1907 | + for asset in deleted: |
| 1908 | + assert asset.status == EntityStatus.DELETED |
| 1909 | + |
| 1910 | + table_after = await client.asset.get_by_guid( |
| 1911 | + table.guid, asset_type=Table, ignore_relationships=False |
| 1912 | + ) |
| 1913 | + assert not table_after.has_contract |
| 1914 | + assert table_after.data_contract_latest is None |
| 1915 | + assert table_after.data_contract_latest_certified is None |
0 commit comments