200 lines
6.9 KiB
Python
200 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
notion-apikeys - Query Notion 'API' database for API keys
|
|
Usage:
|
|
query.py <service_name> Look up a service by name
|
|
query.py --list List all entries
|
|
query.py --find-db Discover database structure/properties
|
|
query.py --setup Interactive setup (save token + db-id)
|
|
"""
|
|
|
|
import sys
|
|
import json
|
|
import os
|
|
import argparse
|
|
|
|
CONFIG_FILE = os.path.expanduser("~/.config/notion-apikeys.json")
|
|
NOTION_VERSION = "2022-06-28"
|
|
NOTION_BASE = "https://api.notion.com/v1"
|
|
|
|
|
|
def get_headers(token):
|
|
return {
|
|
"Authorization": f"Bearer {token}",
|
|
"Notion-Version": NOTION_VERSION,
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
|
|
def notion_request(method, path, token, payload=None):
|
|
try:
|
|
import urllib.request
|
|
import urllib.error
|
|
|
|
url = f"{NOTION_BASE}{path}"
|
|
data = json.dumps(payload).encode() if payload is not None else None
|
|
req = urllib.request.Request(url, data=data, headers=get_headers(token), method=method)
|
|
with urllib.request.urlopen(req) as resp:
|
|
return json.loads(resp.read())
|
|
except Exception as e:
|
|
if hasattr(e, "read"):
|
|
err = e.read().decode()
|
|
try:
|
|
err = json.loads(err)
|
|
except Exception:
|
|
pass
|
|
print(f"Notion API error: {err}", file=sys.stderr)
|
|
else:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def find_database(token, name="API"):
|
|
result = notion_request("POST", "/search", token, {
|
|
"query": name,
|
|
"filter": {"value": "database", "property": "object"},
|
|
"page_size": 20,
|
|
})
|
|
dbs = result.get("results", [])
|
|
for db in dbs:
|
|
title_parts = db.get("title", [])
|
|
db_name = "".join(t.get("plain_text", "") for t in title_parts).strip()
|
|
if db_name.lower() == name.lower():
|
|
return db["id"], db_name, db.get("properties", {})
|
|
# If no exact match, return all found
|
|
return None, None, dbs
|
|
|
|
|
|
def query_database(token, db_id, filter_text=None):
|
|
payload = {"page_size": 100}
|
|
if filter_text:
|
|
payload["filter"] = {
|
|
"or": [
|
|
{"property": "Name", "title": {"contains": filter_text}},
|
|
]
|
|
}
|
|
return notion_request("POST", f"/databases/{db_id}/query", token, payload)
|
|
|
|
|
|
def extract_value(prop):
|
|
ptype = prop.get("type", "")
|
|
if ptype == "title":
|
|
return "".join(t.get("plain_text", "") for t in prop.get("title", []))
|
|
elif ptype == "rich_text":
|
|
return "".join(t.get("plain_text", "") for t in prop.get("rich_text", []))
|
|
elif ptype in ("url", "email", "phone_number"):
|
|
return prop.get(ptype) or ""
|
|
elif ptype == "select":
|
|
s = prop.get("select")
|
|
return s.get("name", "") if s else ""
|
|
elif ptype == "multi_select":
|
|
return ", ".join(s.get("name", "") for s in prop.get("multi_select", []))
|
|
elif ptype == "number":
|
|
v = prop.get("number")
|
|
return str(v) if v is not None else ""
|
|
elif ptype == "checkbox":
|
|
return "✓" if prop.get("checkbox") else "✗"
|
|
elif ptype == "date":
|
|
d = prop.get("date")
|
|
return d.get("start", "") if d else ""
|
|
return ""
|
|
|
|
|
|
def load_config():
|
|
if os.path.exists(CONFIG_FILE):
|
|
with open(CONFIG_FILE) as f:
|
|
return json.load(f)
|
|
return {}
|
|
|
|
|
|
def save_config(config):
|
|
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
|
|
with open(CONFIG_FILE, "w") as f:
|
|
json.dump(config, f, indent=2)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Notion API key lookup")
|
|
parser.add_argument("service", nargs="?", help="Service name to look up")
|
|
parser.add_argument("--list", action="store_true", help="List all entries")
|
|
parser.add_argument("--find-db", action="store_true", help="Discover DB structure")
|
|
parser.add_argument("--setup", action="store_true", help="Save token + db-id to config")
|
|
parser.add_argument("--token", help="Notion token (overrides config/env)")
|
|
parser.add_argument("--db-id", help="Notion database ID (overrides config)")
|
|
args = parser.parse_args()
|
|
|
|
config = load_config()
|
|
token = args.token or os.environ.get("NOTION_TOKEN") or config.get("token")
|
|
db_id = args.db_id or config.get("database_id")
|
|
|
|
# Setup mode
|
|
if args.setup:
|
|
if not token:
|
|
token = input("Notion Integration Token (secret_...): ").strip()
|
|
if not db_id:
|
|
print("Finding 'API' database automatically...")
|
|
found_id, found_name, _ = find_database(token, "API")
|
|
if found_id:
|
|
print(f"Found: '{found_name}' (ID: {found_id})")
|
|
db_id = found_id
|
|
else:
|
|
db_id = input("Database ID (or leave blank to auto-find each time): ").strip()
|
|
config["token"] = token
|
|
if db_id:
|
|
config["database_id"] = db_id
|
|
save_config(config)
|
|
print(f"✓ Config saved to {CONFIG_FILE}")
|
|
return
|
|
|
|
if not token:
|
|
print("ERROR: Notion token required.", file=sys.stderr)
|
|
print(" Run: query.py --setup", file=sys.stderr)
|
|
print(" Or set: NOTION_TOKEN env var", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Auto-discover database if needed
|
|
if not db_id:
|
|
found_id, found_name, found_dbs = find_database(token, "API")
|
|
if found_id:
|
|
db_id = found_id
|
|
else:
|
|
print("ERROR: Could not find 'API' database.", file=sys.stderr)
|
|
print(f"Found databases: {[d.get('id') for d in found_dbs]}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Find database structure
|
|
if args.find_db:
|
|
found_id, found_name, props = find_database(token, "API")
|
|
if found_id:
|
|
print(f"Database: '{found_name}' (ID: {found_id})")
|
|
print("Properties:")
|
|
for name, prop in props.items():
|
|
print(f" • {name} ({prop.get('type', '?')})")
|
|
return
|
|
|
|
# Query
|
|
filter_text = args.service if not args.list else None
|
|
result = query_database(token, db_id, filter_text)
|
|
pages = result.get("results", [])
|
|
|
|
if not pages:
|
|
label = f" for '{args.service}'" if args.service else ""
|
|
print(f"No entries found{label}.")
|
|
return
|
|
|
|
for page in pages:
|
|
props = page.get("properties", {})
|
|
values = {k: extract_value(v) for k, v in props.items()}
|
|
name = next((values[k] for k, v in props.items() if v.get("type") == "title"), "Unknown")
|
|
print(f"\n── {name} ──")
|
|
for k, val in values.items():
|
|
if val and props[k].get("type") != "title":
|
|
# Mask long keys for security in list mode
|
|
if args.list and len(val) > 20 and k.lower() in ("key", "api key", "token", "secret", "value"):
|
|
val = val[:8] + "..." + val[-4:]
|
|
print(f" {k}: {val}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|