first step in migratng
Browse files- .vscode/launch.json +17 -1
- scribe_python_client/cli.py +73 -1
- scribe_python_client/client.py +278 -2
.vscode/launch.json
CHANGED
|
@@ -61,7 +61,7 @@
|
|
| 61 |
"SCRIBE_TOKEN": "${env:SCRIBE_TOKEN}",
|
| 62 |
}
|
| 63 |
},
|
| 64 |
-
|
| 65 |
"name": "get attestation",
|
| 66 |
"type": "debugpy",
|
| 67 |
"request": "launch",
|
|
@@ -76,7 +76,23 @@
|
|
| 76 |
"env": {
|
| 77 |
"SCRIBE_TOKEN": "${env:SCRIBE_TOKEN}",
|
| 78 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
|
|
|
|
| 81 |
]
|
| 82 |
}
|
|
|
|
| 61 |
"SCRIBE_TOKEN": "${env:SCRIBE_TOKEN}",
|
| 62 |
}
|
| 63 |
},
|
| 64 |
+
{
|
| 65 |
"name": "get attestation",
|
| 66 |
"type": "debugpy",
|
| 67 |
"request": "launch",
|
|
|
|
| 76 |
"env": {
|
| 77 |
"SCRIBE_TOKEN": "${env:SCRIBE_TOKEN}",
|
| 78 |
}
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"name": "get metadata",
|
| 82 |
+
"type": "debugpy",
|
| 83 |
+
"request": "launch",
|
| 84 |
+
"module": "scribe_python_client.cli",
|
| 85 |
+
"console": "internalConsole",
|
| 86 |
+
"args": [
|
| 87 |
+
"--api-call",
|
| 88 |
+
"get-dataset-tables-dict",
|
| 89 |
+
"--env", "prod",
|
| 90 |
+
],
|
| 91 |
+
"env": {
|
| 92 |
+
"SCRIBE_TOKEN": "${env:SCRIBE_TOKEN}",
|
| 93 |
+
}
|
| 94 |
}
|
| 95 |
|
| 96 |
+
|
| 97 |
]
|
| 98 |
}
|
scribe_python_client/cli.py
CHANGED
|
@@ -2,6 +2,7 @@ import argparse
|
|
| 2 |
import os
|
| 3 |
import logging
|
| 4 |
from .client import ScribeClient
|
|
|
|
| 5 |
|
| 6 |
def main():
|
| 7 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
@@ -21,6 +22,10 @@ def main():
|
|
| 21 |
"query-lineage",
|
| 22 |
"query-risk",
|
| 23 |
"query-findings",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
], help="Which API call to execute")
|
| 25 |
parser.add_argument("--api-token", required=False, default=os.getenv('SCRIBE_TOKEN'), help="Your API token from ScribeHub integrations page (defaults to SCRIBE_TOKEN environment variable)")
|
| 26 |
parser.add_argument("--env", default="prod", choices=["prod", "dev", "test", "ci"], help="Which environment to use")
|
|
@@ -28,6 +33,9 @@ def main():
|
|
| 28 |
parser.add_argument("--attestation-id", required=False, help="The ID of the attestation (required for get-attestation API call)")
|
| 29 |
parser.add_argument("--query", required=False, help="The query to run on the dataset (required for query commands)")
|
| 30 |
parser.add_argument("--lineage-graph-file", required=False, help="The file to save the lineage graph to (optional)")
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
args = parser.parse_args()
|
| 33 |
|
|
@@ -50,8 +58,13 @@ def main():
|
|
| 50 |
base_url = "https://api.scribesecurity.com"
|
| 51 |
if args.env != "prod":
|
| 52 |
base_url = f"https://api.{args.env}.scribesecurity.com"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
client = ScribeClient(api_token=args.api_token, base_url=base_url, env = args.env)
|
| 55 |
|
| 56 |
logging.info(f"Executing API call: {args.api_call}")
|
| 57 |
|
|
@@ -86,5 +99,64 @@ def main():
|
|
| 86 |
elif args.api_call == "query-findings":
|
| 87 |
print(client.query_findings(querystr=args.query))
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
if __name__ == "__main__":
|
| 90 |
main()
|
|
|
|
| 2 |
import os
|
| 3 |
import logging
|
| 4 |
from .client import ScribeClient
|
| 5 |
+
import json
|
| 6 |
|
| 7 |
def main():
|
| 8 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
|
|
| 22 |
"query-lineage",
|
| 23 |
"query-risk",
|
| 24 |
"query-findings",
|
| 25 |
+
"get-dataset-links",
|
| 26 |
+
"get-dataset-data",
|
| 27 |
+
"get-dataset-tables-dict",
|
| 28 |
+
"get-dataset-tables-md",
|
| 29 |
], help="Which API call to execute")
|
| 30 |
parser.add_argument("--api-token", required=False, default=os.getenv('SCRIBE_TOKEN'), help="Your API token from ScribeHub integrations page (defaults to SCRIBE_TOKEN environment variable)")
|
| 31 |
parser.add_argument("--env", default="prod", choices=["prod", "dev", "test", "ci"], help="Which environment to use")
|
|
|
|
| 33 |
parser.add_argument("--attestation-id", required=False, help="The ID of the attestation (required for get-attestation API call)")
|
| 34 |
parser.add_argument("--query", required=False, help="The query to run on the dataset (required for query commands)")
|
| 35 |
parser.add_argument("--lineage-graph-file", required=False, help="The file to save the lineage graph to (optional)")
|
| 36 |
+
parser.add_argument("--username", required=False, default = os.getenv('SUPERSET_DEV_USERNAME'), help="Superset username (required for Superset API calls)")
|
| 37 |
+
parser.add_argument("--password", required=False, default = os.getenv('SUPERSET_DEV_PASSWORD'), help="Superset password (required for Superset API calls)")
|
| 38 |
+
|
| 39 |
|
| 40 |
args = parser.parse_args()
|
| 41 |
|
|
|
|
| 58 |
base_url = "https://api.scribesecurity.com"
|
| 59 |
if args.env != "prod":
|
| 60 |
base_url = f"https://api.{args.env}.scribesecurity.com"
|
| 61 |
+
|
| 62 |
+
if args.api_call in ["get-dataset-links", "get-dataset-data", "get-dataset-tables-dict", "get-dataset-tables-md"]:
|
| 63 |
+
if not args.username or not args.password:
|
| 64 |
+
logging.error("--username and --password are required for Superset table export commands.")
|
| 65 |
+
exit(1)
|
| 66 |
|
| 67 |
+
client = ScribeClient(api_token=args.api_token, base_url=base_url, env = args.env, username= args.username, password=args.password)
|
| 68 |
|
| 69 |
logging.info(f"Executing API call: {args.api_call}")
|
| 70 |
|
|
|
|
| 99 |
elif args.api_call == "query-findings":
|
| 100 |
print(client.query_findings(querystr=args.query))
|
| 101 |
|
| 102 |
+
# Get dataset metadata section:
|
| 103 |
+
elif args.api_call == "get-dataset-links":
|
| 104 |
+
links = client.get_dataset_links()
|
| 105 |
+
if links:
|
| 106 |
+
print("Dataset Links:")
|
| 107 |
+
for link in links:
|
| 108 |
+
print(link)
|
| 109 |
+
else:
|
| 110 |
+
logging.error("No dataset links found.")
|
| 111 |
+
result = None
|
| 112 |
+
elif args.api_call == "get-dataset-data":
|
| 113 |
+
dataset_links = client.get_dataset_links()
|
| 114 |
+
if dataset_links:
|
| 115 |
+
client.get_dataset_data(dataset_links, output_file="dataset_metadata.csv")
|
| 116 |
+
logging.info("Metadata export completed successfully.")
|
| 117 |
+
else:
|
| 118 |
+
logging.error("No dataset links found.")
|
| 119 |
+
result = None
|
| 120 |
+
elif args.api_call == "get-dataset-tables-dict":
|
| 121 |
+
dataset_links = client.get_dataset_links()
|
| 122 |
+
if dataset_links:
|
| 123 |
+
client.get_dataset_data(dataset_links, output_file="dataset_metadata.csv")
|
| 124 |
+
logging.info("Metadata export completed successfully.")
|
| 125 |
+
else:
|
| 126 |
+
logging.error("No dataset links found.")
|
| 127 |
+
result = None
|
| 128 |
+
|
| 129 |
+
tables_dict = client.get_dataset_tables_dict()
|
| 130 |
+
#Output path is set to docs/dataset_tables.json
|
| 131 |
+
output_path = os.path.join("docs", "dataset_tables.json")
|
| 132 |
+
|
| 133 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
| 134 |
+
json.dump(tables_dict, f, indent=2)
|
| 135 |
+
|
| 136 |
+
logging.info(f"Saved table metadata dictionary to {output_path}")
|
| 137 |
+
|
| 138 |
+
if result is not None:
|
| 139 |
+
print(result)
|
| 140 |
+
elif args.api_call == "get-dataset-tables-md":
|
| 141 |
+
if not client.jwt_token:
|
| 142 |
+
username = input("Enter your Superset username: ")
|
| 143 |
+
password = input("Enter your Superset password: ")
|
| 144 |
+
client.username = username
|
| 145 |
+
client.password = password
|
| 146 |
+
if not client.authenticate():
|
| 147 |
+
logging.error("Superset login failed.")
|
| 148 |
+
exit(1)
|
| 149 |
+
dataset_links = client.get_dataset_links()
|
| 150 |
+
if dataset_links:
|
| 151 |
+
client.get_dataset_data(dataset_links, output_file="dataset_metadata.csv")
|
| 152 |
+
md_output = client.get_dataset_tables_md()
|
| 153 |
+
#Output path is set to docs/dataset_tables.md
|
| 154 |
+
output_path = os.path.join("docs", "dataset_tables.md")
|
| 155 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
| 156 |
+
f.write(md_output)
|
| 157 |
+
logging.info(f"Saved table metadata Markdown to {output_path}")
|
| 158 |
+
else:
|
| 159 |
+
logging.error("No dataset links found.")
|
| 160 |
+
|
| 161 |
if __name__ == "__main__":
|
| 162 |
main()
|
scribe_python_client/client.py
CHANGED
|
@@ -47,7 +47,8 @@ class ScribeClient:
|
|
| 47 |
return True
|
| 48 |
return (datetime.datetime.now() - last_update).days > 1
|
| 49 |
|
| 50 |
-
def __init__(self, api_token=None, ignore_env = False, base_url="https://api.scribesecurity.com", refresh_interval=600, cached_data={}, env = "prod"
|
|
|
|
| 51 |
def update_from_cached_data(cached_data):
|
| 52 |
if "datasets" in cached_data:
|
| 53 |
self.datasets = cached_data["datasets"]
|
|
@@ -75,8 +76,16 @@ class ScribeClient:
|
|
| 75 |
if not self.api_token:
|
| 76 |
logging.error("No API token provided. Please set the SCRIBE_TOKEN environment variable or pass it as an argument.")
|
| 77 |
return
|
|
|
|
|
|
|
| 78 |
self.env = env
|
| 79 |
self.base_url = base_url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
self.refresh_interval = refresh_interval
|
| 81 |
self.last_refresh = datetime.datetime.now() - datetime.timedelta(days=5) # Initialize to a time in the past
|
| 82 |
|
|
@@ -1091,4 +1100,271 @@ class ScribeClient:
|
|
| 1091 |
r = self.query_superset(self.scribe_findings_dataset, query, time_tokens=["timestamp"])
|
| 1092 |
if r:
|
| 1093 |
return f"{title}\n{self.format_query_result_table(r)}"
|
| 1094 |
-
return "Error running query"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
return True
|
| 48 |
return (datetime.datetime.now() - last_update).days > 1
|
| 49 |
|
| 50 |
+
def __init__(self, api_token=None, ignore_env = False, base_url="https://api.scribesecurity.com", refresh_interval=600, cached_data={}, env = "prod",
|
| 51 |
+
username=None, password=None):
|
| 52 |
def update_from_cached_data(cached_data):
|
| 53 |
if "datasets" in cached_data:
|
| 54 |
self.datasets = cached_data["datasets"]
|
|
|
|
| 76 |
if not self.api_token:
|
| 77 |
logging.error("No API token provided. Please set the SCRIBE_TOKEN environment variable or pass it as an argument.")
|
| 78 |
return
|
| 79 |
+
self.username = username
|
| 80 |
+
self.password = password
|
| 81 |
self.env = env
|
| 82 |
self.base_url = base_url
|
| 83 |
+
|
| 84 |
+
if env == "prod":
|
| 85 |
+
self.superset_base_url = "https://superset.scribesecurity.com"
|
| 86 |
+
else:
|
| 87 |
+
self.superset_base_url = f"https://internal-superset.{env}.scribesecurity.com/login/"
|
| 88 |
+
|
| 89 |
self.refresh_interval = refresh_interval
|
| 90 |
self.last_refresh = datetime.datetime.now() - datetime.timedelta(days=5) # Initialize to a time in the past
|
| 91 |
|
|
|
|
| 1100 |
r = self.query_superset(self.scribe_findings_dataset, query, time_tokens=["timestamp"])
|
| 1101 |
if r:
|
| 1102 |
return f"{title}\n{self.format_query_result_table(r)}"
|
| 1103 |
+
return "Error running query"
|
| 1104 |
+
|
| 1105 |
+
def superset_authenticate(self):
|
| 1106 |
+
if not self.username or not self.password:
|
| 1107 |
+
logging.error("Username and password must be provided for Superset login.")
|
| 1108 |
+
return False
|
| 1109 |
+
|
| 1110 |
+
login_url = f"{self.superset_base_url}/api/v1/security/login"
|
| 1111 |
+
body = {
|
| 1112 |
+
"username": self.username,
|
| 1113 |
+
"password": self.password,
|
| 1114 |
+
"provider": "db"
|
| 1115 |
+
}
|
| 1116 |
+
|
| 1117 |
+
response = self.send_post_request(login_url, body)
|
| 1118 |
+
if not response:
|
| 1119 |
+
logging.error("Login failed. No response received.")
|
| 1120 |
+
return False
|
| 1121 |
+
|
| 1122 |
+
try:
|
| 1123 |
+
result = json.loads(response)
|
| 1124 |
+
except json.JSONDecodeError:
|
| 1125 |
+
logging.error("Failed to parse login response.")
|
| 1126 |
+
return False
|
| 1127 |
+
|
| 1128 |
+
access_token = result.get("access_token")
|
| 1129 |
+
if access_token:
|
| 1130 |
+
self.jwt_token = access_token
|
| 1131 |
+
self.superset_token = access_token
|
| 1132 |
+
logging.info("Login successful. Tokens obtained.")
|
| 1133 |
+
return True
|
| 1134 |
+
else:
|
| 1135 |
+
logging.error("Login failed. No access token found.")
|
| 1136 |
+
return False
|
| 1137 |
+
|
| 1138 |
+
def get_dataset_links(self):
|
| 1139 |
+
|
| 1140 |
+
self.superset_authenticate()
|
| 1141 |
+
|
| 1142 |
+
if not self.jwt_token:
|
| 1143 |
+
logging.error("No JWT token found. Please authenticate first.")
|
| 1144 |
+
return []
|
| 1145 |
+
|
| 1146 |
+
dataset_links = []
|
| 1147 |
+
|
| 1148 |
+
#Use slug to directly fetch the API dashboard's datasets
|
| 1149 |
+
dashboard_slug = "api-dashboard"
|
| 1150 |
+
datasets_url = f"{self.superset_base_url}/api/v1/dashboard/{dashboard_slug}/datasets"
|
| 1151 |
+
logging.info(f"Fetching datasets from {datasets_url}")
|
| 1152 |
+
|
| 1153 |
+
datasets_response = self.send_get_request(datasets_url, token=self.jwt_token)
|
| 1154 |
+
if not datasets_response:
|
| 1155 |
+
logging.error("Failed to fetch datasets for API dashboard.")
|
| 1156 |
+
return []
|
| 1157 |
+
|
| 1158 |
+
try:
|
| 1159 |
+
datasets_data = json.loads(datasets_response)
|
| 1160 |
+
datasets = datasets_data.get("result", [])
|
| 1161 |
+
|
| 1162 |
+
for dataset in datasets:
|
| 1163 |
+
dataset_id = dataset.get("id")
|
| 1164 |
+
if dataset_id:
|
| 1165 |
+
link = f"{self.base_url}/explore/?datasource_type=table&datasource_id={dataset_id}"
|
| 1166 |
+
dataset_links.append(link)
|
| 1167 |
+
|
| 1168 |
+
except json.JSONDecodeError as e:
|
| 1169 |
+
logging.error(f"Error parsing dataset response: {e}")
|
| 1170 |
+
|
| 1171 |
+
logging.info(f"Total dataset links collected: {len(dataset_links)}")
|
| 1172 |
+
return dataset_links
|
| 1173 |
+
|
| 1174 |
+
|
| 1175 |
+
def get_dataset_data(self, dataset_links, output_file="dataset_metadata.csv"):
|
| 1176 |
+
records = []
|
| 1177 |
+
|
| 1178 |
+
for link in dataset_links:
|
| 1179 |
+
#Properly parse dataset ID from the URL
|
| 1180 |
+
parsed_url = urlparse(link)
|
| 1181 |
+
query_params = parse_qs(parsed_url.query)
|
| 1182 |
+
dataset_id_list = query_params.get("datasource_id")
|
| 1183 |
+
if not dataset_id_list:
|
| 1184 |
+
logging.error(f"Could not find dataset_id in link: {link}")
|
| 1185 |
+
continue
|
| 1186 |
+
dataset_id = dataset_id_list[0]
|
| 1187 |
+
|
| 1188 |
+
dataset_url = f"{self.base_url}/api/v1/dataset/{dataset_id}"
|
| 1189 |
+
logging.info(f"Fetching metadata from {dataset_url}")
|
| 1190 |
+
|
| 1191 |
+
response = self.send_get_request(dataset_url)
|
| 1192 |
+
if not response:
|
| 1193 |
+
logging.error(f"Failed to fetch metadata for dataset ID {dataset_id}")
|
| 1194 |
+
continue
|
| 1195 |
+
try:
|
| 1196 |
+
metadata_json = json.loads(response)
|
| 1197 |
+
dataset_metadata = metadata_json.get("result", {})
|
| 1198 |
+
except json.JSONDecodeError as e:
|
| 1199 |
+
logging.error(f"Failed to parse metadata JSON for dataset ID {dataset_id}: {e}")
|
| 1200 |
+
continue
|
| 1201 |
+
|
| 1202 |
+
verbose_name = dataset_metadata.get("verbose_name")
|
| 1203 |
+
if not verbose_name or verbose_name == dataset_metadata.get("table_name"):
|
| 1204 |
+
verbose_name = dataset_metadata.get("table_name", "").replace("_", " ").title()
|
| 1205 |
+
|
| 1206 |
+
#Get dataset metadata
|
| 1207 |
+
dataset_info = {
|
| 1208 |
+
"dataset_id": dataset_metadata.get("id"),
|
| 1209 |
+
"catalog": dataset_metadata.get("catalog"),
|
| 1210 |
+
"changed_on": dataset_metadata.get("changed_on"),
|
| 1211 |
+
"table_name": dataset_metadata.get("table_name"),
|
| 1212 |
+
"verbose_name": verbose_name,
|
| 1213 |
+
"schema": dataset_metadata.get("schema"),
|
| 1214 |
+
"datasource_type": dataset_metadata.get("datasource_type"),
|
| 1215 |
+
"description": dataset_metadata.get("description"),
|
| 1216 |
+
}
|
| 1217 |
+
|
| 1218 |
+
#Get column metadata
|
| 1219 |
+
columns = dataset_metadata.get("columns", [])
|
| 1220 |
+
if columns:
|
| 1221 |
+
for col in columns:
|
| 1222 |
+
record = dataset_info.copy()
|
| 1223 |
+
record.update({
|
| 1224 |
+
"column_id": col.get("id"),
|
| 1225 |
+
"column_name": col.get("column_name"),
|
| 1226 |
+
"column_verbose_name": col.get("verbose_name") or col.get("column_name", "").replace("_", " ").title(),
|
| 1227 |
+
"column_type": col.get("type"),
|
| 1228 |
+
"groupby": col.get("groupby"),
|
| 1229 |
+
"filterable": col.get("filterable"),
|
| 1230 |
+
"is_dttm": col.get("is_dttm"),
|
| 1231 |
+
"column_description": col.get("description"),
|
| 1232 |
+
})
|
| 1233 |
+
records.append(record)
|
| 1234 |
+
else:
|
| 1235 |
+
records.append(dataset_info)
|
| 1236 |
+
|
| 1237 |
+
#Write to CSV
|
| 1238 |
+
if records:
|
| 1239 |
+
fieldnames = [
|
| 1240 |
+
"dataset_id", "table_name", "verbose_name", "schema", "catalog",
|
| 1241 |
+
"datasource_type", "changed_on", "description",
|
| 1242 |
+
"column_id", "column_name", "column_verbose_name", "column_type",
|
| 1243 |
+
"groupby", "filterable", "is_dttm", "column_description"
|
| 1244 |
+
]
|
| 1245 |
+
fieldnames = [field for field in fieldnames if any(field in rec for rec in records)]
|
| 1246 |
+
|
| 1247 |
+
with open(output_file, mode='w', newline='', encoding='utf-8') as csvfile:
|
| 1248 |
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
| 1249 |
+
writer.writeheader()
|
| 1250 |
+
writer.writerows(records)
|
| 1251 |
+
|
| 1252 |
+
logging.info(f"Dataset metadata exported to {output_file}")
|
| 1253 |
+
else:
|
| 1254 |
+
logging.warning("No dataset metadata exported.")
|
| 1255 |
+
|
| 1256 |
+
def get_dataset_tables_dict(self, tables=[]) -> dict:
|
| 1257 |
+
if not self.jwt_token:
|
| 1258 |
+
self.superset_authenticate()
|
| 1259 |
+
|
| 1260 |
+
dataset_links = self.get_dataset_links()
|
| 1261 |
+
if not dataset_links:
|
| 1262 |
+
logging.error("No dataset links found.")
|
| 1263 |
+
return {}
|
| 1264 |
+
|
| 1265 |
+
self.get_dataset_data(dataset_links)
|
| 1266 |
+
|
| 1267 |
+
#Load data from the CSV file (generated by get_dataset_data)
|
| 1268 |
+
records = []
|
| 1269 |
+
try:
|
| 1270 |
+
with open("dataset_metadata.csv", mode="r", encoding="utf-8") as f:
|
| 1271 |
+
reader = csv.DictReader(f)
|
| 1272 |
+
for row in reader:
|
| 1273 |
+
records.append(row)
|
| 1274 |
+
except Exception as e:
|
| 1275 |
+
logging.error(f"Failed to load dataset_metadata.csv: {e}")
|
| 1276 |
+
return {}
|
| 1277 |
+
|
| 1278 |
+
#Group column data by table name
|
| 1279 |
+
table_dict = {}
|
| 1280 |
+
for row in records:
|
| 1281 |
+
table_name = row.get("table_name")
|
| 1282 |
+
if tables and table_name not in tables:
|
| 1283 |
+
continue
|
| 1284 |
+
|
| 1285 |
+
if table_name not in table_dict:
|
| 1286 |
+
table_dict[table_name] = []
|
| 1287 |
+
|
| 1288 |
+
table_dict[table_name].append({
|
| 1289 |
+
"column_name": row.get("column_name", ""),
|
| 1290 |
+
"type": row.get("column_type", ""),
|
| 1291 |
+
"label": row.get("column_verbose_name") or row.get("column_name", "").replace("_", " ").title(),
|
| 1292 |
+
"description": row.get("column_description", "")
|
| 1293 |
+
})
|
| 1294 |
+
|
| 1295 |
+
return table_dict
|
| 1296 |
+
|
| 1297 |
+
|
| 1298 |
+
def get_dataset_tables_md(self, tables=[]) -> str:
|
| 1299 |
+
if not self.jwt_token:
|
| 1300 |
+
self.superset_authenticate()
|
| 1301 |
+
|
| 1302 |
+
dataset_links = self.get_dataset_links()
|
| 1303 |
+
if not dataset_links:
|
| 1304 |
+
logging.error("No dataset links found.")
|
| 1305 |
+
return ""
|
| 1306 |
+
|
| 1307 |
+
self.get_dataset_data(dataset_links)
|
| 1308 |
+
|
| 1309 |
+
# Load data from the CSV file
|
| 1310 |
+
records = []
|
| 1311 |
+
try:
|
| 1312 |
+
with open("dataset_metadata.csv", mode="r", encoding="utf-8") as f:
|
| 1313 |
+
reader = csv.DictReader(f)
|
| 1314 |
+
for row in reader:
|
| 1315 |
+
records.append(row)
|
| 1316 |
+
except Exception as e:
|
| 1317 |
+
logging.error(f"Failed to load dataset_metadata.csv: {e}")
|
| 1318 |
+
return ""
|
| 1319 |
+
|
| 1320 |
+
# Group columns by table_name
|
| 1321 |
+
table_columns = {}
|
| 1322 |
+
for row in records:
|
| 1323 |
+
table_name = row.get("table_name")
|
| 1324 |
+
if tables and table_name not in tables:
|
| 1325 |
+
continue
|
| 1326 |
+
|
| 1327 |
+
if table_name not in table_columns:
|
| 1328 |
+
table_columns[table_name] = []
|
| 1329 |
+
|
| 1330 |
+
column_name = row.get("column_name", "")
|
| 1331 |
+
col_type = row.get("column_type", "")
|
| 1332 |
+
col_type = row.get("column_type", "").upper()
|
| 1333 |
+
expr = row.get("expression", "") or ""
|
| 1334 |
+
|
| 1335 |
+
if col_type == "STRING" and (" in (" in expr.lower() or " in(" in expr.lower()):
|
| 1336 |
+
like_compliant = False
|
| 1337 |
+
else:
|
| 1338 |
+
like_compliant = True
|
| 1339 |
+
|
| 1340 |
+
|
| 1341 |
+
table_columns[table_name].append({
|
| 1342 |
+
"column_name": column_name,
|
| 1343 |
+
"label": row.get("column_verbose_name") or column_name.replace("_", " ").title(),
|
| 1344 |
+
"type": col_type,
|
| 1345 |
+
"description": row.get("column_description", ""),
|
| 1346 |
+
"like_compliant": str(like_compliant)
|
| 1347 |
+
})
|
| 1348 |
+
|
| 1349 |
+
# Build markdown
|
| 1350 |
+
md_parts = []
|
| 1351 |
+
for table_name, cols in table_columns.items():
|
| 1352 |
+
md_parts.append(f"## `{table_name}` Columns\n\n")
|
| 1353 |
+
md_parts.append("| Column Name | Label | Type | Description | Like-compliant |\n")
|
| 1354 |
+
md_parts.append("|-------------|-------|------|-------------|----------------|\n")
|
| 1355 |
+
|
| 1356 |
+
for col in cols:
|
| 1357 |
+
md_parts.append(
|
| 1358 |
+
f"| `{col['column_name']}` | {col['label']} | {col['type']} | "
|
| 1359 |
+
f"{col['description']} | {col['like_compliant']} |\n"
|
| 1360 |
+
)
|
| 1361 |
+
md_parts.append("\n")
|
| 1362 |
+
|
| 1363 |
+
markdown_output = "".join(md_parts)
|
| 1364 |
+
|
| 1365 |
+
# Write to file
|
| 1366 |
+
with open("dataset_tables.md", "w", encoding="utf-8") as f:
|
| 1367 |
+
f.write(markdown_output)
|
| 1368 |
+
|
| 1369 |
+
logging.info("Saved table metadata Markdown to dataset_tables.md")
|
| 1370 |
+
return markdown_output
|