diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..b35165fe6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +.idea/ + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 762cea763..cebfe7215 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.3.0 hooks: - id: check-yaml - id: end-of-file-fixer @@ -8,19 +8,19 @@ repos: - id: debug-statements - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + rev: 5.0.4 hooks: - id: flake8 additional_dependencies: ["flake8-bandit", "flake8-bugbear"] - repo: https://github.com/pycqa/isort - rev: 5.9.3 + rev: 5.10.1 hooks: - id: isort args: ["--profile", "black", "--filter-files"] - repo: https://github.com/psf/black - rev: 21.7b0 + rev: 22.8.0 hooks: - id: black language_version: python3 diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..fca280824 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..6bc9bcc77 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +########################################################################## +# Copyright 2022 Atlan Pte, Ltd +# Copyright [2015-2021] The Apache Software Foundation +# +# 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. +########################################################################## + +.PHONY: test_unit +test_unit: + python -b -m pytest tests + +lint: + python -m flake8 pyatlan + +black: + black pyatlan + +.PHONY: mypy +mypy: + mypy --ignore-missing-imports --follow-imports=skip --strict-optional --warn-no-return . + +.PHONY: test +test: black lint mypy diff --git a/NOTICE b/NOTICE index e69de29bb..f0763ba50 100644 --- a/NOTICE +++ b/NOTICE @@ -0,0 +1,7 @@ +Pyatlan + +Copyright [2022] Atlan Pte. Ltd. +Copyright [2015-2021] The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/README.md b/README.md new file mode 100644 index 000000000..e38e28b63 --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +# Apache Atlas Python Client + +Python library for Apache Atlas. + +## Installation + +Use the package manager [pip](https://pip.pypa.io/en/stable/) to install Python client for Apache Atlas. + +```bash +> pip install apache-atlas +``` + +Verify if apache-atlas client is installed: +```bash +> pip list + +Package Version +------------ --------- +apache-atlas 0.0.11 +``` + +## Usage + +```python atlas_example.py``` +```python +# atlas_example.py + +import time + +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 + + +## Step 1: create a client to connect to Apache Atlas server +client = AtlasClient('http://localhost:21000', ('admin', 'atlasR0cks!')) + +# For Kerberos authentication, use HTTPKerberosAuth as shown below +# +# from requests_kerberos import HTTPKerberosAuth +# +# client = AtlasClient('http://localhost:21000', HTTPKerberosAuth()) + +# to disable SSL certificate validation (not recommended for production use!) +# +# client.session.verify = False + + +## 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' } + +entity_info = AtlasEntityWithExtInfo() +entity_info.entity = test_db + +print('Creating test_db') + +resp = client.entity.create_entity(entity_info) + +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 }) ] } + +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 }) ] } + +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 }) ] } + +entities_info = AtlasEntitiesWithExtInfo() +entities_info.entities = [ test_process, test_col1_lineage, test_col2_lineage ] + +entities_info.add_referenced_entity(test_view) +entities_info.add_referenced_entity(test_view_col1) +entities_info.add_referenced_entity(test_view_col2) + +print('Creating test_view') + +resp = client.entity.create_entities(entities_info) + +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) + +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) + + +## Step 5: Finally, cleanup by deleting entities created above +print('Deleting entities') + +resp = client.entity.delete_entities_by_guids([ guid_col1_lineage, guid_col2_lineage, guid_process, guid_view, guid_tbl, guid_db ]) + +deleted_count = len(resp.mutatedEntities[EntityOperation.DELETE.name]) if resp and resp.mutatedEntities and EntityOperation.DELETE.name in resp.mutatedEntities else 0 + +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. diff --git a/pyatlan/__init__.py b/pyatlan/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyatlan/client/__init__.py b/pyatlan/client/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyatlan/client/atlan.py b/pyatlan/client/atlan.py new file mode 100644 index 000000000..0a6db6eb0 --- /dev/null +++ b/pyatlan/client/atlan.py @@ -0,0 +1,111 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# Copyright [2015-2021] The Apache Software Foundation +# +# 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 copy +import json +import logging +import os + +import requests + +from pyatlan.client.index import IndexClient +from pyatlan.exceptions import AtlanServiceException +from pyatlan.utils import HTTPMethod, HTTPStatus, get_logger, type_coerce + +LOGGER = get_logger() + + +class AtlanClient: + def __init__(self, host, api_key): + self.session = requests.Session() + self.host = host + self.request_params = {"headers": {"authorization": f"Bearer {api_key}"}} + self.index_client = IndexClient(self) + + def call_api(self, api, response_type=None, query_params=None, request_obj=None): + params = copy.deepcopy(self.request_params) + path = os.path.join(self.host, api.path) + + params["headers"]["Accept"] = api.consumes + params["headers"]["Content-type"] = api.produces + + if query_params is not None: + params["params"] = query_params + + if request_obj is not None: + params["data"] = json.dumps(request_obj) + + if LOGGER.isEnabledFor(logging.DEBUG): + LOGGER.debug("------------------------------------------------------") + LOGGER.debug("Call : %s %s", api.method, path) + LOGGER.debug("Content-type : %s", api.consumes) + LOGGER.debug("Accept : %s", api.produces) + + response = None + + if api.method == HTTPMethod.GET: + response = self.session.get(path, **params) + elif api.method == HTTPMethod.POST: + response = self.session.post(path, **params) + elif api.method == HTTPMethod.PUT: + response = self.session.put(path, **params) + elif api.method == HTTPMethod.DELETE: + response = self.session.delete(path, **params) + + if response is not None: + LOGGER.debug("HTTP Status: %s", response.status_code) + + if response is None: + return None + elif response.status_code == api.expected_status: + if response_type is None: + return None + + try: + if response.content is None: + return None + if LOGGER.isEnabledFor(logging.DEBUG): + LOGGER.debug( + "<== __call_api(%s,%s,%s), result = %s", + vars(api), + params, + request_obj, + response, + ) + + LOGGER.debug(response.json()) + if response_type == str: + return json.dumps(response.json()) + + return type_coerce(response.json(), response_type) + except Exception as e: + print(e) + LOGGER.exception( + "Exception occurred while parsing response with msg: %s", e + ) + raise AtlanServiceException(api, response) from e + elif response.status_code == HTTPStatus.SERVICE_UNAVAILABLE: + LOGGER.error( + "Atlas Service unavailable. HTTP Status: %s", + HTTPStatus.SERVICE_UNAVAILABLE, + ) + + return None + else: + raise AtlanServiceException(api, response) diff --git a/pyatlan/client/bulk.py b/pyatlan/client/bulk.py new file mode 100644 index 000000000..138e50d90 --- /dev/null +++ b/pyatlan/client/bulk.py @@ -0,0 +1,27 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# +# 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. +from pyatlan.utils import API, BASE_URI, HTTPMethod, HTTPStatus + + +class BulkClient: + BULK_API = BASE_URI + "entity/bulk" + BULK_UPDATE = API(BULK_API, HTTPMethod.POST, HTTPStatus.OK) + + def __init__(self, client): + self.client = client diff --git a/pyatlan/client/entity.py b/pyatlan/client/entity.py new file mode 100644 index 000000000..c0c38cf9b --- /dev/null +++ b/pyatlan/client/entity.py @@ -0,0 +1,571 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# Copyright [2015-2021] The Apache Software Foundation +# +# 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. +from pyatlan.model.instance import ( + AtlanClassifications, + AtlanEntitiesWithExtInfo, + AtlanEntityHeader, + AtlanEntityHeaders, + AtlanEntityWithExtInfo, + EntityMutationResponse, +) +from pyatlan.utils import ( + API, + APPLICATION_JSON, + APPLICATION_OCTET_STREAM, + BASE_URI, + MULTIPART_FORM_DATA, + HTTPMethod, + HTTPStatus, + attributes_to_params, + list_attributes_to_params, +) + + +class EntityClient: + ENTITY_API = BASE_URI + "entity/" + PREFIX_ATTR = "attr:" + PREFIX_ATTR_ = "attr_" + ADMIN_API = BASE_URI + "admin/" + ENTITY_PURGE_API = ADMIN_API + "purge/" + ENTITY_BULK_API = ENTITY_API + "bulk/" + BULK_SET_CLASSIFICATIONS = "bulk/setClassifications" + BULK_HEADERS = "bulk/headers" + + # Entity APIs + GET_ENTITY_BY_GUID = API(ENTITY_API + "guid", HTTPMethod.GET, HTTPStatus.OK) + GET_ENTITY_BY_UNIQUE_ATTRIBUTE = API( + ENTITY_API + "uniqueAttribute/type", HTTPMethod.GET, HTTPStatus.OK + ) + GET_ENTITIES_BY_GUIDS = API(ENTITY_BULK_API, HTTPMethod.GET, HTTPStatus.OK) + GET_ENTITIES_BY_UNIQUE_ATTRIBUTE = API( + ENTITY_BULK_API + "uniqueAttribute/type", HTTPMethod.GET, HTTPStatus.OK + ) + GET_ENTITY_HEADER_BY_GUID = API( + ENTITY_API + "guid/{entity_guid}/header", HTTPMethod.GET, HTTPStatus.OK + ) + GET_ENTITY_HEADER_BY_UNIQUE_ATTRIBUTE = API( + ENTITY_API + "uniqueAttribute/type/{type_name}/header", + HTTPMethod.GET, + HTTPStatus.OK, + ) + + GET_AUDIT_EVENTS = API(ENTITY_API + "{guid}/audit", HTTPMethod.GET, HTTPStatus.OK) + CREATE_ENTITY = API(ENTITY_API, HTTPMethod.POST, HTTPStatus.OK) + CREATE_ENTITIES = API(ENTITY_BULK_API, HTTPMethod.POST, HTTPStatus.OK) + UPDATE_ENTITY = API(ENTITY_API, HTTPMethod.POST, HTTPStatus.OK) + UPDATE_ENTITY_BY_ATTRIBUTE = API( + ENTITY_API + "uniqueAttribute/type/", HTTPMethod.PUT, HTTPStatus.OK + ) + UPDATE_ENTITIES = API(ENTITY_BULK_API, HTTPMethod.POST, HTTPStatus.OK) + PARTIAL_UPDATE_ENTITY_BY_GUID = API( + ENTITY_API + "guid/{entity_guid}", HTTPMethod.PUT, HTTPStatus.OK + ) + DELETE_ENTITY_BY_GUID = API(ENTITY_API + "guid", HTTPMethod.DELETE, HTTPStatus.OK) + DELETE_ENTITY_BY_ATTRIBUTE = API( + ENTITY_API + "uniqueAttribute/type/", HTTPMethod.DELETE, HTTPStatus.OK + ) + DELETE_ENTITIES_BY_GUIDS = API(ENTITY_BULK_API, HTTPMethod.DELETE, HTTPStatus.OK) + PURGE_ENTITIES_BY_GUIDS = API(ENTITY_PURGE_API, HTTPMethod.PUT, HTTPStatus.OK) + + # Classification APIs + GET_CLASSIFICATIONS = API( + ENTITY_API + "guid/{guid}/classifications", HTTPMethod.GET, HTTPStatus.OK + ) + GET_FROM_CLASSIFICATION = API( + ENTITY_API + "guid/{entity_guid}/classification/{classification}", + HTTPMethod.GET, + HTTPStatus.OK, + ) + ADD_CLASSIFICATIONS = API( + ENTITY_API + "guid/{guid}/classifications", + HTTPMethod.POST, + HTTPStatus.NO_CONTENT, + ) + ADD_CLASSIFICATION = API( + ENTITY_BULK_API + "/classification", HTTPMethod.POST, HTTPStatus.NO_CONTENT + ) + ADD_CLASSIFICATION_BY_TYPE_AND_ATTRIBUTE = API( + ENTITY_API + "uniqueAttribute/type/{type_name}/classifications", + HTTPMethod.POST, + HTTPStatus.NO_CONTENT, + ) + UPDATE_CLASSIFICATIONS = API( + ENTITY_API + "guid/{guid}/classifications", + HTTPMethod.PUT, + HTTPStatus.NO_CONTENT, + ) + UPDATE_CLASSIFICATION_BY_TYPE_AND_ATTRIBUTE = API( + ENTITY_API + "uniqueAttribute/type/{type_name}/classifications", + HTTPMethod.PUT, + HTTPStatus.NO_CONTENT, + ) + UPDATE_BULK_SET_CLASSIFICATIONS = API( + ENTITY_API + BULK_SET_CLASSIFICATIONS, HTTPMethod.POST, HTTPStatus.OK + ) + DELETE_CLASSIFICATION = API( + ENTITY_API + "guid/{guid}/classification/{classification_name}", + HTTPMethod.DELETE, + HTTPStatus.NO_CONTENT, + ) + DELETE_CLASSIFICATION_BY_TYPE_AND_ATTRIBUTE = API( + ENTITY_API + "uniqueAttribute/type/{type_name}/classification/{" + "classification_name}", + HTTPMethod.DELETE, + HTTPStatus.NO_CONTENT, + ) + GET_BULK_HEADERS = API(ENTITY_API + BULK_HEADERS, HTTPMethod.GET, HTTPStatus.OK) + + # Business Attributes APIs + ADD_BUSINESS_ATTRIBUTE = API( + ENTITY_API + "guid/{entity_guid}/businessmetadata", + HTTPMethod.POST, + HTTPStatus.NO_CONTENT, + ) + ADD_BUSINESS_ATTRIBUTE_BY_NAME = API( + ENTITY_API + "guid/{entity_guid}/businessmetadata/{bm_name}", + HTTPMethod.POST, + HTTPStatus.NO_CONTENT, + ) + DELETE_BUSINESS_ATTRIBUTE = API( + ENTITY_API + "guid/{entity_guid}/businessmetadata", + HTTPMethod.DELETE, + HTTPStatus.NO_CONTENT, + ) + DELETE_BUSINESS_ATTRIBUTE_BY_NAME = API( + ENTITY_API + "guid/{entity_guid}/businessmetadata/{bm_name}", + HTTPMethod.DELETE, + HTTPStatus.NO_CONTENT, + ) + GET_BUSINESS_METADATA_TEMPLATE = API( + ENTITY_API + "businessmetadata/import/template", + HTTPMethod.GET, + HTTPStatus.OK, + APPLICATION_JSON, + APPLICATION_OCTET_STREAM, + ) + IMPORT_BUSINESS_METADATA = API( + ENTITY_API + "businessmetadata/import", + HTTPMethod.POST, + HTTPStatus.OK, + MULTIPART_FORM_DATA, + APPLICATION_JSON, + ) + + # Labels APIs + ADD_LABELS = API( + ENTITY_API + "guid/{entity_guid}/labels", HTTPMethod.PUT, HTTPStatus.NO_CONTENT + ) + ADD_LABELS_BY_UNIQUE_ATTRIBUTE = API( + ENTITY_API + "uniqueAttribute/type/{type_name}/labels", + HTTPMethod.PUT, + HTTPStatus.NO_CONTENT, + ) + SET_LABELS = API( + ENTITY_API + "guid/%s/labels", HTTPMethod.POST, HTTPStatus.NO_CONTENT + ) + SET_LABELS_BY_UNIQUE_ATTRIBUTE = API( + ENTITY_API + "uniqueAttribute/type/{entity_guid}/labels", + HTTPMethod.POST, + HTTPStatus.NO_CONTENT, + ) + DELETE_LABELS = API( + ENTITY_API + "guid/{entity_guid}/labels", + HTTPMethod.DELETE, + HTTPStatus.NO_CONTENT, + ) + DELETE_LABELS_BY_UNIQUE_ATTRIBUTE = API( + ENTITY_API + "uniqueAttribute/type/{type_name}/labels", + HTTPMethod.DELETE, + HTTPStatus.NO_CONTENT, + ) + + def __init__(self, client): + self.client = client + + def get_entity_by_guid(self, guid, min_ext_info=False, ignore_relationships=False): + query_params = { + "minExtInfo": min_ext_info, + "ignoreRelationships": ignore_relationships, + } + + return self.client.call_api( + EntityClient.GET_ENTITY_BY_GUID.format_path_with_params(guid), + AtlanEntityWithExtInfo, + query_params, + ) + + def get_entity_by_attribute( + self, type_name, uniq_attributes, min_ext_info=False, ignore_relationships=False + ): + query_params = attributes_to_params(uniq_attributes) + query_params["minExtInfo"] = min_ext_info + query_params["ignoreRelationships"] = ignore_relationships + + return self.client.call_api( + EntityClient.GET_ENTITY_BY_UNIQUE_ATTRIBUTE.format_path_with_params( + type_name + ), + AtlanEntityWithExtInfo, + query_params, + ) + + def get_entities_by_guids( + self, guids, min_ext_info=False, ignore_relationships=False + ): + query_params = { + "guid": guids, + "minExtInfo": min_ext_info, + "ignoreRelationships": ignore_relationships, + } + + return self.client.call_api( + EntityClient.GET_ENTITIES_BY_GUIDS, AtlanEntitiesWithExtInfo, query_params + ) + + def get_entities_by_attribute( + self, + type_name, + uniq_attributes_list, + min_ext_info=False, + ignore_relationships=False, + ): + query_params = list_attributes_to_params(uniq_attributes_list) + query_params["minExtInfo"] = min_ext_info + query_params["ignoreRelationships"] = ignore_relationships + + return self.client.call_api( + EntityClient.GET_ENTITIES_BY_UNIQUE_ATTRIBUTE.format_path_with_params( + type_name + ), + AtlanEntitiesWithExtInfo, + query_params, + ) + + def get_entity_header_by_guid(self, entity_guid): + return self.client.call_api( + EntityClient.GET_ENTITY_HEADER_BY_GUID.format_path( + {"entity_guid": entity_guid} + ), + AtlanEntityHeader, + ) + + def get_entity_header_by_attribute(self, type_name, uniq_attributes): + query_params = attributes_to_params(uniq_attributes) + + return self.client.call_api( + EntityClient.GET_ENTITY_HEADER_BY_UNIQUE_ATTRIBUTE.format_path( + {"type_name": type_name} + ), + AtlanEntityHeader, + query_params, + ) + + def get_audit_events(self, guid, start_key, audit_action, count): + query_params = {"startKey": start_key, "count": count} + + if audit_action is not None: + query_params["auditAction"] = audit_action + + return self.client.call_api( + EntityClient.GET_AUDIT_EVENTS.format_path({"guid": guid}), + list, + query_params, + ) + + def create_entity(self, entity): + return self.client.call_api( + EntityClient.CREATE_ENTITY, EntityMutationResponse, None, entity + ) + + def create_entities(self, atlas_entities): + return self.client.call_api( + EntityClient.CREATE_ENTITIES, EntityMutationResponse, None, atlas_entities + ) + + def update_entity(self, entity): + return self.client.call_api( + EntityClient.UPDATE_ENTITY, EntityMutationResponse, None, entity + ) + + def update_entities(self, atlas_entities): + return self.client.call_api( + EntityClient.UPDATE_ENTITY, EntityMutationResponse, None, atlas_entities + ) + + def partial_update_entity_by_guid(self, entity_guid, attr_value, attr_name): + query_params = {"name": attr_name} + + return self.client.call_api( + EntityClient.PARTIAL_UPDATE_ENTITY_BY_GUID.format_path( + {"entity_guid": entity_guid} + ), + EntityMutationResponse, + query_params, + attr_value, + ) + + def delete_entity_by_guid(self, guid): + return self.client.call_api( + EntityClient.DELETE_ENTITY_BY_GUID.format_path_with_params(guid), + EntityMutationResponse, + ) + + def delete_entity_by_attribute(self, type_name, uniq_attributes): + query_param = attributes_to_params(uniq_attributes) + + return self.client.call_api( + EntityClient.DELETE_ENTITY_BY_ATTRIBUTE.format_path_with_params(type_name), + EntityMutationResponse, + query_param, + ) + + def delete_entities_by_guids(self, guids): + query_params = {"guid": guids} + + return self.client.call_api( + EntityClient.DELETE_ENTITIES_BY_GUIDS, EntityMutationResponse, query_params + ) + + def purge_entities_by_guids(self, guids): + return self.client.call_api( + EntityClient.PURGE_ENTITIES_BY_GUIDS, EntityMutationResponse, None, guids + ) + + # Entity-classification APIs + + def get_classifications(self, guid): + return self.client.call_api( + EntityClient.GET_CLASSIFICATIONS.format_path({"guid": guid}), + AtlanClassifications, + ) + + def get_entity_classifications(self, entity_guid, classification_name): + return self.client.call_api( + EntityClient.GET_FROM_CLASSIFICATION.format_path( + {"entity_guid": entity_guid, "classification": classification_name} + ), + AtlanClassifications, + ) + + def add_classification(self, request): + self.client.call_api(EntityClient.ADD_CLASSIFICATION, None, None, request) + + def add_classifications_by_guid(self, guid, classifications): + self.client.call_api( + EntityClient.ADD_CLASSIFICATIONS.format_path({"guid": guid}), + None, + None, + classifications, + ) + + def add_classifications_by_type(self, type_name, uniq_attributes, classifications): + query_param = attributes_to_params(uniq_attributes) + + self.client.call_api( + EntityClient.ADD_CLASSIFICATION_BY_TYPE_AND_ATTRIBUTE.format_path( + {"type_name": type_name} + ), + None, + query_param, + classifications, + ) + + def update_classifications(self, guid, classifications): + self.client.call_api( + EntityClient.UPDATE_CLASSIFICATIONS.format_path({"guid": guid}), + None, + None, + classifications, + ) + + def update_classifications_by_attr( + self, type_name, uniq_attributes, classifications + ): + query_param = attributes_to_params(uniq_attributes) + + self.client.call_api( + EntityClient.UPDATE_CLASSIFICATION_BY_TYPE_AND_ATTRIBUTE.format_path( + {"type_name": type_name} + ), + None, + query_param, + classifications, + ) + + def set_classifications(self, entity_headers): + return self.client.call_api( + EntityClient.UPDATE_BULK_SET_CLASSIFICATIONS, str, None, entity_headers + ) + + def delete_classification(self, guid, classification_name): + query = {"guid": guid, "classification_name": classification_name} + + return self.client.call_api( + EntityClient.DELETE_CLASSIFICATION.format_path(query) + ) + + def delete_classifications(self, guid, classifications): + for atlas_classification in classifications: + query = {"guid": guid, "classification_name": atlas_classification.typeName} + + self.client.call_api(EntityClient.DELETE_CLASSIFICATION.format_path(query)) + + def remove_classification( + self, entity_guid, classification_name, associated_entity_guid + ): + query = {"guid": entity_guid, "classification_name": classification_name} + + self.client.call_api( + EntityClient.DELETE_CLASSIFICATION.format_path(query), + None, + None, + associated_entity_guid, + ) + + def remove_classification_by_name( + self, type_name, uniq_attributes, classification_name + ): + query_params = attributes_to_params(uniq_attributes) + query = {"type_name": type_name, "classification_name": classification_name} + + self.client.call_api( + EntityClient.DELETE_CLASSIFICATION_BY_TYPE_AND_ATTRIBUTE.format_path( + {query} + ), + None, + query_params, + ) + + def get_entity_headers(self, tag_update_start_time): + query_params = {"tagUpdateStartTime": tag_update_start_time} + + return self.client.call_api( + EntityClient.GET_BULK_HEADERS, AtlanEntityHeaders, query_params + ) + + # Business attributes APIs + def add_or_update_business_attributes( + self, entity_guid, is_overwrite, business_attributes + ): + query_params = {"isOverwrite": is_overwrite} + + self.client.call_api( + EntityClient.ADD_BUSINESS_ATTRIBUTE.format_path( + {"entity_guid": entity_guid} + ), + None, + query_params, + business_attributes, + ) + + def add_or_update_business_attributes_bm_name( + self, entity_guid, bm_name, business_attributes + ): + query = {"entity_guid": entity_guid, "bm_name": bm_name} + + self.client.call_api( + EntityClient.ADD_BUSINESS_ATTRIBUTE_BY_NAME.format_path(query), + None, + None, + business_attributes, + ) + + def remove_business_attributes(self, entity_guid, business_attributes): + self.client.call_api( + EntityClient.DELETE_BUSINESS_ATTRIBUTE.format_path( + {"entity_guid": entity_guid} + ), + None, + None, + business_attributes, + ) + + def remove_business_attributes_bm_name( + self, entity_guid, bm_name, business_attributes + ): + query = {"entity_guid": entity_guid, "bm_name": bm_name} + + self.client.call_api( + EntityClient.DELETE_BUSINESS_ATTRIBUTE_BY_NAME.format_path(query), + None, + None, + business_attributes, + ) + + # Labels APIs + def add_labels_by_guid(self, entity_guid, labels): + self.client.call_api( + EntityClient.ADD_LABELS.format_path({"entity_guid": entity_guid}), + None, + None, + labels, + ) + + def add_labels_by_name(self, type_name, uniq_attributes, labels): + query_param = attributes_to_params(uniq_attributes) + + self.client.call_api( + EntityClient.SET_LABELS_BY_UNIQUE_ATTRIBUTE.format_path( + {"type_name": type_name} + ), + None, + query_param, + labels, + ) + + def remove_labels_by_guid(self, entity_guid, labels): + self.client.call_api( + EntityClient.DELETE_LABELS.format_path({"entity_guid": entity_guid}), + None, + None, + labels, + ) + + def remove_labels_by_name(self, type_name, uniq_attributes, labels): + query_param = attributes_to_params(uniq_attributes) + + self.client.call_api( + EntityClient.DELETE_LABELS_BY_UNIQUE_ATTRIBUTE.format_path( + {"type_name": type_name} + ), + None, + query_param, + labels, + ) + + def set_labels_by_guid(self, entity_guid, labels): + self.client.call_api( + EntityClient.SET_LABELS.format_path({"entity_guid": entity_guid}), + None, + None, + labels, + ) + + def set_labels_by_name(self, type_name, uniq_attributes, labels): + query_param = attributes_to_params(uniq_attributes) + + self.client.call_api( + EntityClient.ADD_LABELS_BY_UNIQUE_ATTRIBUTE.format_path( + {"type_name": type_name} + ), + None, + query_param, + labels, + ) diff --git a/pyatlan/client/glossary.py b/pyatlan/client/glossary.py new file mode 100644 index 000000000..de0668052 --- /dev/null +++ b/pyatlan/client/glossary.py @@ -0,0 +1,454 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# Copyright [2015-2021] The Apache Software Foundation +# +# 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. +from pyatlan.model.glossary import ( + AtlanGlossary, + AtlanGlossaryCategory, + AtlanGlossaryExtInfo, + AtlanGlossaryTerm, +) +from pyatlan.utils import ( + API, + APPLICATION_JSON, + APPLICATION_OCTET_STREAM, + BASE_URI, + MULTIPART_FORM_DATA, + HTTPMethod, + HTTPStatus, +) + + +class GlossaryClient: + GLOSSARY_URI = BASE_URI + "glossary" + GLOSSARY_TERM = GLOSSARY_URI + "/term" + GLOSSARY_TERMS = GLOSSARY_URI + "/terms" + GLOSSARY_CATEGORY = GLOSSARY_URI + "/category" + GLOSSARY_CATEGORIES = GLOSSARY_URI + "/categories" + + GET_ALL_GLOSSARIES = API(GLOSSARY_URI, HTTPMethod.GET, HTTPStatus.OK) + GET_GLOSSARY_BY_GUID = API( + GLOSSARY_URI + "/{glossary_guid}", HTTPMethod.GET, HTTPStatus.OK + ) + GET_DETAILED_GLOSSARY = API( + GLOSSARY_URI + "/{glossary_guid}/detailed", HTTPMethod.GET, HTTPStatus.OK + ) + + GET_GLOSSARY_TERM = API(GLOSSARY_TERM, HTTPMethod.GET, HTTPStatus.OK) + GET_GLOSSARY_TERMS = API( + GLOSSARY_URI + "/{glossary_guid}/terms", HTTPMethod.GET, HTTPStatus.OK + ) + GET_GLOSSARY_TERMS_HEADERS = API( + GLOSSARY_URI + "/{glossary_guid}/terms/headers", HTTPMethod.GET, HTTPStatus.OK + ) + + GET_GLOSSARY_CATEGORY = API(GLOSSARY_CATEGORY, HTTPMethod.GET, HTTPStatus.OK) + GET_GLOSSARY_CATEGORIES = API( + GLOSSARY_URI + "/{glossary_guid}/categories", HTTPMethod.GET, HTTPStatus.OK + ) + GET_GLOSSARY_CATEGORIES_HEADERS = API( + GLOSSARY_URI + "/{glossary_guid}/categories/headers", + HTTPMethod.GET, + HTTPStatus.OK, + ) + + GET_CATEGORY_TERMS = API( + GLOSSARY_CATEGORY + "/{category_guid}/terms", HTTPMethod.GET, HTTPStatus.OK + ) + GET_RELATED_TERMS = API( + GLOSSARY_TERMS + "/{term_guid}/related", HTTPMethod.GET, HTTPStatus.OK + ) + GET_RELATED_CATEGORIES = API( + GLOSSARY_CATEGORY + "/{category_guid}/related", HTTPMethod.GET, HTTPStatus.OK + ) + CREATE_GLOSSARY = API(GLOSSARY_URI, HTTPMethod.POST, HTTPStatus.OK) + CREATE_GLOSSARY_TERM = API(GLOSSARY_TERM, HTTPMethod.POST, HTTPStatus.OK) + CREATE_GLOSSARY_TERMS = API(GLOSSARY_TERMS, HTTPMethod.POST, HTTPStatus.OK) + CREATE_GLOSSARY_CATEGORY = API(GLOSSARY_CATEGORY, HTTPMethod.POST, HTTPStatus.OK) + CREATE_GLOSSARY_CATEGORIES = API( + GLOSSARY_CATEGORIES, HTTPMethod.POST, HTTPStatus.OK + ) + + UPDATE_GLOSSARY_BY_GUID = API( + GLOSSARY_URI + "/{glossary_guid}", HTTPMethod.PUT, HTTPStatus.OK + ) + UPDATE_PARTIAL_GLOSSARY = API( + GLOSSARY_URI + "/{glossary_guid}/partial", HTTPMethod.PUT, HTTPStatus.OK + ) + UPDATE_GLOSSARY_TERM = API( + GLOSSARY_TERM + "/{term_guid}", HTTPMethod.PUT, HTTPStatus.OK + ) + UPDATE_PARTIAL_TERM = API( + GLOSSARY_TERM + "/{term_guid}/partial", HTTPMethod.PUT, HTTPStatus.OK + ) + + UPDATE_CATEGORY_BY_GUID = API( + GLOSSARY_CATEGORY + "/{category_guid}", HTTPMethod.PUT, HTTPStatus.OK + ) + UPDATE_PARTIAL_CATEGORY = API( + GLOSSARY_CATEGORY + "/{category_guid}/partial", HTTPMethod.PUT, HTTPStatus.OK + ) + + DELETE_GLOSSARY_BY_GUID = API( + GLOSSARY_URI + "/{glossary_guid}", HTTPMethod.DELETE, HTTPStatus.NO_CONTENT + ) + DELETE_TERM_BY_GUID = API( + GLOSSARY_TERM + "/{term_guid}", HTTPMethod.DELETE, HTTPStatus.NO_CONTENT + ) + DELETE_CATEGORY_BY_GUID = API( + GLOSSARY_CATEGORY + "/{category_guid}", HTTPMethod.DELETE, HTTPStatus.NO_CONTENT + ) + + GET_ENTITIES_ASSIGNED_WITH_TERM = API( + GLOSSARY_TERMS + "/{term_guid}/assignedEntities", HTTPMethod.GET, HTTPStatus.OK + ) + ASSIGN_TERM_TO_ENTITIES = API( + GLOSSARY_TERMS + "/{term_guid}/assignedEntities", + HTTPMethod.POST, + HTTPStatus.NO_CONTENT, + ) + DISASSOCIATE_TERM_FROM_ENTITIES = API( + GLOSSARY_TERMS + "/{term_guid}/assignedEntities", + HTTPMethod.PUT, + HTTPStatus.NO_CONTENT, + ) + + GET_IMPORT_GLOSSARY_TEMPLATE = API( + GLOSSARY_URI + "/import/template", + HTTPMethod.GET, + HTTPStatus.OK, + APPLICATION_JSON, + APPLICATION_OCTET_STREAM, + ) + IMPORT_GLOSSARY = API( + GLOSSARY_URI + "/import", + HTTPMethod.POST, + HTTPStatus.OK, + MULTIPART_FORM_DATA, + APPLICATION_JSON, + ) + + QUERY = "query" + LIMIT = "limit" + OFFSET = "offset" + STATUS = "Status" + + DEFAULT_LIMIT = -1 + DEFAULT_OFFSET = 0 + DEFAULT_SORT = "ASC" + + def __init__(self, client): + self.client = client + + def get_all_glossaries( + self, sort_by_attribute=DEFAULT_SORT, limit=DEFAULT_LIMIT, offset=DEFAULT_OFFSET + ): + query_params = { + "sort": sort_by_attribute, + GlossaryClient.LIMIT: limit, + GlossaryClient.OFFSET: offset, + } + + return self.client.call_api( + GlossaryClient.GET_ALL_GLOSSARIES, list, query_params + ) + + def get_glossary_by_guid(self, glossary_guid): + return self.client.call_api( + GlossaryClient.GET_GLOSSARY_BY_GUID.format_path( + {"glossary_guid": glossary_guid} + ), + AtlanGlossary, + ) + + def get_glossary_ext_info(self, glossary_guid): + return self.client.call_api( + GlossaryClient.GET_DETAILED_GLOSSARY.format_path( + {"glossary_guid": glossary_guid} + ), + AtlanGlossaryExtInfo, + ) + + def get_glossary_term(self, term_guid): + return self.client.call_api( + GlossaryClient.GET_GLOSSARY_TERM.format_path_with_params(term_guid), + AtlanGlossaryTerm, + ) + + def get_glossary_terms( + self, + glossary_guid, + sort_by_attribute=DEFAULT_SORT, + limit=DEFAULT_LIMIT, + offset=DEFAULT_OFFSET, + ): + query_params = { + "glossaryGuid": glossary_guid, + GlossaryClient.LIMIT: limit, + GlossaryClient.OFFSET: offset, + "sort": sort_by_attribute, + } + + return self.client.call_api( + GlossaryClient.GET_GLOSSARY_TERMS.format_path( + {"glossary_guid": glossary_guid} + ), + list, + query_params, + ) + + def get_glossary_term_headers( + self, glossary_guid, sort_by_attribute, limit, offset + ): + query_params = { + "glossaryGuid": glossary_guid, + GlossaryClient.LIMIT: limit, + GlossaryClient.OFFSET: offset, + "sort": sort_by_attribute, + } + + return self.client.call_api( + GlossaryClient.GET_GLOSSARY_TERMS_HEADERS.format_path( + {"glossary_guid": glossary_guid} + ), + list, + query_params, + ) + + def get_glossary_category(self, category_guid): + return self.client.call_api( + GlossaryClient.GET_GLOSSARY_CATEGORY.format_path_with_params(category_guid), + AtlanGlossaryCategory, + ) + + def get_glossary_categories(self, glossary_guid, sort_by_attribute, limit, offset): + query_params = { + "glossaryGuid": glossary_guid, + GlossaryClient.LIMIT: limit, + GlossaryClient.OFFSET: offset, + "sort": sort_by_attribute, + } + + return self.client.call_api( + GlossaryClient.GET_GLOSSARY_CATEGORIES.format_path( + {"glossary_guid": glossary_guid} + ), + list, + query_params, + ) + + def get_glossary_category_headers( + self, glossary_guid, sort_by_attribute, limit, offset + ): + query_params = { + "glossaryGuid": glossary_guid, + GlossaryClient.LIMIT: limit, + GlossaryClient.OFFSET: offset, + "sort": sort_by_attribute, + } + + return self.client.call_api( + GlossaryClient.GET_GLOSSARY_CATEGORIES_HEADERS.format_path( + {"glossary_guid": glossary_guid} + ), + list, + query_params, + ) + + def get_category_terms(self, category_guid, sort_by_attribute, limit, offset): + query_params = { + "categoryGuid": category_guid, + GlossaryClient.LIMIT: limit, + GlossaryClient.OFFSET: offset, + "sort": sort_by_attribute, + } + + return self.client.call_api( + GlossaryClient.GET_CATEGORY_TERMS.format_path( + {"category_guid": category_guid} + ), + list, + query_params, + ) + + def get_related_terms(self, term_guid, sort_by_attribute, limit, offset): + query_params = { + "termGuid": term_guid, + GlossaryClient.LIMIT: limit, + GlossaryClient.OFFSET: offset, + "sort": sort_by_attribute, + } + + return self.client.call_api( + GlossaryClient.GET_RELATED_TERMS.format_path({"term_guid": term_guid}), + dict, + query_params, + ) + + def get_related_categories(self, category_guid, sort_by_attribute, limit, offset): + query_params = { + GlossaryClient.LIMIT: limit, + GlossaryClient.OFFSET: offset, + "sort": sort_by_attribute, + } + + return self.client.call_api( + GlossaryClient.GET_RELATED_CATEGORIES.format_path( + {"category_guid": category_guid} + ), + dict, + query_params, + ) + + def create_glossary(self, glossary): + return self.client.call_api( + GlossaryClient.CREATE_GLOSSARY, AtlanGlossary, None, glossary + ) + + def create_glossary_term(self, glossary_term): + return self.client.call_api( + GlossaryClient.CREATE_GLOSSARY_TERM, AtlanGlossaryTerm, None, glossary_term + ) + + def create_glossary_terms(self, glossary_terms): + return self.client.call_api( + GlossaryClient.CREATE_GLOSSARY_TERMS, list, None, glossary_terms + ) + + def create_glossary_category(self, glossary_category): + return self.client.call_api( + GlossaryClient.CREATE_GLOSSARY_CATEGORY, + AtlanGlossaryCategory, + None, + glossary_category, + ) + + def create_glossary_categories(self, glossary_categories): + return self.client.call_api( + GlossaryClient.CREATE_GLOSSARY_CATEGORIES, list, glossary_categories + ) + + def update_glossary_by_guid(self, glossary_guid, updated_glossary): + return self.client.call_api( + GlossaryClient.UPDATE_GLOSSARY_BY_GUID.format_path( + {"glossary_guid": glossary_guid} + ), + AtlanGlossary, + None, + updated_glossary, + ) + + def partial_update_glossary_by_guid(self, glossary_guid, attributes): + return self.client.call_api( + GlossaryClient.UPDATE_PARTIAL_GLOSSARY.format_path( + {"glossary_guid": glossary_guid} + ), + AtlanGlossary, + attributes, + ) + + def update_glossary_term_by_guid(self, term_guid, glossary_term): + return self.client.call_api( + GlossaryClient.UPDATE_GLOSSARY_TERM.format_path({"term_guid": term_guid}), + AtlanGlossaryTerm, + None, + glossary_term, + ) + + def partial_update_term_by_guid(self, term_guid, attributes): + return self.client.call_api( + GlossaryClient.UPDATE_PARTIAL_TERM.format_path({"term_guid": term_guid}), + AtlanGlossaryTerm, + attributes, + ) + + def update_glossary_category_by_guid(self, category_guid, glossary_category): + return self.client.call_api( + GlossaryClient.UPDATE_CATEGORY_BY_GUID.format_path( + {"category_guid": category_guid} + ), + AtlanGlossaryCategory, + glossary_category, + ) + + def partial_update_category_by_guid(self, category_guid, attributes): + return self.client.call_api( + GlossaryClient.UPDATE_PARTIAL_CATEGORY.format_path( + {"category_guid": category_guid} + ), + AtlanGlossaryCategory, + None, + attributes, + ) + + def delete_glossary_by_guid(self, glossary_guid): + return self.client.call_api( + GlossaryClient.DELETE_GLOSSARY_BY_GUID.format_path( + {"glossary_guid": glossary_guid} + ) + ) + + def delete_glossary_term_by_guid(self, term_guid): + return self.client.call_api( + GlossaryClient.DELETE_TERM_BY_GUID.format_path({"term_guid": term_guid}) + ) + + def delete_glossary_category_by_guid(self, category_guid): + return self.client.call_api( + GlossaryClient.DELETE_CATEGORY_BY_GUID.format_path( + {"category_guid": category_guid} + ) + ) + + def get_entities_assigned_with_term( + self, term_guid, sort_by_attribute, limit, offset + ): + query_params = { + "termGuid": term_guid, + GlossaryClient.LIMIT: limit, + GlossaryClient.OFFSET: offset, + "sort": sort_by_attribute, + } + + return self.client.call_api( + GlossaryClient.GET_ENTITIES_ASSIGNED_WITH_TERM.format_path( + {"term_guid": term_guid} + ), + list, + query_params, + ) + + def assign_term_to_entities(self, term_guid, related_object_ids): + return self.client.call_api( + GlossaryClient.ASSIGN_TERM_TO_ENTITIES.format_path( + {"term_guid": term_guid} + ), + None, + None, + related_object_ids, + ) + + def disassociate_term_from_entities(self, term_guid, related_object_ids): + return self.client.call_api( + GlossaryClient.DISASSOCIATE_TERM_FROM_ENTITIES.format_path( + {"term_guid": term_guid} + ), + None, + None, + related_object_ids, + ) diff --git a/pyatlan/client/index.py b/pyatlan/client/index.py new file mode 100644 index 000000000..1e8b236ad --- /dev/null +++ b/pyatlan/client/index.py @@ -0,0 +1,95 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# +# 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. +from pyatlan.model.index_search_criteria import IndexSearchRequest +from pyatlan.utils import API, BASE_URI, HTTPMethod, HTTPStatus + +CRITERIA = { + "dsl": { + "from": 6, + "size": 2, + "post_filter": {"bool": {}}, + "query": { + "bool": { + "must": [ + { + "term": { + "announcementTitle": { + "value": "Monte", + "boost": 1.0, + "case_insensitive": True, + } + } + }, + { + "term": { + "announcementTitle": { + "value": "Carlo", + "boost": 1.0, + "case_insensitive": True, + } + } + }, + { + "term": { + "announcementTitle": { + "value": "Incident", + "boost": 1.0, + "case_insensitive": True, + } + } + }, + ] + } + }, + }, + "attributes": [ + "announcementMessage", + "announcementTitle", + "announcementType", + "announcementUpdatedAt", + "announcementUpdatedBy", + "databaseName", + "schemaName", + ], +} + + +class IndexClient: + INDEX_API = BASE_URI + "search/indexsearch" + INDEX_SEARCH = API(INDEX_API, HTTPMethod.POST, HTTPStatus.OK) + + def __init__(self, client): + self.client = client + + def index_search(self, criteria: IndexSearchRequest): + while True: + start = criteria.dsl.from_ + size = criteria.dsl.size + response = self.client.call_api( + IndexClient.INDEX_SEARCH, dict, request_obj=criteria.to_json() + ) + approximate_count = response["approximateCount"] + if "entities" in response: + yield from response["entities"] + if start + size < approximate_count: + criteria.dsl.from_ = start + size + else: + break + else: + break diff --git a/pyatlan/client/typedef.py b/pyatlan/client/typedef.py new file mode 100644 index 000000000..4a290b6b7 --- /dev/null +++ b/pyatlan/client/typedef.py @@ -0,0 +1,188 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# Copyright [2015-2021] The Apache Software Foundation +# +# 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. +from pyatlan.model.misc import SearchFilter +from pyatlan.model.typedef import ( + AtlanBusinessMetadataDef, + AtlanClassificationDef, + AtlanEntityDef, + AtlanEnumDef, + AtlanRelationshipDef, + AtlanStructDef, + AtlanTypesDef, +) +from pyatlan.utils import API, BASE_URI, HTTPMethod, HTTPStatus + + +class TypeDefClient: + TYPES_API = BASE_URI + "types/" + TYPEDEFS_API = TYPES_API + "typedefs/" + TYPEDEF_BY_NAME = TYPES_API + "typedef.py/name" + TYPEDEF_BY_GUID = TYPES_API + "typedef.py/guid" + GET_BY_NAME_TEMPLATE = TYPES_API + "{path_type}/name/{name}" + GET_BY_GUID_TEMPLATE = TYPES_API + "{path_type}/guid/{guid}" + + GET_TYPEDEF_BY_NAME = API(TYPEDEF_BY_NAME, HTTPMethod.GET, HTTPStatus.OK) + GET_TYPEDEF_BY_GUID = API(TYPEDEF_BY_GUID, HTTPMethod.GET, HTTPStatus.OK) + GET_ALL_TYPE_DEFS = API(TYPEDEFS_API, HTTPMethod.GET, HTTPStatus.OK) + GET_ALL_TYPE_DEF_HEADERS = API( + TYPEDEFS_API + "headers", HTTPMethod.GET, HTTPStatus.OK + ) + UPDATE_TYPE_DEFS = API(TYPEDEFS_API, HTTPMethod.PUT, HTTPStatus.OK) + CREATE_TYPE_DEFS = API(TYPEDEFS_API, HTTPMethod.POST, HTTPStatus.OK) + DELETE_TYPE_DEFS = API(TYPEDEFS_API, HTTPMethod.DELETE, HTTPStatus.NO_CONTENT) + DELETE_TYPE_DEF_BY_NAME = API( + TYPEDEF_BY_NAME, HTTPMethod.DELETE, HTTPStatus.NO_CONTENT + ) + + def __init__(self, client): + self.client = client + + def get_all_typedefs(self, search_filter): + return self.client.call_api( + TypeDefClient.GET_ALL_TYPE_DEFS, AtlanTypesDef, search_filter.params + ) + + def get_all_typedef_headers(self, search_filter): + return self.client.call_api( + TypeDefClient.GET_ALL_TYPE_DEF_HEADERS, list, search_filter.params + ) + + def type_with_guid_exists(self, guid): + try: + obj = self.client.call_api( + TypeDefClient.GET_TYPEDEF_BY_GUID.format_path_with_params(guid), str + ) + + if obj is None: + return False + except Exception: + return False + + return True + + def type_with_name_exists(self, name): + try: + obj = self.client.call_api( + TypeDefClient.GET_TYPEDEF_BY_NAME.format_path_with_params(name), str + ) + + if obj is None: + return False + except Exception: + return False + + return True + + def get_enumdef_by_name(self, name): + return self.__get_typedef_by_name(name, AtlanEnumDef) + + def get_enumdef_by_guid(self, guid): + return self.__get_typedef_by_guid(guid, AtlanEntityDef) + + def get_structdef_by_name(self, name): + return self.__get_typedef_by_name(name, AtlanStructDef) + + def get_structdef_by_guid(self, guid): + return self.__get_typedef_by_guid(guid, AtlanStructDef) + + def get_classificationdef_by_name(self, name): + return self.__get_typedef_by_name(name, AtlanClassificationDef) + + def get_classificationdef_by_guid(self, guid): + return self.__get_typedef_by_guid(guid, AtlanClassificationDef) + + def get_entitydef_by_name(self, name): + return self.__get_typedef_by_name(name, AtlanEntityDef) + + def get_entitydef_by_guid(self, guid): + return self.__get_typedef_by_guid(guid, AtlanEntityDef) + + def get_relationshipdef_by_name(self, name): + return self.__get_typedef_by_name(name, AtlanRelationshipDef) + + def get_relationshipdef_by_guid(self, guid): + return self.__get_typedef_by_guid(guid, AtlanRelationshipDef) + + def get_businessmetadatadef_by_name(self, name): + return self.__get_typedef_by_name(name, AtlanBusinessMetadataDef) + + def get_businessmetadatadef_by_guid(self, guid): + return self.__get_typedef_by_guid(guid, AtlanBusinessMetadataDef) + + def get_businessmetadatadef_by_display_name(self, display_name): + type_defs = self.get_all_typedefs(SearchFilter()) + return next( + ( + business_meta_data + for business_meta_data in type_defs.businessMetadataDefs + if business_meta_data.displayName == display_name + ), + None, + ) + + def create_atlas_typedefs(self, types_def): + return self.client.call_api( + TypeDefClient.CREATE_TYPE_DEFS, AtlanTypesDef, None, types_def + ) + + def update_atlas_typedefs(self, types_def): + return self.client.call_api( + TypeDefClient.UPDATE_TYPE_DEFS, AtlanTypesDef, None, types_def + ) + + def delete_atlas_typedefs(self, types_def): + return self.client.call_api(TypeDefClient.DELETE_TYPE_DEFS, None, types_def) + + def delete_type_by_name(self, type_name): + return self.client.call_api( + TypeDefClient.DELETE_TYPE_DEF_BY_NAME.format_path_with_params(type_name) + ) + + def __get_typedef_by_name(self, name, typedef_class): + path_type = self.__get_path_for_type(typedef_class) + api = API(TypeDefClient.GET_BY_NAME_TEMPLATE, HTTPMethod.GET, HTTPStatus.OK) + + return self.client.call_api( + api.format_path({"path_type": path_type, "name": name}), typedef_class + ) + + def __get_typedef_by_guid(self, guid, typedef_class): + path_type = self.__get_path_for_type(typedef_class) + api = API(TypeDefClient.GET_BY_GUID_TEMPLATE, HTTPMethod.GET, HTTPStatus.OK) + + return self.client.call_api( + api.format_path({"path_type": path_type, "guid": guid}), typedef_class + ) + + def __get_path_for_type(self, typedef_class): + if issubclass(AtlanEnumDef, typedef_class): + return "enumdef" + if issubclass(AtlanEntityDef, typedef_class): + return "entitydef" + if issubclass(AtlanClassificationDef, typedef_class): + return "classificationdef" + if issubclass(AtlanStructDef, typedef_class): + return "structdef" + if issubclass(AtlanRelationshipDef, typedef_class): + return "relationshipdef" + if issubclass(AtlanBusinessMetadataDef, typedef_class): + return "businessmetadatadef" + + return "" diff --git a/pyatlan/exceptions.py b/pyatlan/exceptions.py new file mode 100644 index 000000000..6d31a1d45 --- /dev/null +++ b/pyatlan/exceptions.py @@ -0,0 +1,49 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# Copyright [2015-2021] The Apache Software Foundation +# +# 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. +class AtlanServiceException(Exception): + """Exception raised for errors in API calls. + Attributes: + api -- api endpoint which caused the error + response -- response from the server + """ + + def __init__(self, api, response): + msg = "" + + if api: + msg = "Metadata service API {method} : {path} failed".format( + **{"method": api.method, "path": api.path} + ) + + if response.content is not None: + status = response.status_code if response.status_code is not None else -1 + msg = ( + "Metadata service API with url {url} and method {method} : failed with status {status} and " + "Response Body is :{response}".format( + **{ + "url": response.url, + "method": api.method, + "status": status, + "response": response.json(), + } + ) + ) + + Exception.__init__(self, msg) diff --git a/pyatlan/model/__init__.py b/pyatlan/model/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyatlan/model/enums.py b/pyatlan/model/enums.py new file mode 100644 index 000000000..6d353bd7f --- /dev/null +++ b/pyatlan/model/enums.py @@ -0,0 +1,149 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# Copyright [2015-2021] The Apache Software Foundation +# +# 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 enum + + +class AtlanTermAssignmentStatus(enum.Enum): + DISCOVERED = 0 + PROPOSED = 1 + IMPORTED = 2 + VALIDATED = 3 + DEPRECATED = 4 + OBSOLETE = 5 + OTHER = 6 + + +class AtlanTermRelationshipStatus(enum.Enum): + DRAFT = 0 + ACTIVE = 1 + DEPRECATED = 2 + OBSOLETE = 3 + OTHER = 99 + + +class TypeCategory(enum.Enum): + PRIMITIVE = 0 + OBJECT_ID_TYPE = 1 + ENUM = 2 + STRUCT = 3 + CLASSIFICATION = 4 + ENTITY = 5 + ARRAY = 6 + MAP = 7 + RELATIONSHIP = 8 + BUSINESS_METADATA = 9 + + +class Cardinality(enum.Enum): + SINGLE = 0 + LIST = 1 + SET = 2 + + +class Condition(enum.Enum): + AND = 0 + OR = 1 + + +class EntityOperation(enum.Enum): + CREATE = 0 + UPDATE = 1 + PARTIAL_UPDATE = 2 + DELETE = 3 + PURGE = 4 + + +class EntityStatus(enum.Enum): + ACTIVE = 0 + DELETED = 1 + PURGED = 2 + + +class IndexType(enum.Enum): + DEFAULT = 0 + STRING = 1 + + +class LineageDirection(enum.Enum): + INPUT = 0 + OUTPUT = 1 + BOTH = 2 + + +class Operator(enum.Enum): + LT = ("<", "lt") + GT = (">", "gt") + LTE = ("<=", "lte") + GTE = (">=", "gte") + EQ = ("=", "eq") + NEQ = ("!=", "neq") + IN = ("in", "IN") + LIKE = ("like", "LIKE") + STARTS_WITH = ("startsWith", "STARTSWITH", "begins_with", "BEGINS_WITH") + ENDS_WITH = ("endsWith", "ENDSWITH", "ends_with", "ENDS_WITH") + CONTAINS = ("contains", "CONTAINS") + NOT_CONTAINS = ("not_contains", "NOT_CONTAINS") + CONTAINS_ANY = ("containsAny", "CONTAINSANY", "contains_any", "CONTAINS_ANY") + CONTAINS_ALL = ("containsAll", "CONTAINSALL", "contains_all", "CONTAINS_ALL") + IS_NULL = ("isNull", "ISNULL", "is_null", "IS_NULL") + NOT_NULL = ("notNull", "NOTNULL", "not_null", "NOT_NULL") + + +class PropagateTags(enum.Enum): + NONE = 0 + ONE_TO_TWO = 1 + TWO_TO_ONE = 2 + BOTH = 3 + + +class QueryType(enum.Enum): + DSL = 0 + FULL_TEXT = 1 + GREMLIN = 2 + BASIC = 3 + ATTRIBUTE = 4 + RELATIONSHIP = 5 + + +class RelationshipCategory(enum.Enum): + ASSOCIATION = 0 + AGGREGATION = 1 + COMPOSITION = 2 + + +class RelationshipStatus(enum.Enum): + ACTIVE = 0 + DELETED = 1 + + +class SavedSearchType(enum.Enum): + BASIC = 0 + ADVANCED = 1 + + +class SortOrder(enum.Enum): + ASCENDING = 0 + DESCENDING = 1 + + +class SortType(enum.Enum): + NONE = 0 + ASC = 1 + DESC = 2 diff --git a/pyatlan/model/glossary.py b/pyatlan/model/glossary.py new file mode 100644 index 000000000..d2a91ae27 --- /dev/null +++ b/pyatlan/model/glossary.py @@ -0,0 +1,259 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# Copyright [2015-2021] The Apache Software Foundation +# +# 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. +from pyatlan.model.misc import AtlanBase, AtlanBaseModelObject +from pyatlan.utils import type_coerce, type_coerce_dict, type_coerce_list + + +class AtlanGlossaryBaseObject(AtlanBaseModelObject): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBaseModelObject.__init__(self, attrs) + + self.qualifiedName = attrs.get("qualifiedName") + self.name = attrs.get("name") + self.shortDescription = attrs.get("shortDescription") + self.longDescription = attrs.get("longDescription") + self.additionalAttributes = attrs.get("additionalAttributes") + self.classifications = attrs.get("classifications") + + def type_coerce_attrs(self): + # This is to avoid the circular dependencies that instance.py and glossary.py has. + import pyatlan.model.instance as instance + + super(AtlanGlossaryBaseObject, self).type_coerce_attrs() + self.classifications = type_coerce_list( + self.classifications, instance.AtlanClassification + ) + + +class AtlanGlossary(AtlanGlossaryBaseObject): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanGlossaryBaseObject.__init__(self, attrs) + + self.language = attrs.get("language") + self.usage = attrs.get("usage") + self.terms = attrs.get("terms") + self.categories = attrs.get("categories") + + def type_coerce_attrs(self): + super(AtlanGlossary, self).type_coerce_attrs() + + self.terms = type_coerce_list(self.classifications, AtlanRelatedTermHeader) + self.categories = type_coerce_list(self.categories, AtlanRelatedCategoryHeader) + + +class AtlanGlossaryExtInfo(AtlanGlossary): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanGlossary.__init__(self, attrs) + + self.termInfo = attrs.get("termInfo") + self.categoryInfo = attrs.get("categoryInfo") + + def type_coerce_attrs(self): + super(AtlanGlossaryExtInfo, self).type_coerce_attrs() + + self.termInfo = type_coerce_dict(self.termInfo, AtlanGlossaryTerm) + self.categoryInfo = type_coerce_dict(self.categoryInfo, AtlanGlossaryCategory) + + +class AtlanGlossaryCategory(AtlanGlossaryBaseObject): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanGlossaryBaseObject.__init__(self, attrs) + + # Inherited attributes from relations + self.anchor = attrs.get("anchor") + + # Category hierarchy links + self.parentCategory = attrs.get("parentCategory") + self.childrenCategories = attrs.get("childrenCategories") + + # Terms associated with this category + self.terms = attrs.get("terms") + + def type_coerce_attrs(self): + super(AtlanGlossaryCategory, self).type_coerce_attrs() + + self.anchor = type_coerce(self.anchor, AtlanGlossaryHeader) + self.parentCategory = type_coerce( + self.parentCategory, AtlanRelatedCategoryHeader + ) + self.childrenCategories = type_coerce_list( + self.childrenCategories, AtlanRelatedCategoryHeader + ) + self.terms = type_coerce_list(self.terms, AtlanRelatedTermHeader) + + +class AtlanGlossaryTerm(AtlanGlossaryBaseObject): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanGlossaryBaseObject.__init__(self, attrs) + + # Core attributes + self.examples = attrs.get("examples") + self.abbreviation = attrs.get("abbreviation") + self.usage = attrs.get("usage") + + # Attributes derived from relationships + self.anchor = attrs.get("anchor") + self.assignedEntities = attrs.get("assignedEntities") + self.categories = attrs.get("categories") + + # Related Terms + self.seeAlso = attrs.get("seeAlso") + + # Term Synonyms + self.synonyms = attrs.get("synonyms") + + # Term antonyms + self.antonyms = attrs.get("antonyms") + + # Term preference + self.preferredTerms = attrs.get("preferredTerms") + self.preferredToTerms = attrs.get("preferredToTerms") + + # Term replacements + self.replacementTerms = attrs.get("replacementTerms") + self.replacedBy = attrs.get("replacedBy") + + # Term translations + self.translationTerms = attrs.get("translationTerms") + self.translatedTerms = attrs.get("translatedTerms") + + # Term classification + self.isA = attrs.get("isA") + self.classifies = attrs.get("classifies") + + # Values for terms + self.validValues = attrs.get("validValues") + self.validValuesFor = attrs.get("validValuesFor") + + def type_coerce_attrs(self): + super(AtlanGlossaryTerm, self).type_coerce_attrs() + + # This is to avoid the circular dependencies that instance.py and glossary.py has. + import pyatlan.model.instance as instance + + self.anchor = type_coerce(self.anchor, AtlanGlossaryHeader) + self.assignedEntities = type_coerce_list( + self.assignedEntities, instance.AtlanRelatedObjectId + ) + self.categories = type_coerce_list( + self.categories, AtlanTermCategorizationHeader + ) + self.seeAlso = type_coerce_list(self.seeAlso, AtlanRelatedTermHeader) + self.synonyms = type_coerce_list(self.synonyms, AtlanRelatedTermHeader) + self.antonyms = type_coerce_list(self.antonyms, AtlanRelatedTermHeader) + self.preferredTerms = type_coerce_list( + self.preferredTerms, AtlanRelatedTermHeader + ) + self.preferredToTerms = type_coerce_list( + self.preferredToTerms, AtlanRelatedTermHeader + ) + self.replacementTerms = type_coerce_list( + self.replacementTerms, AtlanRelatedTermHeader + ) + self.replacedBy = type_coerce_list(self.replacedBy, AtlanRelatedTermHeader) + self.translationTerms = type_coerce_list( + self.translationTerms, AtlanRelatedTermHeader + ) + self.isA = type_coerce_list(self.isA, AtlanRelatedTermHeader) + self.classifies = type_coerce_list(self.classifies, AtlanRelatedTermHeader) + self.validValues = type_coerce_list(self.validValues, AtlanRelatedTermHeader) + self.validValuesFor = type_coerce_list( + self.validValuesFor, AtlanRelatedTermHeader + ) + + +class AtlanGlossaryHeader(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.glossaryGuid = attrs.get("glossaryGuid") + self.relationGuid = attrs.get("relationGuid") + self.displayText = attrs.get("displayText") + + +class AtlanRelatedCategoryHeader(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.categoryGuid = attrs.get("categoryGuid") + self.parentCategoryGuid = attrs.get("parentCategoryGuid") + self.relationGuid = attrs.get("relationGuid") + self.displayText = attrs.get("displayText") + self.description = attrs.get("description") + + +class AtlanRelatedTermHeader(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.termGuid = attrs.get("termGuid") + self.relationGuid = attrs.get("relationGuid") + self.displayText = attrs.get("displayText") + self.description = attrs.get("description") + self.expression = attrs.get("expression") + self.steward = attrs.get("steward") + self.source = attrs.get("source") + self.status = attrs.get("status") + + +class AtlanTermAssignmentHeader(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.termGuid = attrs.get("termGuid") + self.relationGuid = attrs.get("relationGuid") + self.description = attrs.get("description") + self.displayText = attrs.get("displayText") + self.expression = attrs.get("expression") + self.createdBy = attrs.get("createdBy") + self.steward = attrs.get("steward") + self.source = attrs.get("source") + self.confidence = attrs.get("confidence") + + +class AtlanTermCategorizationHeader(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.categoryGuid = attrs.get("categoryGuid") + self.relationGuid = attrs.get("relationGuid") + self.description = attrs.get("description") + self.displayText = attrs.get("displayText") + self.status = attrs.get("status") diff --git a/pyatlan/model/index_search_criteria.py b/pyatlan/model/index_search_criteria.py new file mode 100644 index 000000000..13b5662b1 --- /dev/null +++ b/pyatlan/model/index_search_criteria.py @@ -0,0 +1,46 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# +# 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. +from dataclasses import dataclass, field + + +@dataclass() +class DSL: + from_: int = 0 + size: int = 100 + post_filter: dict = field(default_factory=dict) + query: dict = field(default_factory=dict) + + def to_json(self): + json = {"from": self.from_, "size": self.size, "query": self.query} + if self.post_filter: + json["post_filter"] = self.post_filter + return json + + +@dataclass() +class IndexSearchRequest: + dsl: DSL = DSL() + attributes: list = field(default_factory=list) + relation_attributes: list = field(default_factory=list) + + def to_json(self): + json = {"dsl": self.dsl.to_json(), "attributes": self.attributes} + if self.relation_attributes: + json["relationAttributes"] = self.relation_attributes + return json diff --git a/pyatlan/model/instance.py b/pyatlan/model/instance.py new file mode 100644 index 000000000..439c24733 --- /dev/null +++ b/pyatlan/model/instance.py @@ -0,0 +1,708 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# Copyright [2015-2021] The Apache Software Foundation +# +# 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. +from pyatlan.model.enums import EntityStatus +from pyatlan.model.glossary import AtlanTermAssignmentHeader +from pyatlan.model.misc import AtlanBase, Plist, TimeBoundary, next_id +from pyatlan.utils import ( + non_null, + type_coerce, + type_coerce_dict, + type_coerce_dict_list, + type_coerce_list, +) + + +class AtlanStruct(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.typeName = attrs.get("typeName") + self.attributes = attrs.get("attributes") + + def get_attribute(self, name): + return ( + self.attributes[name] + if self.attributes is not None and name in self.attributes + else None + ) + + def set_attribute(self, name, value): + if self.attributes is None: + self.attributes = {} + + self.attributes[name] = value + + def remove_attribute(self, name): + if name and self.attributes is not None and name in self.attributes: + del self.attributes[name] + + +class AtlanEntity(AtlanStruct): + def __init__(self, attrs=None): + attrs = attrs or {} + AtlanStruct.__init__(self, attrs) + self.guid: str = attrs.get("guid") + self.homeId = attrs.get("homeId") + self.relationshipAttributes = attrs.get("relationshipAttributes") + self.classifications = attrs.get("classifications") + self.meanings = attrs.get("meanings") + self.customAttributes = attrs.get("customAttributes") + self.businessAttributes = attrs.get("businessAttributes") + self.labels = attrs.get("labels") + self.status = attrs.get("status") + self.isIncomplete = attrs.get("isIncomplete") + self.provenanceType = attrs.get("provenanceType") + self.proxy = attrs.get("proxy") + self.version = attrs.get("version") + self.createdBy = attrs.get("createdBy") + self.updatedBy = attrs.get("updatedBy") + self.createTime = attrs.get("createTime") + self.updateTime = attrs.get("updateTime") + if self.guid is None: + self.guid = next_id() + + @property + def qualified_name(self) -> str: + return self.get_attribute("qualifiedName") + + @qualified_name.setter + def qualified_name(self, value: str): + self.set_attribute("qualifiedName", value) + + @property + def replicated_from(self): + return self.get_attribute("replicatedFrom") + + @replicated_from.setter + def replicated_from(self, value): + self.set_attribute("replicatedFrom", value) + + @property + def replicated_to(self): + return self.get_attribute("replicatedTo") + + @replicated_to.setter + def replicated_to(self, value): + self.set_attribute("replicatedTo", value) + + @property + def name(self) -> str: + return self.get_attribute("name") + + @name.setter + def name(self, value: str): + self.set_attribute("name", value) + + @property + def display_name(self) -> str: + return self.get_attribute("displayName") + + @display_name.setter + def display_name(self, value: str): + self.set_attribute("displayName", value) + + @property + def description(self) -> str: + return self.get_attribute("description") + + @description.setter + def description(self, value: str): + self.set_attribute("description", value) + + @property + def user_description(self) -> str: + return self.get_attribute("userDescription") + + @user_description.setter + def user_description(self, value: str): + self.set_attribute("userDescription", value) + + @property + def tenant_id(self) -> str: + return self.get_attribute("tenantId") + + @tenant_id.setter + def tenant_id(self, value: str): + self.set_attribute("tenantId", value) + + @property + def certificate_status(self): + return self.get_attribute("certificateStatus") + + @certificate_status.setter + def certificate_status(self, value): + self.set_attribute("certificateStatus", value) + + @property + def certificate_status_message(self) -> str: + return self.get_attribute("certificateStatusMessage") + + @certificate_status_message.setter + def certificate_status_message(self, value: str): + self.set_attribute("certificateStatusMessage", value) + + @property + def certificate_updated_by(self) -> str: + return self.get_attribute("certificateUpdatedBy") + + @certificate_updated_by.setter + def certificate_updated_by(self, value: str): + self.set_attribute("certificateUpdatedBy", value) + + @property + def certificate_updated_at(self): + return self.get_attribute("certificateUpdatedAt") + + @certificate_updated_at.setter + def certificate_updated_at(self, value): + self.set_attribute("certificateUpdatedAt", value) + + @property + def announcement_title(self) -> str: + return self.get_attribute("announcementTitle") + + @announcement_title.setter + def announcement_title(self, value: str): + self.set_attribute("announcementTitle", value) + + @property + def announcement_message(self) -> str: + return self.get_attribute("announcementMessage") + + @announcement_message.setter + def announcement_message(self, value: str): + self.set_attribute("announcementMessage", value) + + @property + def announcement_type(self) -> str: + return self.get_attribute("announcementType") + + @announcement_type.setter + def announcement_type(self, value: str): + self.set_attribute("announcementType", value) + + @property + def announcement_updated_at(self): + return self.get_attribute("announcementUpdatedAt") + + @announcement_updated_at.setter + def announcement_updated_at(self, value): + self.set_attribute("announcementUpdatedAt", value) + + @property + def announcement_updated_by(self) -> str: + return self.get_attribute("announcementUpdatedBy") + + @announcement_updated_by.setter + def announcement_updated_by(self, value: str): + self.set_attribute("announcementUpdatedBy", value) + + @property + def owner_users(self) -> list[str]: + return self.get_attribute("ownerUsers") + + @owner_users.setter + def owner_users(self, value: list[str]): + self.set_attribute("ownerUsers", value) + + @property + def owner_groups(self) -> list[str]: + return self.get_attribute("ownerGroups") + + @owner_groups.setter + def owner_groups(self, value: list[str]): + self.set_attribute("ownerGroups", value) + + @property + def admin_users(self) -> list[str]: + return self.get_attribute("adminUsers") + + @admin_users.setter + def admin_users(self, value: list[str]): + self.set_attribute("adminUsers", value) + + @property + def admin_groups(self) -> list[str]: + return self.get_attribute("adminGroups") + + @admin_groups.setter + def admin_groups(self, value: list[str]): + self.set_attribute("adminGroups", value) + + @property + def viewer_users(self) -> list[str]: + return self.get_attribute("viewerUsers") + + @viewer_users.setter + def viewer_users(self, value: list[str]): + self.set_attribute("viewerUsers", value) + + @property + def viewer_groups(self) -> list[str]: + return self.get_attribute("viewerGroups") + + @viewer_groups.setter + def viewer_groups(self, value: list[str]): + self.set_attribute("viewerGroups", value) + + @property + def connector_name(self) -> str: + return self.get_attribute("connectorName") + + @connector_name.setter + def connector_name(self, value: str): + self.set_attribute("connectorName", value) + + @property + def connection_name(self) -> str: + return self.get_attribute("connectionName") + + @connection_name.setter + def connection_name(self, value: str): + self.set_attribute("connectionName", value) + + @property + def connection_qualified_name(self) -> str: + return self.get_attribute("connectionQualifiedName") + + @connection_qualified_name.setter + def connection_qualified_name(self, value: str): + self.set_attribute("connectionQualifiedName", value) + + @property + def __has_lineage(self) -> bool: + return self.get_attribute("__hasLineage") + + @__has_lineage.setter + def __has_lineage(self, value: bool): + self.set_attribute("__hasLineage", value) + + @property + def is_discoverable(self) -> bool: + return self.get_attribute("isDiscoverable") + + @is_discoverable.setter + def is_discoverable(self, value: bool): + self.set_attribute("isDiscoverable", value) + + @property + def is_editable(self) -> bool: + return self.get_attribute("isEditable") + + @is_editable.setter + def is_editable(self, value: bool): + self.set_attribute("isEditable", value) + + @property + def sub_type(self) -> str: + return self.get_attribute("subType") + + @sub_type.setter + def sub_type(self, value: str): + self.set_attribute("subType", value) + + @property + def view_score(self) -> float: + return self.get_attribute("viewScore") + + @view_score.setter + def view_score(self, value: float): + self.set_attribute("viewScore", value) + + @property + def popularity_score(self) -> float: + return self.get_attribute("popularityScore") + + @popularity_score.setter + def popularity_score(self, value: float): + self.set_attribute("popularityScore", value) + + @property + def source_owners(self) -> str: + return self.get_attribute("sourceOwners") + + @source_owners.setter + def source_owners(self, value: str): + self.set_attribute("sourceOwners", value) + + @property + def source_created_by(self) -> str: + return self.get_attribute("sourceCreatedBy") + + @source_created_by.setter + def source_created_by(self, value: str): + self.set_attribute("sourceCreatedBy", value) + + @property + def source_created_at(self): + return self.get_attribute("sourceCreatedAt") + + @source_created_at.setter + def source_created_at(self, value): + self.set_attribute("sourceCreatedAt", value) + + @property + def source_updated_at(self): + return self.get_attribute("sourceUpdatedAt") + + @source_updated_at.setter + def source_updated_at(self, value): + self.set_attribute("sourceUpdatedAt", value) + + @property + def source_updated_by(self) -> str: + return self.get_attribute("sourceUpdatedBy") + + @source_updated_by.setter + def source_updated_by(self, value: str): + self.set_attribute("sourceUpdatedBy", value) + + @property + def source_url(self) -> str: + return self.get_attribute("sourceURL") + + @source_url.setter + def source_url(self, value: str): + self.set_attribute("sourceURL", value) + + @property + def last_sync_workflow_name(self) -> str: + return self.get_attribute("lastSyncWorkflowName") + + @last_sync_workflow_name.setter + def last_sync_workflow_name(self, value: str): + self.set_attribute("lastSyncWorkflowName", value) + + @property + def last_sync_run_at(self): + return self.get_attribute("lastSyncRunAt") + + @last_sync_run_at.setter + def last_sync_run_at(self, value): + self.set_attribute("lastSyncRunAt", value) + + @property + def last_sync_run(self) -> str: + return self.get_attribute("lastSyncRun") + + @last_sync_run.setter + def last_sync_run(self, value: str): + self.set_attribute("lastSyncRun", value) + + @property + def admin_roles(self) -> list[str]: + return self.get_attribute("adminRoles") + + @admin_roles.setter + def admin_roles(self, value: list[str]): + self.set_attribute("adminRoles", value) + + def type_coerce_attrs(self): + super(AtlanEntity, self).type_coerce_attrs() + self.classifications = type_coerce_list( + self.classifications, AtlanClassification + ) + self.meanings = type_coerce_list(self.meanings, AtlanTermAssignmentHeader) + + def get_relationship_attribute(self, name): + return ( + self.relationshipAttributes[name] + if self.relationshipAttributes is not None + and name in self.relationshipAttributes + else None + ) + + def set_relationship_attribute(self, name, value): + if self.relationshipAttributes is None: + self.relationshipAttributes = {} + self.relationshipAttributes[name] = value + + def remove_relationship_attribute(self, name): + if ( + name + and self.relationshipAttributes is not None + and name in self.relationshipAttributes + ): + del self.relationshipAttributes[name] + + +class AtlanEntityExtInfo(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.referredEntities = attrs.get("referredEntities") + + def type_coerce_attrs(self): + super(AtlanEntityExtInfo, self).type_coerce_attrs() + + self.referredEntities = type_coerce_dict(self.referredEntities, AtlanEntity) + + def get_referenced_entity(self, guid): + return ( + self.referredEntities[guid] + if self.referredEntities is not None and guid in self.referredEntities + else None + ) + + def add_referenced_entity(self, entity): + if self.referredEntities is None: + self.referredEntities = {} + + self.referredEntities[entity.guid] = entity + + +class AtlanEntityWithExtInfo(AtlanEntityExtInfo): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanEntityExtInfo.__init__(self, attrs) + + self.entity = attrs.get("entity") + + def type_coerce_attrs(self): + super(AtlanEntityWithExtInfo, self).type_coerce_attrs() + + self.entity = type_coerce(self.entity, AtlanEntity) + + +class AtlanEntitiesWithExtInfo(AtlanEntityExtInfo): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanEntityExtInfo.__init__(self, attrs) + + self.entities = attrs.get("entities") + + def type_coerce_attrs(self): + super(AtlanEntitiesWithExtInfo, self).type_coerce_attrs() + + self.entities = type_coerce_list(self.entities, AtlanEntity) + + def add_entity(self, entity): + if self.entities is None: + self.entities = [] + + self.entities.append(entity) + + +class AtlanEntityHeader(AtlanStruct): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanStruct.__init__(self, attrs) + + self.guid = attrs.get("guid") + self.status = non_null(attrs.get("status"), EntityStatus.ACTIVE.name) + self.displayText = attrs.get("displayText") + self.classificationNames = attrs.get("classificationNames") + self.classifications = attrs.get("classifications") + self.meaningNames = attrs.get("meaningNames") + self.meanings = attrs.get(".meanings") + self.isIncomplete = non_null(attrs.get("isIncomplete"), False) + self.labels = attrs.get("labels") + + if self.guid is None: + self.guid = next_id() + + def type_coerce_attrs(self): + super(AtlanEntityHeader, self).type_coerce_attrs() + + self.classifications = type_coerce_list( + self.classifications, AtlanClassification + ) + self.meanings = type_coerce_list(self.meanings, AtlanTermAssignmentHeader) + + +class AtlanClassification(AtlanStruct): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanStruct.__init__(self, attrs) + + self.entityGuid = attrs.get("entityGuid") + self.entityStatus = non_null( + attrs.get("entityStatus"), EntityStatus.ACTIVE.name + ) + self.propagate = attrs.get("propagate") + self.validityPeriods = attrs.get("validityPeriods") + self.removePropagationsOnEntityDelete = attrs.get( + "removePropagationsOnEntityDelete" + ) + + def type_coerce_attrs(self): + super(AtlanClassification, self).type_coerce_attrs() + + self.validityPeriods = type_coerce_list(self.validityPeriods, TimeBoundary) + + +class AtlanObjectId(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.guid = attrs.get("guid") + self.typeName = attrs.get("typeName") + self.uniqueAttributes = attrs.get("uniqueAttributes") + + +class AtlanRelatedObjectId(AtlanObjectId): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanObjectId.__init__(self, attrs) + + self.entityStatus = attrs.get("entityStatus") + self.displayText = attrs.get("displayText") + self.relationshipType = attrs.get("relationshipType") + self.relationshipGuid = attrs.get("relationshipGuid") + self.relationshipStatus = attrs.get("relationshipStatus") + self.relationshipAttributes = attrs.get("relationshipAttributes") + + def type_coerce_attrs(self): + super(AtlanRelatedObjectId, self).type_coerce_attrs() + + self.relationshipAttributes = type_coerce( + self.relationshipAttributes, AtlanStruct + ) + + +class AtlanClassifications(Plist): + def __init__(self, attrs=None): + attrs = attrs or {} + + Plist.__init__(self, attrs) + + def type_coerce_attrs(self): + super(AtlanClassifications, self).type_coerce_attrs() + + Plist.list = type_coerce_list(Plist.list, AtlanClassification) + + +class AtlanEntityHeaders(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.guidHeaderMap = attrs.get("guidHeaderMap") + + def type_coerce_attrs(self): + super(AtlanEntityHeaders, self).type_coerce_attrs() + + self.guidHeaderMap = type_coerce_dict(self.guidHeaderMap, AtlanEntityHeader) + + +class EntityMutationResponse(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.mutatedEntities = attrs.get("mutatedEntities") + self.guidAssignments = attrs.get("guidAssignments") + + def type_coerce_attrs(self): + super(EntityMutationResponse, self).type_coerce_attrs() + + self.mutatedEntities = type_coerce_dict_list( + self.mutatedEntities, AtlanEntityHeader + ) + + def get_assigned_guid(self, guid): + return self.guidAssignments.get(guid) if self.guidAssignments else None + + +class EntityMutations(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.entity_mutations = attrs.get("entity_mutations") + + def type_coerce_attrs(self): + super(EntityMutations, self).type_coerce_attrs() + + self.entity_mutations = type_coerce_list(self.entity_mutations, EntityMutation) + + +class EntityMutation(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.op = attrs.get("op") + self.entity = attrs.get("entity") + + def type_coerce_attrs(self): + super(EntityMutation, self).type_coerce_attrs() + + self.entity = type_coerce(self.entity, AtlanEntity) + + +class AtlanCheckStateRequest(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.entityGuids = attrs.get("entityGuids") + self.entityTypes = attrs.get("entityTypes") + self.fixIssues = attrs.get("fixIssues") + + +class AtlanCheckStateResult(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.entitiesScanned = attrs.get("entitiesScanned") + self.entitiesOk = attrs.get("entitiesOk") + self.entitiesFixed = attrs.get("entitiesFixed") + self.entitiesPartiallyFixed = attrs.get("entitiesPartiallyFixed") + self.entitiesNotFixed = attrs.get("entitiesNotFixed") + self.state = attrs.get("state") + self.entities = attrs.get("entities") + + def type_coerce_attrs(self): + super(AtlanCheckStateResult, self).type_coerce_attrs() + + self.entities = type_coerce(self.entities, AtlanEntityState) + + +class AtlanEntityState(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.guid = attrs.get("guid") + self.typeName = attrs.get("typeName") + self.name = attrs.get("name") + self.status = attrs.get("status") + self.state = attrs.get("state") + self.issues = attrs.get("issues") diff --git a/pyatlan/model/misc.py b/pyatlan/model/misc.py new file mode 100644 index 000000000..158d91be7 --- /dev/null +++ b/pyatlan/model/misc.py @@ -0,0 +1,97 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# Copyright [2015-2021] The Apache Software Foundation +# +# 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 json +import sys + +from pyatlan.utils import next_id, non_null + + +class AtlanBase(dict): + def __init__(self, attrs): + pass + + def __getattr__(self, attr): + return self.get(attr) + + def __setattr__(self, key, value): + self.__setitem__(key, value) + + def __setitem__(self, key, value): + super(AtlanBase, self).__setitem__(key, value) + self.__dict__.update({key: value}) + + def __delattr__(self, item): + self.__delitem__(item) + + def __delitem__(self, key): + super(AtlanBase, self).__delitem__(key) + del self.__dict__[key] + + def __repr__(self): + return json.dumps(self) + + def type_coerce_attrs(self): + pass + + +class AtlanBaseModelObject(AtlanBase): + def __init__(self, members): + AtlanBase.__init__(self, members) + + self.guid = members.get("guid") + + if self.guid is None: + self.guid = next_id() + + +class TimeBoundary(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.startTime = attrs.get("startTime") + self.endTime = attrs.get("endTime") + self.timeZone = attrs.get("timeZone") + + +class Plist(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.list = non_null(attrs.get("list"), []) + self.startIndex = non_null(attrs.get("startIndex"), 0) + self.pageSize = non_null(attrs.get("pageSize"), 0) + self.totalCount = non_null(attrs.get("totalCount"), 0) + self.sortBy = attrs.get("sortBy") + self.sortType = attrs.get("sortType") + + +class SearchFilter(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.startIndex = non_null(attrs.get("startIndex"), 0) + self.maxsize = non_null(attrs.get("maxsize"), sys.maxsize) + self.getCount = non_null(attrs.get("getCount"), True) diff --git a/pyatlan/model/typedef.py b/pyatlan/model/typedef.py new file mode 100644 index 000000000..05fa9af68 --- /dev/null +++ b/pyatlan/model/typedef.py @@ -0,0 +1,245 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# Copyright [2015-2021] The Apache Software Foundation +# +# 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 logging + +from pyatlan.model.enums import TypeCategory +from pyatlan.model.misc import AtlanBase +from pyatlan.utils import non_null, type_coerce, type_coerce_dict_list, type_coerce_list + +LOG = logging.getLogger("pyatlan") + + +class AtlanBaseTypeDef(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.category = attrs.get("category") + self.guid = attrs.get("guid") + self.createdBy = attrs.get("createdBy") + self.updatedBy = attrs.get("updatedBy") + self.createTime = attrs.get("createTime") + self.updateTime = attrs.get("updateTime") + self.version = attrs.get("version") + self.name = attrs.get("name") + self.description = attrs.get("description") + self.typeVersion = attrs.get("typeVersion") + self.serviceType = attrs.get("serviceType") + self.options = attrs.get("options") + + +class AtlanEnumDef(AtlanBaseTypeDef): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBaseTypeDef.__init__(self, attrs) + + self.elementDefs = attrs.get("elementDefs") + self.defaultValue = attrs.get("defaultValue") + + def type_coerce_attrs(self): + super(AtlanEnumDef, self).type_coerce_attrs() + + self.elementDefs = type_coerce_list(self.elementDefs, AtlanEnumElementDef) + + +class AtlanStructDef(AtlanBaseTypeDef): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBaseTypeDef.__init__(self, attrs) + + self.category = non_null(attrs.get("category"), TypeCategory.STRUCT.name) + self.attributeDefs = attrs.get("attributeDefs") + + def type_coerce_attrs(self): + super(AtlanStructDef, self).type_coerce_attrs() + + self.attributeDefs = type_coerce_list(self.attributeDefs, AtlanAttributeDef) + + +class AtlanClassificationDef(AtlanStructDef): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanStructDef.__init__(self, attrs) + + self.category = TypeCategory.CLASSIFICATION.name + self.superTypes = attrs.get("superTypes") + self.entityTypes = attrs.get("entityTypes") + self.subTypes = attrs.get("subTypes") + + +class AtlanEntityDef(AtlanStructDef): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanStructDef.__init__(self, attrs) + + self.category = TypeCategory.ENTITY.name + self.superTypes = attrs.get("superTypes") + self.subTypes = attrs.get("subTypes") + self.relationshipAttributeDefs = attrs.get("relationshipAttributeDefs") + self.businessAttributeDefs = attrs.get("businessAttributeDefs") + + def type_coerce_attrs(self): + super(AtlanEntityDef, self).type_coerce_attrs() + + self.relationshipAttributeDefs = type_coerce_list( + self.relationshipAttributeDefs, AtlanRelationshipAttributeDef + ) + self.businessAttributeDefs = type_coerce_dict_list( + self.businessAttributeDefs, AtlanAttributeDef + ) + + +class AtlanRelationshipDef(AtlanStructDef): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanStructDef.__init__(self, attrs) + + self.category = TypeCategory.RELATIONSHIP.name + self.relationshipCategory = attrs.get("relationshipCategory") + self.relationshipLabel = attrs.get("relationshipLabel") + self.propagateTags = attrs.get("propagateTags") + self.endDef1 = attrs.get("endDef1") + self.endDef2 = attrs.get("endDef2") + + def type_coerce_attrs(self): + super(AtlanRelationshipDef, self).type_coerce_attrs() + + self.endDef1 = type_coerce(self.endDef1, AtlanRelationshipEndDef) + self.endDef2 = type_coerce(self.endDef2, AtlanRelationshipEndDef) + + +class AtlanBusinessMetadataDef(AtlanStructDef): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanStructDef.__init__(self, attrs) + + self.category = TypeCategory.BUSINESS_METADATA.name + self.displayName = attrs.get("displayName") + + +class AtlanAttributeDef(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.name = attrs.get("name") + self.typeName = attrs.get("typeName") + self.isOptional = attrs.get("isOptional") + self.cardinality = attrs.get("cardinality") + self.valuesMinCount = attrs.get("valuesMinCount") + self.valuesMaxCount = attrs.get("valuesMaxCount") + self.isUnique = attrs.get("isUnique") + self.isIndexable = attrs.get("isIndexable") + self.includeInNotification = attrs.get("includeInNotification") + self.defaultValue = attrs.get("defaultValue") + self.description = attrs.get("description") + self.searchWeight = non_null(attrs.get("searchWeight"), -1) + self.indexType = attrs.get("indexType") + self.constraints = attrs.get("constraints") + self.options = attrs.get("options") + self.displayName = attrs.get("displayName") + + def type_coerce_attrs(self): + super(AtlanAttributeDef, self).type_coerce_attrs() + + self.constraints = type_coerce_list(self.constraints, AtlanConstraintDef) + + +class AtlanConstraintDef(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.type = attrs.get("type") + self.params = attrs.get("params") + + +class AtlanEnumElementDef(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.value = attrs.get("value") + self.description = attrs.get("description") + self.ordinal = attrs.get("ordinal") + + +class AtlanRelationshipAttributeDef(AtlanAttributeDef): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanAttributeDef.__init__(self, attrs) + + self.relationshipTypeName = attrs.get("relationshipTypeName") + self.isLegacyAttribute = attrs.get("isLegacyAttribute") + + +class AtlanRelationshipEndDef(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.type = attrs.get("type") + self.name = attrs.get("name") + self.isContainer = attrs.get("isContainer") + self.cardinality = attrs.get("cardinality") + self.isLegacyAttribute = attrs.get("isLegacyAttribute") + self.description = attrs.get("description") + + +class AtlanTypesDef(AtlanBase): + def __init__(self, attrs=None): + attrs = attrs or {} + + AtlanBase.__init__(self, attrs) + + self.enumDefs = attrs.get("enumDefs") + self.structDefs = attrs.get("structDefs") + self.classificationDefs = attrs.get("classificationDefs") + self.entityDefs = attrs.get("entityDefs") + self.relationshipDefs = attrs.get("relationshipDefs") + self.businessMetadataDefs = attrs.get("businessMetadataDefs") + + def type_coerce_attrs(self): + super(AtlanTypesDef, self).type_coerce_attrs() + + self.enumDefs = type_coerce_list(self.enumDefs, AtlanEnumDef) + self.structDefs = type_coerce_list(self.structDefs, AtlanStructDef) + self.classificationDefs = type_coerce_list( + self.classificationDefs, AtlanClassificationDef + ) + self.entityDefs = type_coerce_list(self.entityDefs, AtlanEntityDef) + self.relationshipDefs = type_coerce_list( + self.relationshipDefs, AtlanRelationshipDef + ) + self.businessMetadataDefs = type_coerce_list( + self.businessMetadataDefs, AtlanBusinessMetadataDef + ) diff --git a/pyatlan/utils.py b/pyatlan/utils.py new file mode 100644 index 000000000..3467033a1 --- /dev/null +++ b/pyatlan/utils.py @@ -0,0 +1,189 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# Copyright [2015-2021] The Apache Software Foundation +# +# 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 enum +import logging +import time +from functools import reduce +from typing import Optional + +BASE_URI = "api/meta/" +APPLICATION_JSON = "application/json" +APPLICATION_OCTET_STREAM = "application/octet-stream" +MULTIPART_FORM_DATA = "multipart/form-data" +PREFIX_ATTR = "attr:" +PREFIX_ATTR_ = "attr_" + +s_nextId = milliseconds = int(round(time.time() * 1000)) + 1 + + +def get_logger(name: str = __name__, level: str = "WARN"): + """ + name - defaults to __name__ + """ + logging.basicConfig( + level=logging.WARN, + format="%(asctime)s.%(msecs)03d %(levelname)s %(module)s - %(funcName)s: %(message)s", + ) + logger = logging.getLogger(name) + logger.setLevel(level=level) + return logger + + +def next_id() -> str: + global s_nextId + + s_nextId += 1 + + return f"-{s_nextId}" + + +def list_attributes_to_params( + attributes_list: list, query_params: Optional[dict] = None +) -> dict: + if query_params is None: + query_params = {} + + for i, attr in enumerate(attributes_list): + for key, value in attr.items(): + new_key = PREFIX_ATTR_ + str(i) + ":" + key + query_params[new_key] = value + + return query_params + + +def attributes_to_params( + attributes: list[tuple[str, object]], query_params: Optional[dict] = None +) -> dict: + if query_params is None: + query_params = {} + + if attributes: + for key, value in attributes: + new_key = PREFIX_ATTR + key + query_params[new_key] = value + + return query_params + + +def non_null(obj: Optional[object], def_value: object): + return obj if obj is not None else def_value + + +def type_coerce(obj, obj_type): + if isinstance(obj, obj_type): + ret = obj + elif isinstance(obj, dict): + ret = obj_type(obj) + + ret.type_coerce_attrs() + else: + ret = None + + return ret + + +def type_coerce_list(obj, obj_type): + return ( + [type_coerce(entry, obj_type) for entry in obj] + if isinstance(obj, list) + else None + ) + + +def type_coerce_dict(obj, obj_type): + return ( + {k: type_coerce(v, obj_type) for k, v in obj.items()} + if isinstance(obj, dict) + else None + ) + + +def type_coerce_dict_list(obj, obj_type): + return ( + {k: type_coerce_list(v, obj_type) for k, v in obj.items()} + if isinstance(obj, dict) + else None + ) + + +class API: + def __init__( + self, + path, + method, + expected_status, + consumes=APPLICATION_JSON, + produces=APPLICATION_JSON, + ): + self.path = path + self.method = method + self.expected_status = expected_status + self.consumes = consumes + self.produces = produces + + @staticmethod + def multipart_urljoin(base_path, *path_elems): + """Join a base path and multiple context path elements. Handle single + leading and trailing `/` characters transparently. + + Args: + base_path (string): the base path or url (ie. `http://atlas/v2/`) + *path_elems (string): multiple relative path elements (ie. `/my/relative`, `/path`) + + Returns: + string: the result of joining the base_path with the additional path elements + """ + + def urljoin_pair(left, right): + return "/".join([left.rstrip("/"), right.strip("/")]) + + return reduce(urljoin_pair, path_elems, base_path) + + def format_path(self, params): + return API( + self.path.format(**params), + self.method, + self.expected_status, + self.consumes, + self.produces, + ) + + def format_path_with_params(self, *params): + request_path = API.multipart_urljoin(self.path, *params) + return API( + request_path, + self.method, + self.expected_status, + self.consumes, + self.produces, + ) + + +class HTTPMethod(enum.Enum): + GET = "GET" + PUT = "PUT" + POST = "POST" + DELETE = "DELETE" + + +class HTTPStatus: + OK = 200 + NO_CONTENT = 204 + SERVICE_UNAVAILABLE = 503 diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 000000000..2e3ae4965 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,6 @@ +flake8 +mypy +black +types-requests +pytest +pre-commit diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..96a6d591a --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests>=2.24 diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..84239aab8 --- /dev/null +++ b/setup.py @@ -0,0 +1,51 @@ +#!/usr/bin/env/python +# Copyright 2022 Atlan Pte, Ltd +# Copyright [2015-2021] The Apache Software Foundation +# +# 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. + +from setuptools import find_packages, setup + +# External dependencies +requirements = ["requests>=2.24"] + +long_description = "" +with open("README.md", "r") as fh: + long_description = fh.read() + +setup( + name="pyatlan", + version="0.0.1", + author="Atlan Pte, Ltd", + author_email="engineering@atlan.com", + description="Atlan Python Client", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/atlanhq/atlan-python", + license="Apache LICENSE 2.0", + classifiers=[ + "Programming Language :: Python :: 3.9", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + ], + packages=find_packages(), + install_requires=requirements, + include_package_data=True, + zip_safe=False, + keywords="atlas client, apache atlas", + python_requires=">=3.9", +) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 000000000..735dc72c5 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,12 @@ +from pyatlan.utils import list_attributes_to_params + + +def test_list_attributes_to_params_with_no_query_parms(): + assert list_attributes_to_params([{"first": "Dave"}]) == {"attr_0:first": "Dave"} + + +def test_list_attributes_to_params_with_query_parms(): + assert list_attributes_to_params([{"first": "Dave"}], {"last": "Jo"}) == { + "attr_0:first": "Dave", + "last": "Jo", + } diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..d3a4f75f6 --- /dev/null +++ b/tox.ini @@ -0,0 +1,25 @@ +########################################################################## +# Copyright 2022 Atlan Pte, Ltd +# Copyright [2015-2021] The Apache Software Foundation +# +# 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. +########################################################################## + +[flake8] +max-line-length = 120 +per-file-ignores = + tests/*:S101