diff --git a/.github/workflows/python-client.yml b/.github/workflows/python-client.yml new file mode 100644 index 00000000000..751449cd2d4 --- /dev/null +++ b/.github/workflows/python-client.yml @@ -0,0 +1,63 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Python Client Tests + +on: + push: + paths: + - 'intg/src/main/python/**' + - 'intg/src/test/python/**' + - 'intg/pom.xml' + - '.github/workflows/python-client.yml' + pull_request: + paths: + - 'intg/src/main/python/**' + - 'intg/src/test/python/**' + - 'intg/pom.xml' + - '.github/workflows/python-client.yml' + +jobs: + python-client-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install dependencies + run: | + cd intg/src/main/python + python -m pip install --upgrade pip + pip install -r requirements.txt -r requirements-dev.txt + pip install -e . + + - name: Run unit tests + run: | + cd intg + PYTHONPATH=src/main/python python -B src/test/python/test_atlas_client.py + + - name: Lint and type-check + run: | + cd intg/src/main/python + make lint mypy + + - name: Build and validate package + run: | + cd intg/src/main/python + make build check diff --git a/.gitignore b/.gitignore index a25f185f9a7..59be9e98823 100755 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,10 @@ test-output **/build **/dist **/apache_atlas.egg-info +**/.tox +**/.venv* +**/__pycache__ +**/.mypy_cache .python-version # review board diff --git a/atlas-examples/sample-app/src/main/python/README.md b/atlas-examples/sample-app/src/main/python/README.md index b199ecd6eb2..195e77a70d1 100644 --- a/atlas-examples/sample-app/src/main/python/README.md +++ b/atlas-examples/sample-app/src/main/python/README.md @@ -1,7 +1,8 @@ # Python Sample App This is a Python sample app to showcase basic functionality of Atlas. We are using Python client -to call Atlas APIs. Make sure to install Atlas Python client first before trying to run this project. Currently, compatible with Python 3.5+ +to call Atlas APIs. Make sure to install Atlas Python client first before trying to run this project. +Requires Python 3.13 or later (see `apache-atlas` 0.0.16+ on PyPI). ## Installation diff --git a/intg/pom.xml b/intg/pom.xml index 35b0f2f45a8..5e266cfc7f0 100644 --- a/intg/pom.xml +++ b/intg/pom.xml @@ -154,6 +154,31 @@ org.apache.maven.plugins maven-compiler-plugin + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + python-test + + exec + + test + + ${skipTests} + python3 + + ${project.basedir}/src/main/python + + + -B + src/test/python/test_atlas_client.py + + + + + diff --git a/intg/src/main/python/MANIFEST.in b/intg/src/main/python/MANIFEST.in new file mode 100644 index 00000000000..39f94ab6b14 --- /dev/null +++ b/intg/src/main/python/MANIFEST.in @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +include README.md +include requirements.txt +include LICENSE diff --git a/intg/src/main/python/Makefile b/intg/src/main/python/Makefile index ccfe604374f..d74b6d810ca 100644 --- a/intg/src/main/python/Makefile +++ b/intg/src/main/python/Makefile @@ -16,16 +16,24 @@ # limitations under the License. ########################################################################## -.PHONY: test_unit +PYTHONPATH ?= . +TEST_FILE ?= ../../test/python/test_atlas_client.py + +.PHONY: test_unit lint mypy test build check + test_unit: - python3 -b -m pytest tests + PYTHONPATH=$(PYTHONPATH) python3 -B $(TEST_FILE) lint: - python3 -m flake8 + python3 -m flake8 apache_atlas -.PHONY: mypy mypy: - mypy --ignore-missing-imports --follow-imports=skip --strict-optional --warn-no-return . + python3 -m mypy --ignore-missing-imports --follow-imports=skip --strict-optional --warn-no-return apache_atlas + +test: test_unit lint mypy + +build: + python3 -m build -.PHONY: test -test: lint mypy +check: build + twine check dist/* diff --git a/intg/src/main/python/README.md b/intg/src/main/python/README.md index a53a1e1bd07..b9fb8d17711 100644 --- a/intg/src/main/python/README.md +++ b/intg/src/main/python/README.md @@ -1,162 +1,105 @@ # Apache Atlas Python Client -Python library for Apache Atlas. +`apache-atlas` is the official Python client package for Apache Atlas. +It provides typed helpers for entity, type definition, discovery, glossary, +lineage, relationship, and admin APIs. + +## Requirements + +- Python 3.13 or later +- Apache Atlas server reachable from your Python process ## Installation -Use the package manager [pip](https://pip.pypa.io/en/stable/) to install Python client for Apache Atlas. +Install the client from PyPI: ```bash -> pip install apache-atlas +pip install apache-atlas ``` -Verify if apache-atlas client is installed: -```bash -> pip list +For Kerberos authentication, install the optional Kerberos dependency: -Package Version ------------- --------- -apache-atlas 0.0.15 +```bash +pip install requests-kerberos ``` -## Usage +Verify the installed package: -```python atlas_example.py``` -```python -# atlas_example.py - -import time +```bash +python -m pip show apache-atlas +``` -from apache_atlas.client.base_client import AtlasClient -from apache_atlas.model.instance import AtlasEntity, AtlasEntityWithExtInfo, AtlasEntitiesWithExtInfo, AtlasRelatedObjectId -from apache_atlas.model.enums import EntityOperation +## Supported Clients +`AtlasClient` is the main entry point. It exposes the following API clients: -## Step 1: create a client to connect to Apache Atlas server -client = AtlasClient('http://localhost:21000', ('admin', 'atlasR0cks!')) +- `entity`: create, read, update, delete entities and classifications +- `typedef`: manage type definitions +- `discovery`: search and full-text queries +- `glossary`: glossary terms and categories +- `lineage`: lineage graph queries +- `relationship`: relationship CRUD +- `admin`: server admin operations -# For Kerberos authentication, use HTTPKerberosAuth as shown below -# -# from requests_kerberos import HTTPKerberosAuth -# -# client = AtlasClient('http://localhost:21000', HTTPKerberosAuth()) +## Quick Start -# to disable SSL certificate validation (not recommended for production use!) -# -# client.session.verify = False +```python +from apache_atlas.client.base_client import AtlasClient +from apache_atlas.model.instance import AtlasEntity, AtlasEntityWithExtInfo +client = AtlasClient('http://localhost:21000', ('admin', 'atlasR0cks!')) -## Step 2: Let's create a database entity -test_db = AtlasEntity({ 'typeName': 'hive_db' }) -test_db.attributes = { 'name': 'test_db', 'clusterName': 'prod', 'qualifiedName': 'test_db@prod' } +test_db = AtlasEntity({'typeName': 'hive_db'}) +test_db.attributes = {'name': 'test_db', 'clusterName': 'prod', 'qualifiedName': 'test_db@prod'} entity_info = AtlasEntityWithExtInfo() entity_info.entity = test_db -print('Creating test_db') - resp = client.entity.create_entity(entity_info) +guid = resp.get_assigned_guid(test_db.guid) +print(f'created test_db: guid={guid}') +``` -guid_db = resp.get_assigned_guid(test_db.guid) - -print(' created test_db: guid=' + guid_db) - - -## Step 3: Let's create a table entity, and two column entities - in one call -test_tbl = AtlasEntity({ 'typeName': 'hive_table' }) -test_tbl.attributes = { 'name': 'test_tbl', 'qualifiedName': 'test_db.test_tbl@prod' } -test_tbl.relationshipAttributes = { 'db': AtlasRelatedObjectId({ 'guid': guid_db }) } - -test_col1 = AtlasEntity({ 'typeName': 'hive_column' }) -test_col1.attributes = { 'name': 'test_col1', 'type': 'string', 'qualifiedName': 'test_db.test_tbl.test_col1@prod' } -test_col1.relationshipAttributes = { 'table': AtlasRelatedObjectId({ 'guid': test_tbl.guid }) } - -test_col2 = AtlasEntity({ 'typeName': 'hive_column' }) -test_col2.attributes = { 'name': 'test_col2', 'type': 'string', 'qualifiedName': 'test_db.test_tbl.test_col2@prod' } -test_col2.relationshipAttributes = { 'table': AtlasRelatedObjectId({ 'guid': test_tbl.guid }) } - -entities_info = AtlasEntitiesWithExtInfo() -entities_info.entities = [ test_tbl, test_col1, test_col2 ] - -print('Creating test_tbl') - -resp = client.entity.create_entities(entities_info) - -guid_tbl = resp.get_assigned_guid(test_tbl.guid) -guid_col1 = resp.get_assigned_guid(test_col1.guid) -guid_col2 = resp.get_assigned_guid(test_col2.guid) - -print(' created test_tbl: guid=' + guid_tbl) -print(' created test_tbl.test_col1: guid=' + guid_col1) -print(' created test_tbl.test_col2: guid=' + guid_col2) - - -## Step 4: Let's create a view entity that feeds from the table created earlier -# Also create a lineage between the table and the view, and lineages between their columns as well -test_view = AtlasEntity({ 'typeName': 'hive_table' }) -test_view.attributes = { 'name': 'test_view', 'qualifiedName': 'test_db.test_view@prod' } -test_view.relationshipAttributes = { 'db': AtlasRelatedObjectId({ 'guid': guid_db }) } - -test_view_col1 = AtlasEntity({ 'typeName': 'hive_column' }) -test_view_col1.attributes = { 'name': 'test_col1', 'type': 'string', 'qualifiedName': 'test_db.test_view.test_col1@prod' } -test_view_col1.relationshipAttributes = { 'table': AtlasRelatedObjectId({ 'guid': test_view.guid }) } - -test_view_col2 = AtlasEntity({ 'typeName': 'hive_column' }) -test_view_col2.attributes = { 'name': 'test_col2', 'type': 'string', 'qualifiedName': 'test_db.test_view.test_col2@prod' } -test_view_col2.relationshipAttributes = { 'table': AtlasRelatedObjectId({ 'guid': test_view.guid }) } - -test_process = AtlasEntity({ 'typeName': 'hive_process' }) -test_process.attributes = { 'name': 'create_test_view', 'userName': 'admin', 'operationType': 'CREATE', 'qualifiedName': 'create_test_view@prod' } -test_process.attributes['queryText'] = 'create view test_view as select * from test_tbl' -test_process.attributes['queryPlan'] = '' -test_process.attributes['queryId'] = '' -test_process.attributes['startTime'] = int(time.time() * 1000) -test_process.attributes['endTime'] = int(time.time() * 1000) -test_process.relationshipAttributes = { 'inputs': [ AtlasRelatedObjectId({ 'guid': guid_tbl }) ], 'outputs': [ AtlasRelatedObjectId({ 'guid': test_view.guid }) ] } +## Authentication -test_col1_lineage = AtlasEntity({ 'typeName': 'hive_column_lineage' }) -test_col1_lineage.attributes = { 'name': 'test_view.test_col1 lineage', 'depenendencyType': 'read', 'qualifiedName': 'test_db.test_view.test_col1@prod' } -test_col1_lineage.attributes['query'] = { 'guid': test_process.guid } -test_col1_lineage.relationshipAttributes = { 'inputs': [ AtlasRelatedObjectId({ 'guid': guid_col1 }) ], 'outputs': [ AtlasRelatedObjectId({ 'guid': test_view_col1.guid }) ] } +Use the authentication mechanism configured for your Atlas deployment: -test_col2_lineage = AtlasEntity({ 'typeName': 'hive_column_lineage' }) -test_col2_lineage.attributes = { 'name': 'test_view.test_col2 lineage', 'depenendencyType': 'read', 'qualifiedName': 'test_db.test_view.test_col2@prod' } -test_col2_lineage.attributes['query'] = { 'guid': test_process.guid } -test_col2_lineage.relationshipAttributes = { 'inputs': [ AtlasRelatedObjectId({ 'guid': guid_col2 }) ], 'outputs': [ AtlasRelatedObjectId({ 'guid': test_view_col2.guid }) ] } +- Basic auth: pass a `(username, password)` tuple to `AtlasClient`. +- Kerberos/SPNEGO: pass `requests_kerberos.HTTPKerberosAuth()` after installing + `requests-kerberos`. +- To disable SSL certificate validation (not recommended for production): + `client.session.verify = False` -entities_info = AtlasEntitiesWithExtInfo() -entities_info.entities = [ test_process, test_col1_lineage, test_col2_lineage ] +Example Kerberos setup: -entities_info.add_referenced_entity(test_view) -entities_info.add_referenced_entity(test_view_col1) -entities_info.add_referenced_entity(test_view_col2) +```python +from requests_kerberos import HTTPKerberosAuth +from apache_atlas.client.base_client import AtlasClient -print('Creating test_view') +client = AtlasClient('http://localhost:21000', HTTPKerberosAuth()) +``` -resp = client.entity.create_entities(entities_info) +## Examples and Code References -guid_view = resp.get_assigned_guid(test_view.guid) -guid_view_col1 = resp.get_assigned_guid(test_view_col1.guid) -guid_view_col2 = resp.get_assigned_guid(test_view_col2.guid) -guid_process = resp.get_assigned_guid(test_process.guid) -guid_col1_lineage = resp.get_assigned_guid(test_col1_lineage.guid) -guid_col2_lineage = resp.get_assigned_guid(test_col2_lineage.guid) +Runnable examples and additional usage patterns: -print(' created test_view: guid=' + guid_view) -print(' created test_view.test_col1: guid=' + guid_view_col1) -print(' created test_view.test_col2: guid=' + guid_view_col1) -print(' created test_view lineage: guid=' + guid_process) -print(' created test_col1 lineage: guid=' + guid_col1_lineage) -print(' created test_col2 lineage: guid=' + guid_col2_lineage) +- Sample app: + [`atlas-examples/sample-app/src/main/python/sample_client.py`](https://github.com/apache/atlas/blob/master/atlas-examples/sample-app/src/main/python/sample_client.py) +- Entity, lineage, glossary, discovery, and typedef examples in the same directory +- Unit tests: + [`intg/src/test/python/test_atlas_client.py`](https://github.com/apache/atlas/blob/master/intg/src/test/python/test_atlas_client.py) +Run unit tests from the `intg/` module: -## Step 5: Finally, cleanup by deleting entities created above -print('Deleting entities') +```bash +PYTHONPATH=src/main/python python -B src/test/python/test_atlas_client.py +``` -resp = client.entity.delete_entities_by_guids([ guid_col1_lineage, guid_col2_lineage, guid_process, guid_view, guid_tbl, guid_db ]) +## Version 0.0.16 Highlights -deleted_count = len(resp.mutatedEntities[EntityOperation.DELETE.name]) if resp and resp.mutatedEntities and EntityOperation.DELETE.name in resp.mutatedEntities else 0 +- Requires Python 3.13 or later (`python_requires='>=3.13'`) +- Updated `requests` dependency floor to `>=2.34.2` +- Expanded unit test coverage for client and model coercion -print(' ' + str(deleted_count) + ' entities deleted') -``` -For more examples, checkout `sample-app` python project in [atlas-examples](https://github.com/apache/atlas/blob/master/atlas-examples/sample-app/src/main/python/sample_client.py) module. +**Breaking change:** version 0.0.16 and later require Python 3.13+. Use 0.0.15 +if you need an older Python runtime. diff --git a/intg/src/main/python/apache_atlas/exceptions.py b/intg/src/main/python/apache_atlas/exceptions.py index 812ba1fbd7b..17cda91101d 100644 --- a/intg/src/main/python/apache_atlas/exceptions.py +++ b/intg/src/main/python/apache_atlas/exceptions.py @@ -34,6 +34,14 @@ def __init__(self, api, response): except Exception: body = response.content - msg = "Metadata service API {method} {path} failed with status {status}. Response body is: {body}".format(**{'method': api.method, 'path': api.path, 'status': response.status_code, 'body': body}) + msg = ( + "Metadata service API {method} {path} failed with status {status}. " + "Response body is: {body}" + ).format( + method=api.method, + path=api.path, + status=response.status_code, + body=body, + ) Exception.__init__(self, msg) diff --git a/intg/src/main/python/requirements-dev.txt b/intg/src/main/python/requirements-dev.txt index 26c42959fe0..d735a5683e5 100644 --- a/intg/src/main/python/requirements-dev.txt +++ b/intg/src/main/python/requirements-dev.txt @@ -1,2 +1,5 @@ flake8 mypy +pytest +build +twine diff --git a/intg/src/main/python/requirements.txt b/intg/src/main/python/requirements.txt index 3b87e26dac3..02fa2ea38b6 100644 --- a/intg/src/main/python/requirements.txt +++ b/intg/src/main/python/requirements.txt @@ -1 +1 @@ -requests>=2.24 \ No newline at end of file +requests>=2.34.2 diff --git a/intg/src/main/python/setup.py b/intg/src/main/python/setup.py index 034b3fc758c..3291491ec9c 100644 --- a/intg/src/main/python/setup.py +++ b/intg/src/main/python/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env/python +#!/usr/bin/env python # # Licensed to the Apache Software Foundation (ASF) under one @@ -17,34 +17,42 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pathlib import Path + from setuptools import setup, find_packages -# External dependencies -requirements = ['requests>=2.24'] +HERE = Path(__file__).parent.resolve() + + +def read_requirements(filename): + with open(HERE / filename, "r", encoding="utf-8") as fh: + return [line.strip() for line in fh if line.strip() and not line.startswith("#")] + -long_description = '' -with open("README.md", "r") as fh: +with open(HERE / "README.md", "r", encoding="utf-8") as fh: long_description = fh.read() setup( name='apache-atlas', - version='0.0.15', + version='0.0.16', author="Apache Atlas", author_email='dev@atlas.apache.org', description="Apache Atlas Python Client", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/apache/atlas/tree/master/intg/src/main/python", - license='Apache LICENSE 2.0', + license="Apache-2.0", + license_files=("LICENSE",), classifiers=[ - "Programming Language :: Python :: 3.6", - "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", ], packages=find_packages(), - install_requires=requirements, + install_requires=read_requirements("requirements.txt"), include_package_data=True, zip_safe=False, keywords='atlas client, apache atlas', - python_requires='>=3.6', + python_requires='>=3.13', ) diff --git a/intg/src/main/python/tox.ini b/intg/src/main/python/tox.ini index 191a709ef35..a9cfa6e3c54 100644 --- a/intg/src/main/python/tox.ini +++ b/intg/src/main/python/tox.ini @@ -16,5 +16,20 @@ # limitations under the License. ########################################################################## +[tox] +envlist = py313 +skipsdist = true + +[testenv] +setenv = + PYTHONPATH={toxinidir} +deps = + -rrequirements.txt + -rrequirements-dev.txt +commands = + python -B ../../test/python/test_atlas_client.py + python -m flake8 apache_atlas + python -m mypy --ignore-missing-imports --follow-imports=skip --strict-optional --warn-no-return apache_atlas + [flake8] max-line-length = 120 diff --git a/intg/src/test/python/test_atlas_client.py b/intg/src/test/python/test_atlas_client.py new file mode 100644 index 00000000000..9fb97b374ff --- /dev/null +++ b/intg/src/test/python/test_atlas_client.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python + +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from unittest.mock import Mock, patch + +from apache_atlas.exceptions import AtlasServiceException +from apache_atlas.model.enums import EntityOperation, EntityStatus +from apache_atlas.model.instance import AtlasClassification, AtlasEntity, AtlasEntityWithExtInfo +from apache_atlas.utils import API, HTTPMethod, HTTPStatus, type_coerce + +try: + from apache_atlas.client.base_client import AtlasClient + from apache_atlas.client.entity import EntityClient +except ModuleNotFoundError: # requests not installed + exit() # skipping unit tests + + +class MockResponse: + def __init__(self, status_code, response=None, content=None): + self.status_code = status_code + self.response = response + self.content = content + + def json(self): + return self.response + + +class TestAtlasClient(unittest.TestCase): + URL = 'http://localhost:21000' + AUTH = ('admin', 'atlasR0cks!') + + @patch('apache_atlas.client.base_client.Session') + def test_get_entity_service_unavailable(self, mock_session): + mock_session.return_value.request.return_value = MockResponse(HTTPStatus.SERVICE_UNAVAILABLE) + + with self.assertRaises(AtlasServiceException): + AtlasClient(self.URL, self.AUTH).entity.get_entity_by_guid('guid-1') + + @patch('apache_atlas.client.base_client.Session') + def test_get_entity_success(self, mock_session): + response = { + 'entity': { + 'typeName': 'hive_db', + 'attributes': {'name': 'test_db', 'qualifiedName': 'test_db@prod'}, + }, + } + mock_session.return_value.request.return_value = MockResponse( + HTTPStatus.OK, response=response, content='Success') + + result = AtlasClient(self.URL, self.AUTH).entity.get_entity_by_guid('guid-1') + + self.assertIsInstance(result, AtlasEntityWithExtInfo) + self.assertEqual('hive_db', result.entity.typeName) + + @patch('apache_atlas.client.base_client.Session') + def test_get_entity_unexpected_status_code(self, mock_session): + mock_response = Mock() + mock_response.text = 'Internal Server Error' + mock_response.content = 'Internal Server Error' + mock_response.status_code = 500 + mock_session.return_value.request.return_value = mock_response + + with self.assertRaises(AtlasServiceException) as context: + AtlasClient(self.URL, self.AUTH).entity.get_entity_by_guid('guid-1') + + self.assertIn('500', str(context.exception)) + + def test_url_missing_format(self): + with self.assertRaises(KeyError): + API("{arg1}test{arg2}path{arg3}", HTTPMethod.GET, HTTPStatus.OK).format_path( + {'arg1': 1, 'arg2': 2}) + + def test_url_invalid_format(self): + with self.assertRaises(TypeError): + API("{}test{}path{}", HTTPMethod.GET, HTTPStatus.OK).format_path({'1', '2'}) + + def test_multipart_urljoin(self): + api = API("api/atlas/v2/entity/", HTTPMethod.GET, HTTPStatus.OK) + path = api.multipart_urljoin("api/atlas/v2/entity/", "guid", "header") + + self.assertEqual("api/atlas/v2/entity/guid/header", path) + + +class TestAtlasModel(unittest.TestCase): + def test_entity_type_coercion(self): + entity = AtlasEntity({ + 'typeName': 'hive_table', + 'attributes': {'name': 'test_tbl', 'qualifiedName': 'test_db.test_tbl@prod'}, + 'classifications': [{ + 'typeName': 'PII', + 'entityGuid': 'guid-1', + }], + }) + + entity.type_coerce_attrs() + + self.assertIsInstance(entity.classifications[0], AtlasClassification) + self.assertEqual('PII', entity.classifications[0].typeName) + + def test_type_coerce_from_dict(self): + result = type_coerce( + {'typeName': 'hive_db', 'attributes': {'name': 'test_db'}}, + AtlasEntity, + ) + + self.assertIsInstance(result, AtlasEntity) + self.assertEqual('hive_db', result.typeName) + + def test_entity_operation_enum(self): + self.assertEqual('CREATE', EntityOperation.CREATE.name) + self.assertEqual('DELETE', EntityOperation.DELETE.name) + + def test_entity_status_enum(self): + self.assertEqual(0, EntityStatus.ACTIVE.value) + + +class TestEntityClient(unittest.TestCase): + def test_get_entity_by_guid_calls_expected_api(self): + atlas_client = Mock() + atlas_client.call_api.return_value = AtlasEntityWithExtInfo({ + 'entity': {'typeName': 'hive_db', 'attributes': {'name': 'test_db'}}, + }) + + result = EntityClient(atlas_client).get_entity_by_guid( + 'guid-1', min_ext_info=True, ignore_relationships=True) + + atlas_client.call_api.assert_called_once() + call_args = atlas_client.call_api.call_args + self.assertEqual(AtlasEntityWithExtInfo, call_args[0][1]) + query_params = call_args[0][2] if len(call_args[0]) > 2 else call_args[1].get('query_params') + self.assertEqual({'minExtInfo': True, 'ignoreRelationships': True}, query_params) + self.assertIsInstance(result, AtlasEntityWithExtInfo) + + +if __name__ == '__main__': + unittest.main() diff --git a/pom.xml b/pom.xml index c0f35938789..462140854cd 100644 --- a/pom.xml +++ b/pom.xml @@ -1473,6 +1473,13 @@ **/.docz/** **/*.svg + + **/.tox/** + **/.venv*/** + **/__pycache__/** + **/.mypy_cache/** + **/*.egg-info/** +