Some checks failed
CI / skinny-install (aco) (push) Successful in 1m12s
CI / skinny-install (api) (push) Successful in 30s
CI / skinny-install (bcda) (push) Successful in 36s
CI / skinny-install (bib) (push) Successful in 35s
CI / skinny-install (bls) (push) Successful in 27s
CI / skinny-install (ccw) (push) Successful in 32s
CI / skinny-install (cli) (push) Successful in 41s
CI / skinny-install (cms) (push) Successful in 37s
CI / skinny-install (conf) (push) Successful in 38s
CI / skinny-install (opps) (push) Successful in 33s
CI / skinny-install (perf) (push) Successful in 38s
CI / skinny-install (pfs) (push) Successful in 38s
CI / skinny-install (rex) (push) Successful in 34s
Deploy / build-scan-report (push) Failing after 46s
Infra CI / notebooks (push) Failing after 25s
Infra CI / zotero (push) Successful in 12s
Infra CI / docs (push) Failing after 16s
CI / lint-test (push) Failing after 11m2s
Infra CI / mc (push) Successful in 21s
Infra CI / api (push) Successful in 29s
Package Supply Chain / pkg-supply-chain (push) Failing after 41s
Mail: Maddy on DO (corwins.media+Resend, fhirworx.io+Postmark), touchless/stateless/idempotent. Gitea SMTP via env_file. CMS inbox at cmsupdates@mail.fhirworx.io with IMAP→bib poller. Bib: regulations.gov v4 client, Federal Register discovery, 164K comment backfill (running), IMAP email ingest, Zotero sync routing. PRISMA: altcha PoW solver, CrossRef DOI resolution, 83/129 PDFs. Zotero: schema parity, ops module, CLI, fail-fast guard. CI: docs.Dockerfile COPY glob fix (tracks #341). Infra: Gitea+marimo fhirworx themes, IOM/OIG modules.
377 lines
9.7 KiB
Python
377 lines
9.7 KiB
Python
import marimo
|
|
|
|
__generated_with = "0.19.8"
|
|
app = marimo.App(width="medium")
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _():
|
|
import marimo as mo
|
|
|
|
mo.md("""
|
|
# Apache Polaris Tutorial - Iceberg Catalog with Governance
|
|
|
|
Polaris is an open-source Iceberg catalog with Unity Catalog-like governance features:
|
|
RBAC, multi-catalog management, and fine-grained access control.
|
|
|
|
This notebook walks through the key concepts and operations.
|
|
""")
|
|
return (mo,)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _():
|
|
import json
|
|
import os
|
|
|
|
import requests
|
|
|
|
from conf import cfg
|
|
|
|
POLARIS_API = cfg.services.polaris
|
|
CLIENT_ID = "root"
|
|
CLIENT_SECRET = os.environ.get("POLARIS_ROOT_SECRET", "polaris_root_secret")
|
|
return POLARIS_API, CLIENT_ID, CLIENT_SECRET, json, requests, os
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 1. Authenticate with Polaris
|
|
|
|
Polaris uses OAuth2 client credentials flow. Get an access token first.
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(POLARIS_API, CLIENT_ID, CLIENT_SECRET, requests):
|
|
token_resp = requests.post(
|
|
f"{POLARIS_API}/api/catalog/v1/oauth/tokens",
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
data={
|
|
"grant_type": "client_credentials",
|
|
"client_id": CLIENT_ID,
|
|
"client_secret": CLIENT_SECRET,
|
|
"scope": "PRINCIPAL_ROLE:ALL",
|
|
},
|
|
)
|
|
|
|
if token_resp.status_code == 200:
|
|
_token_data = token_resp.json()
|
|
access_token = _token_data["access_token"]
|
|
print(f"Token obtained (expires in {_token_data['expires_in']}s)")
|
|
print(f"Token prefix: {access_token[:50]}...")
|
|
else:
|
|
print(f"Auth failed: {token_resp.status_code}")
|
|
print(token_resp.text)
|
|
access_token = None
|
|
return (access_token,)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 2. List Catalogs
|
|
|
|
Catalogs are top-level containers for namespaces and tables.
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(POLARIS_API, access_token, requests):
|
|
headers = {"Authorization": f"Bearer {access_token}"}
|
|
catalogs_resp = requests.get(
|
|
f"{POLARIS_API}/api/management/v1/catalogs", headers=headers
|
|
)
|
|
|
|
if catalogs_resp.status_code == 200:
|
|
_catalogs = catalogs_resp.json().get("catalogs", [])
|
|
print("Catalogs:")
|
|
for _c in _catalogs:
|
|
print(f" {_c['name']} ({_c['type']})")
|
|
if not _catalogs:
|
|
print(" (none yet)")
|
|
else:
|
|
print(f"Error: {catalogs_resp.status_code}")
|
|
return (headers,)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 3. Create a Catalog
|
|
|
|
Create a catalog with S3 storage configuration.
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(POLARIS_API, headers, json, requests):
|
|
catalog_name = "analytics"
|
|
|
|
create_resp = requests.post(
|
|
f"{POLARIS_API}/api/management/v1/catalogs",
|
|
headers={**headers, "Content-Type": "application/json"},
|
|
data=json.dumps(
|
|
{
|
|
"name": catalog_name,
|
|
"type": "INTERNAL",
|
|
"properties": {
|
|
"default-base-location": f"s3://polaris/{catalog_name}/"
|
|
},
|
|
"storageConfigInfo": {
|
|
"storageType": "S3",
|
|
"allowedLocations": ["s3://polaris/", "s3://lakehouse/"],
|
|
"s3": {
|
|
"region": "us-east-1",
|
|
"endpoint": cfg.services.rustfs,
|
|
"pathStyleAccess": True,
|
|
},
|
|
},
|
|
}
|
|
),
|
|
)
|
|
|
|
if create_resp.status_code == 200:
|
|
print(f"Created catalog: {catalog_name}")
|
|
print(create_resp.json())
|
|
elif create_resp.status_code == 409:
|
|
print(f"Catalog '{catalog_name}' already exists")
|
|
else:
|
|
print(f"Error {create_resp.status_code}: {create_resp.text}")
|
|
return (catalog_name,)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 4. Get Catalog Details
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(POLARIS_API, catalog_name, headers, requests):
|
|
cat_resp = requests.get(
|
|
f"{POLARIS_API}/api/management/v1/catalogs/{catalog_name}", headers=headers
|
|
)
|
|
|
|
if cat_resp.status_code == 200:
|
|
_cat = cat_resp.json()
|
|
print(f"Catalog: {_cat['name']}")
|
|
print(f"Type: {_cat['type']}")
|
|
print(f"Properties: {_cat.get('properties', {})}")
|
|
else:
|
|
print(f"Error: {cat_resp.status_code}")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 5. List Principals
|
|
|
|
Principals are users or service accounts that can access catalogs.
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(POLARIS_API, headers, requests):
|
|
principals_resp = requests.get(
|
|
f"{POLARIS_API}/api/management/v1/principals", headers=headers
|
|
)
|
|
|
|
if principals_resp.status_code == 200:
|
|
_principals = principals_resp.json().get("principals", [])
|
|
print("Principals:")
|
|
for _p in _principals:
|
|
print(f" {_p['name']} ({_p['type']})")
|
|
else:
|
|
print(f"Error: {principals_resp.status_code}")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 6. List Principal Roles
|
|
|
|
Principal roles group permissions that can be assigned to principals.
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(POLARIS_API, headers, requests):
|
|
roles_resp = requests.get(
|
|
f"{POLARIS_API}/api/management/v1/principal-roles", headers=headers
|
|
)
|
|
|
|
if roles_resp.status_code == 200:
|
|
_roles = roles_resp.json().get("roles", [])
|
|
print("Principal Roles:")
|
|
for _r in _roles:
|
|
print(f" {_r['name']}")
|
|
if not _roles:
|
|
print(" (none yet)")
|
|
else:
|
|
print(f"Error: {roles_resp.status_code}")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 7. Iceberg REST Catalog API
|
|
|
|
Polaris implements the Iceberg REST Catalog specification.
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(POLARIS_API, catalog_name, headers, requests):
|
|
# Get catalog config via Iceberg REST API
|
|
config_resp = requests.get(
|
|
f"{POLARIS_API}/api/catalog/v1/config",
|
|
headers=headers,
|
|
params={"warehouse": catalog_name},
|
|
)
|
|
|
|
if config_resp.status_code == 200:
|
|
_config = config_resp.json()
|
|
print("Iceberg Catalog Config:")
|
|
print(f" Overrides: {_config.get('overrides', {})}")
|
|
print(f" Defaults: {_config.get('defaults', {})}")
|
|
else:
|
|
print(f"Error: {config_resp.status_code}")
|
|
print(config_resp.text)
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 8. List Namespaces
|
|
|
|
Namespaces are like schemas/databases in the catalog.
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(POLARIS_API, catalog_name, headers, requests):
|
|
ns_resp = requests.get(
|
|
f"{POLARIS_API}/api/catalog/v1/{catalog_name}/namespaces", headers=headers
|
|
)
|
|
|
|
if ns_resp.status_code == 200:
|
|
_namespaces = ns_resp.json().get("namespaces", [])
|
|
print(f"Namespaces in {catalog_name}:")
|
|
for _ns in _namespaces:
|
|
print(f" {'.'.join(_ns)}")
|
|
if not _namespaces:
|
|
print(" (none yet)")
|
|
else:
|
|
print(f"Error: {ns_resp.status_code}")
|
|
print(ns_resp.text)
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 9. Create a Namespace
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(POLARIS_API, catalog_name, headers, json, requests):
|
|
ns_name = "tutorial"
|
|
|
|
create_ns_resp = requests.post(
|
|
f"{POLARIS_API}/api/catalog/v1/{catalog_name}/namespaces",
|
|
headers={**headers, "Content-Type": "application/json"},
|
|
data=json.dumps(
|
|
{
|
|
"namespace": [ns_name],
|
|
"properties": {"description": "Tutorial namespace"},
|
|
}
|
|
),
|
|
)
|
|
|
|
if create_ns_resp.status_code == 200:
|
|
print(f"Created namespace: {ns_name}")
|
|
elif create_ns_resp.status_code == 409:
|
|
print(f"Namespace '{ns_name}' already exists")
|
|
else:
|
|
print(f"Error {create_ns_resp.status_code}: {create_ns_resp.text}")
|
|
return (ns_name,)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 10. Delete a Catalog
|
|
|
|
To clean up, you can delete catalogs (must be empty first).
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(POLARIS_API, headers, requests):
|
|
def delete_catalog(name):
|
|
resp = requests.delete(
|
|
f"{POLARIS_API}/api/management/v1/catalogs/{name}", headers=headers
|
|
)
|
|
if resp.status_code == 204:
|
|
print(f"Deleted: {name}")
|
|
elif resp.status_code == 409:
|
|
print(f"Catalog '{name}' is not empty")
|
|
else:
|
|
print(f"Error: {resp.status_code} - {resp.text}")
|
|
|
|
# Uncomment to delete:
|
|
# delete_catalog("analytics")
|
|
print("delete_catalog() ready")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## Summary
|
|
|
|
| Operation | API |
|
|
|-----------|-----|
|
|
| Get token | `POST /api/catalog/v1/oauth/tokens` |
|
|
| List catalogs | `GET /api/management/v1/catalogs` |
|
|
| Create catalog | `POST /api/management/v1/catalogs` |
|
|
| Delete catalog | `DELETE /api/management/v1/catalogs/{name}` |
|
|
| List principals | `GET /api/management/v1/principals` |
|
|
| List roles | `GET /api/management/v1/principal-roles` |
|
|
| List namespaces | `GET /api/catalog/v1/{catalog}/namespaces` |
|
|
| Create namespace | `POST /api/catalog/v1/{catalog}/namespaces` |
|
|
|
|
## Polaris vs Nessie
|
|
|
|
| Feature | Nessie | Polaris |
|
|
|---------|--------|---------|
|
|
| Git-like branching | Yes | No |
|
|
| Time travel | Yes | Yes |
|
|
| RBAC/governance | Basic | Full |
|
|
| Multi-catalog | No | Yes |
|
|
| Unity Catalog-like | No | Yes |
|
|
""")
|
|
return
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run()
|