Source code for ouster.sdk.util.studio

import json
import requests
import secrets
import base64
import hashlib
import webbrowser
import socketserver
import configparser
from http.server import BaseHTTPRequestHandler
from pathlib import Path
from requests.adapters import HTTPAdapter
from urllib.parse import parse_qs, urlparse, urlencode


[docs] def save_tokens(keycloak_url, realm_name, client_id, base_url, token): home_directory = Path.home() config_directory = home_directory / ".config" / "ouster" config_directory.mkdir(parents=True, exist_ok=True) config_file_path = config_directory / "application_default_credentials.json" token_str = json.dumps(token) config = configparser.ConfigParser() config["Keycloak"] = { "KeycloakUrl": keycloak_url, "RealmName": realm_name, "ClientId": client_id, "BaseUrl": base_url, "Token": token_str, # Store the JSON string representation of the token } with open(config_file_path, "w") as configfile: config.write(configfile)
[docs] def delete_tokens_file(): home_directory = Path.home() config_file_path = ( home_directory / ".config" / "ouster" / "application_default_credentials.json" ) if config_file_path.exists(): config_file_path.unlink()
[docs] def load_tokens(): home_directory = Path.home() config_directory = home_directory / ".config" / "ouster" config_file_path = config_directory / "application_default_credentials.json" config = configparser.ConfigParser() config.read(config_file_path) if "Keycloak" in config: keycloak_url = config["Keycloak"].get("KeycloakUrl") realm_name = config["Keycloak"].get("RealmName") client_id = config["Keycloak"].get("ClientId") base_url = config["Keycloak"].get("BaseUrl") token_str = config["Keycloak"].get("Token") token = json.loads(token_str) return keycloak_url, realm_name, client_id, base_url, token else: return None
[docs] class KeycloakPKCETokenProviderV1:
[docs] def code_verifier(self): random = secrets.token_bytes(64) code_verifier = base64.b64encode(random, b"-_").decode().replace("=", "") return code_verifier
def _code_challenge(self, code_verifier): m = hashlib.sha256() m.update(code_verifier.encode()) d = m.digest() code_challenge = base64.b64encode(d, b"-_").decode().replace("=", "") return code_challenge def __init__(self, client_id, base_url, keycloak_url, realm_name, organization_id): self.client_id = client_id self._current_token = None self.token = None self.realm_name = realm_name self.auth_url = None self.base_url = base_url self.keycloak_url = keycloak_url self.code_verifier = self.code_verifier() self.code_challenge = self._code_challenge(self.code_verifier) self.organization_id = organization_id self.redirect_uri = "http://localhost:5050/callback"
[docs] def current(self): result = load_tokens() tokens = None if result: _, _, _, _, tokens = result if tokens is not None and "access_token" in tokens: self.token = tokens["access_token"] return tokens["access_token"] return None
def __next__(self): return None
[docs] def auth(self): """Call to refresh the tokens and initiate the authorization process.""" self.token = self.get_token() save_tokens( keycloak_url=self.keycloak_url, realm_name=self.realm_name, client_id=self.client_id, base_url=self.base_url, token=self.token, ) return self.token
[docs] def make_handler(self, outer_self): class Handler(BaseHTTPRequestHandler): def do_GET(self): query = parse_qs(urlparse(self.path).query) code = query.get("code", [None])[0] if code: outer_self._current_token = outer_self.exchange_code_for_token(code) self.send_response(200) self.end_headers() if "access_token" in outer_self._current_token: self.wfile.write( b"You were authenticated and the authentication was recorded in Data app" b" client. Please return to the console." ) else: self.wfile.write(b"An error occurred while retrieving the token") return Handler
[docs] def get_token(self): self.init_tokens() # first try and refresh the token try: result = load_tokens() tokens = None if result: _, _, _, _, tokens = result if tokens is not None and "refresh_token" in tokens: self._current_token = self.refresh_tokens() if self._current_token is not None: return self._current_token except requests.HTTPError: pass auth_url = self.get_authorization_url() print(f"Opening the following URL to authorize the application: {auth_url}") webbrowser.open(auth_url) # Start a simple HTTP server to handle the callback with socketserver.TCPServer(("", 5050), self.make_handler(self)) as httpd: httpd.handle_request() return self._current_token
[docs] def init_tokens(self, client=None): """Initialize the token provider.""" auth_url = self.keycloak_url + "realms/" + self.realm_name + "/protocol/openid-connect/auth" params = { "response_type": "code", "scope": "openid email profile", "client_id": self.client_id, "code_challenge_method": "S256", "code_challenge": self.code_challenge, "redirect_uri": self.redirect_uri, } self.auth_url = f"{auth_url}?{urlencode(params)}"
[docs] def exchange_code_for_token(self, code): token_url = self.keycloak_url + "realms/" + self.realm_name + "/protocol/openid-connect/token" token_params = { "client_id": self.client_id, "grant_type": "authorization_code", "code": code, "redirect_uri": self.redirect_uri, "code_verifier": self.code_verifier, } token_response = requests.post(token_url, data=token_params) token = token_response.json() return token
[docs] def refresh_tokens(self): result = load_tokens() tokens = None if result: _, _, _, _, tokens = result if tokens is None or "refresh_token" not in tokens: return None refresh_token = tokens["refresh_token"] payload = { "grant_type": "refresh_token", "client_id": self.client_id, "refresh_token": refresh_token } refresh_url = self.keycloak_url + "realms/" + self.realm_name + "/protocol/openid-connect/token" response = requests.post(refresh_url, data=payload) response.raise_for_status() token_data = response.json() return token_data
[docs] def get_authorization_url(self): return self.auth_url
[docs] class RetryableException(Exception): """Exception used for driving retries in the requests. Internal use.""" pass
[docs] class UnAuthorizedException(Exception): """Exception used when user's Access token is invalid. Internal use.""" pass
[docs] class TokenExpiredException(Exception): pass
[docs] class BearerTokenRedirectAdapter(HTTPAdapter): """Custom adapter to ensure the bearer token is passed along in redirects.""" def __init__(self, token_provider, *args, **kwargs): self.token_provider = token_provider super().__init__(*args, **kwargs)
[docs] def send(self, request, **kwargs): # Attach the Authorization header before sending the request token = self.token_provider.current() if token: request.headers["Authorization"] = f"Bearer {token}" response = super().send(request, **kwargs) # Handle redirection and re-apply the token if necessary if response.is_redirect: request.headers["Authorization"] = f"Bearer {self.token_provider.current()}" return response
[docs] def get_common_headers(token_provider): if token_provider and token_provider.current() is not None: return {"Authorization": f"Bearer {token_provider.current()}"} else: return {}
[docs] def check_and_raise(response, token_provider): if response.status_code == 401: token = next(token_provider) if token is None: raise UnAuthorizedException() raise RetryableException() if response.status_code in [429, 503]: # If we think something is wrong, we say we retry raise RetryableException() return response
[docs] def get_json(response): """Helper for getting json out of response. If the response is not json or the json is invalid it returns None Args: response (Response): Response from the API Returns: dict: json response """ if response.status_code == 204: return None try: return response.json() except json.decoder.JSONDecodeError: return None
TIMEOUT = 15
[docs] def get(url, *, params=None, token_provider): session = get_session_with_redirect(token_provider) def call(): response = session.get( url, params=params, timeout=TIMEOUT, headers=get_common_headers(token_provider), ) result = None try: result = check_and_raise(response, token_provider) return result except UnAuthorizedException: return response return call()
[docs] def post(url, *, data, token_provider): session = get_session_with_redirect(token_provider) def call(): response = session.post( url, json=data, timeout=TIMEOUT, headers=get_common_headers(token_provider) ) result = check_and_raise(response, token_provider) return result return call()
# Function to set up session with redirection and retry logic
[docs] def get_session_with_redirect(token_provider): session = requests.Session() adapter = BearerTokenRedirectAdapter(token_provider) # Mount the adapter to the session session.mount("http://", adapter) session.mount("https://", adapter) return session
[docs] class DataAppFile: def __init__(self, id, recording_url, client, output_type, type): self.id = id self.recording_url = recording_url self.client = client self.output_type = output_type self.type = type @property def url(self): return self.recording_url + f"/output/files/{self.id}"
[docs] def get_signed_url(self): url = self.url + "/signed_url" data = self.client.post(url, data={}) return data.json()["url"]
[docs] class DataAppClient: def __init__(self, org_id): self.base_url = "https://data.ouster.dev/" self.organization_id = org_id self.api_url = self.base_url + f"api/v1/organizations/{self.organization_id}/" self.token_provider = KeycloakPKCETokenProviderV1( client_id="pkce-client", realm_name="Ouster", keycloak_url="https://id.ouster.com/", base_url=self.base_url, organization_id=self.organization_id)
[docs] def get(self, url, *, params=None): res = get(url, params=params, token_provider=self.token_provider) if res.status_code == 401: raise UnAuthorizedException() if res.status_code == 404: raise RuntimeError("URL not found") return res
[docs] def post(self, url, *, data=None): res = post(url, data=data, token_provider=self.token_provider) if res.status_code == 401: raise UnAuthorizedException() if res.status_code == 404: raise RuntimeError("URL not found") return res
[docs] def get_url_from_id(self, id): return self.api_url + f"recordings/{id}"
[docs] def auth(self): """Authenticates""" self.token_provider.auth()
[docs] def get_output_files(self, id): """Get output file information about a specific drive.""" url = self.get_url_from_id(id) files_output_url = url + "/output/files/" out = self.get(files_output_url) files = out.json() l = [] for file in files: l.append(DataAppFile(file["id"], url, self, file["output_type"], file["type"])) return l
[docs] def get_drive(self, id): """Gets information about a specific drive.""" url = self.get_url_from_id(id) # If a drive was specified, we should have an id by now # and we get back just one drive return self.get(url).json()