kscale 0.1.3__py3-none-any.whl → 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kscale/__init__.py +1 -1
- kscale/web/api.py +4 -0
- kscale/web/cli/user.py +26 -6
- kscale/web/clients/base.py +17 -6
- kscale/web/clients/robot_class.py +7 -3
- kscale/web/clients/user.py +4 -0
- kscale/web/utils.py +11 -9
- {kscale-0.1.3.dist-info → kscale-0.1.4.dist-info}/METADATA +1 -1
- {kscale-0.1.3.dist-info → kscale-0.1.4.dist-info}/RECORD +13 -13
- {kscale-0.1.3.dist-info → kscale-0.1.4.dist-info}/LICENSE +0 -0
- {kscale-0.1.3.dist-info → kscale-0.1.4.dist-info}/WHEEL +0 -0
- {kscale-0.1.3.dist-info → kscale-0.1.4.dist-info}/entry_points.txt +0 -0
- {kscale-0.1.3.dist-info → kscale-0.1.4.dist-info}/top_level.txt +0 -0
    
        kscale/__init__.py
    CHANGED
    
    
    
        kscale/web/api.py
    CHANGED
    
    | @@ -12,3 +12,7 @@ class WebAPI(APIBase): | |
| 12 12 | 
             
                async def get_profile_info(self) -> ProfileResponse:
         | 
| 13 13 | 
             
                    client = await self.www_client()
         | 
| 14 14 | 
             
                    return await client.get_profile_info()
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                async def get_api_key(self, num_hours: int = 24) -> str:
         | 
| 17 | 
            +
                    client = await self.www_client()
         | 
| 18 | 
            +
                    return await client.get_api_key(num_hours)
         | 
    
        kscale/web/cli/user.py
    CHANGED
    
    | @@ -3,6 +3,7 @@ | |
| 3 3 | 
             
            import logging
         | 
| 4 4 |  | 
| 5 5 | 
             
            import click
         | 
| 6 | 
            +
            from tabulate import tabulate
         | 
| 6 7 |  | 
| 7 8 | 
             
            from kscale.utils.cli import coro
         | 
| 8 9 | 
             
            from kscale.web.clients.user import UserClient
         | 
| @@ -19,14 +20,33 @@ def cli() -> None: | |
| 19 20 | 
             
            @cli.command()
         | 
| 20 21 | 
             
            @coro
         | 
| 21 22 | 
             
            async def me() -> None:
         | 
| 23 | 
            +
                """Get information about the currently-authenticated user."""
         | 
| 22 24 | 
             
                client = UserClient()
         | 
| 23 25 | 
             
                profile = await client.get_profile_info()
         | 
| 24 | 
            -
                 | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 26 | 
            +
                click.echo(
         | 
| 27 | 
            +
                    tabulate(
         | 
| 28 | 
            +
                        [
         | 
| 29 | 
            +
                            ["Email", profile.email],
         | 
| 30 | 
            +
                            ["Email verified", profile.email_verified],
         | 
| 31 | 
            +
                            ["User ID", profile.user.user_id],
         | 
| 32 | 
            +
                            ["Is admin", profile.user.is_admin],
         | 
| 33 | 
            +
                            ["Can upload", profile.user.can_upload],
         | 
| 34 | 
            +
                            ["Can test", profile.user.can_test],
         | 
| 35 | 
            +
                        ],
         | 
| 36 | 
            +
                        headers=["Key", "Value"],
         | 
| 37 | 
            +
                        tablefmt="simple",
         | 
| 38 | 
            +
                    )
         | 
| 39 | 
            +
                )
         | 
| 40 | 
            +
             | 
| 41 | 
            +
             | 
| 42 | 
            +
            @cli.command()
         | 
| 43 | 
            +
            @coro
         | 
| 44 | 
            +
            async def key() -> None:
         | 
| 45 | 
            +
                """Get an API key for the currently-authenticated user."""
         | 
| 46 | 
            +
                client = UserClient()
         | 
| 47 | 
            +
                api_key = await client.get_api_key()
         | 
| 48 | 
            +
                click.echo("API key:")
         | 
| 49 | 
            +
                click.echo(click.style(api_key, fg="green"))
         | 
| 30 50 |  | 
| 31 51 |  | 
| 32 52 | 
             
            if __name__ == "__main__":
         | 
    
        kscale/web/clients/base.py
    CHANGED
    
    | @@ -3,6 +3,7 @@ | |
| 3 3 | 
             
            import asyncio
         | 
| 4 4 | 
             
            import json
         | 
| 5 5 | 
             
            import logging
         | 
| 6 | 
            +
            import os
         | 
| 6 7 | 
             
            import secrets
         | 
| 7 8 | 
             
            import time
         | 
| 8 9 | 
             
            import webbrowser
         | 
| @@ -19,13 +20,16 @@ from pydantic import BaseModel | |
| 19 20 | 
             
            from yarl import URL
         | 
| 20 21 |  | 
| 21 22 | 
             
            from kscale.web.gen.api import OICDInfo
         | 
| 22 | 
            -
            from kscale.web.utils import DEFAULT_UPLOAD_TIMEOUT, get_api_root,  | 
| 23 | 
            +
            from kscale.web.utils import DEFAULT_UPLOAD_TIMEOUT, get_api_root, get_auth_dir
         | 
| 23 24 |  | 
| 24 25 | 
             
            logger = logging.getLogger(__name__)
         | 
| 25 26 |  | 
| 26 27 | 
             
            # This port matches the available port for the OAuth callback.
         | 
| 27 28 | 
             
            OAUTH_PORT = 16821
         | 
| 28 29 |  | 
| 30 | 
            +
            # This is the name of the API key header for the K-Scale WWW API.
         | 
| 31 | 
            +
            HEADER_NAME = "x-kscale-api-key"
         | 
| 32 | 
            +
             | 
| 29 33 |  | 
| 30 34 | 
             
            class OAuthCallback:
         | 
| 31 35 | 
             
                def __init__(self) -> None:
         | 
| @@ -162,7 +166,7 @@ class BaseClient: | |
| 162 166 |  | 
| 163 167 | 
             
                @alru_cache
         | 
| 164 168 | 
             
                async def _get_oicd_info(self) -> OICDInfo:
         | 
| 165 | 
            -
                    cache_path =  | 
| 169 | 
            +
                    cache_path = get_auth_dir() / "oicd_info.json"
         | 
| 166 170 | 
             
                    if self.use_cache and cache_path.exists():
         | 
| 167 171 | 
             
                        with open(cache_path, "r") as f:
         | 
| 168 172 | 
             
                            return OICDInfo(**json.load(f))
         | 
| @@ -180,7 +184,7 @@ class BaseClient: | |
| 180 184 | 
             
                    Returns:
         | 
| 181 185 | 
             
                        The OpenID Connect server configuration.
         | 
| 182 186 | 
             
                    """
         | 
| 183 | 
            -
                    cache_path =  | 
| 187 | 
            +
                    cache_path = get_auth_dir() / "oicd_metadata.json"
         | 
| 184 188 | 
             
                    if self.use_cache and cache_path.exists():
         | 
| 185 189 | 
             
                        with open(cache_path, "r") as f:
         | 
| 186 190 | 
             
                            return json.load(f)
         | 
| @@ -207,7 +211,7 @@ class BaseClient: | |
| 207 211 | 
             
                    auth_endpoint = metadata["authorization_endpoint"]
         | 
| 208 212 |  | 
| 209 213 | 
             
                    # Use the cached state and nonce if available, otherwise generate.
         | 
| 210 | 
            -
                    state_file =  | 
| 214 | 
            +
                    state_file = get_auth_dir() / "oauth_state.json"
         | 
| 211 215 | 
             
                    state: str | None = None
         | 
| 212 216 | 
             
                    nonce: str | None = None
         | 
| 213 217 | 
             
                    if state_file.exists():
         | 
| @@ -301,7 +305,7 @@ class BaseClient: | |
| 301 305 | 
             
                    Returns:
         | 
| 302 306 | 
             
                        A bearer token to use with the K-Scale WWW API.
         | 
| 303 307 | 
             
                    """
         | 
| 304 | 
            -
                    cache_path =  | 
| 308 | 
            +
                    cache_path = get_auth_dir() / "bearer_token.txt"
         | 
| 305 309 | 
             
                    if self.use_cache and cache_path.exists():
         | 
| 306 310 | 
             
                        token = cache_path.read_text()
         | 
| 307 311 | 
             
                        if not await self._is_token_expired(token):
         | 
| @@ -315,9 +319,16 @@ class BaseClient: | |
| 315 319 | 
             
                async def get_client(self, *, auth: bool = True) -> httpx.AsyncClient:
         | 
| 316 320 | 
             
                    client = self._client if auth else self._client_no_auth
         | 
| 317 321 | 
             
                    if client is None:
         | 
| 322 | 
            +
                        headers: dict[str, str] = {}
         | 
| 323 | 
            +
                        if auth:
         | 
| 324 | 
            +
                            if "KSCALE_API_KEY" in os.environ:
         | 
| 325 | 
            +
                                headers[HEADER_NAME] = os.environ["KSCALE_API_KEY"]
         | 
| 326 | 
            +
                            else:
         | 
| 327 | 
            +
                                headers["Authorization"] = f"Bearer {await self.get_bearer_token()}"
         | 
| 328 | 
            +
             | 
| 318 329 | 
             
                        client = httpx.AsyncClient(
         | 
| 319 330 | 
             
                            base_url=self.base_url,
         | 
| 320 | 
            -
                            headers= | 
| 331 | 
            +
                            headers=headers,
         | 
| 321 332 | 
             
                            timeout=httpx.Timeout(30.0),
         | 
| 322 333 | 
             
                        )
         | 
| 323 334 | 
             
                        if auth:
         | 
| @@ -3,6 +3,7 @@ | |
| 3 3 | 
             
            import hashlib
         | 
| 4 4 | 
             
            import json
         | 
| 5 5 | 
             
            import logging
         | 
| 6 | 
            +
            import tarfile
         | 
| 6 7 | 
             
            from pathlib import Path
         | 
| 7 8 |  | 
| 8 9 | 
             
            import httpx
         | 
| @@ -13,7 +14,7 @@ from kscale.web.gen.api import ( | |
| 13 14 | 
             
                RobotDownloadURDFResponse,
         | 
| 14 15 | 
             
                RobotUploadURDFResponse,
         | 
| 15 16 | 
             
            )
         | 
| 16 | 
            -
            from kscale.web.utils import  | 
| 17 | 
            +
            from kscale.web.utils import get_robots_dir, should_refresh_file
         | 
| 17 18 |  | 
| 18 19 | 
             
            logger = logging.getLogger(__name__)
         | 
| 19 20 |  | 
| @@ -98,7 +99,7 @@ class RobotClassClient(BaseClient): | |
| 98 99 | 
             
                    return response
         | 
| 99 100 |  | 
| 100 101 | 
             
                async def download_robot_class_urdf(self, class_name: str, *, cache: bool = True) -> Path:
         | 
| 101 | 
            -
                    cache_path =  | 
| 102 | 
            +
                    cache_path = get_robots_dir() / class_name / "robot.tgz"
         | 
| 102 103 | 
             
                    if cache and cache_path.exists() and not should_refresh_file(cache_path):
         | 
| 103 104 | 
             
                        return cache_path
         | 
| 104 105 | 
             
                    data = await self._request("GET", f"/robots/urdf/{class_name}", auth=True)
         | 
| @@ -132,7 +133,10 @@ class RobotClassClient(BaseClient): | |
| 132 133 | 
             
                    if hash_value_hex != expected_hash:
         | 
| 133 134 | 
             
                        raise ValueError(f"MD5 hash mismatch: {hash_value_hex} != {expected_hash}")
         | 
| 134 135 |  | 
| 135 | 
            -
                     | 
| 136 | 
            +
                    logger.info("Unpacking downloaded file")
         | 
| 137 | 
            +
                    with tarfile.open(cache_path, "r:gz") as tar:
         | 
| 138 | 
            +
                        tar.extractall(path=cache_path.parent)
         | 
| 139 | 
            +
             | 
| 136 140 | 
             
                    logger.info("Updating downloaded file information")
         | 
| 137 141 | 
             
                    info = {"md5_hash": hash_value_hex}
         | 
| 138 142 | 
             
                    with open(cache_path_info, "w") as f:
         | 
    
        kscale/web/clients/user.py
    CHANGED
    
    | @@ -8,3 +8,7 @@ class UserClient(BaseClient): | |
| 8 8 | 
             
                async def get_profile_info(self) -> ProfileResponse:
         | 
| 9 9 | 
             
                    data = await self._request("GET", "/auth/profile", auth=True)
         | 
| 10 10 | 
             
                    return ProfileResponse(**data)
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                async def get_api_key(self, num_hours: int = 24) -> str:
         | 
| 13 | 
            +
                    data = await self._request("POST", "/auth/key", auth=True, data={"num_hours": num_hours})
         | 
| 14 | 
            +
                    return data["api_key"]
         | 
    
        kscale/web/utils.py
    CHANGED
    
    | @@ -13,24 +13,26 @@ DEFAULT_UPLOAD_TIMEOUT = 300.0  # 5 minutes | |
| 13 13 |  | 
| 14 14 |  | 
| 15 15 | 
             
            @functools.lru_cache
         | 
| 16 | 
            -
            def  | 
| 16 | 
            +
            def get_kscale_dir() -> Path:
         | 
| 17 17 | 
             
                """Returns the cache directory for artifacts."""
         | 
| 18 18 | 
             
                return Path(Settings.load().www.cache_dir).expanduser().resolve()
         | 
| 19 19 |  | 
| 20 20 |  | 
| 21 | 
            +
            def get_auth_dir() -> Path:
         | 
| 22 | 
            +
                """Returns the directory for authentication artifacts."""
         | 
| 23 | 
            +
                return get_kscale_dir() / "auth"
         | 
| 24 | 
            +
             | 
| 25 | 
            +
             | 
| 26 | 
            +
            def get_robots_dir() -> Path:
         | 
| 27 | 
            +
                """Returns the directory for robot artifacts."""
         | 
| 28 | 
            +
                return get_kscale_dir() / "robots"
         | 
| 29 | 
            +
             | 
| 30 | 
            +
             | 
| 21 31 | 
             
            def should_refresh_file(file: Path) -> bool:
         | 
| 22 32 | 
             
                """Returns whether the file should be refreshed."""
         | 
| 23 33 | 
             
                return file.exists() and file.stat().st_mtime < time.time() - Settings.load().www.refresh_interval_minutes * 60
         | 
| 24 34 |  | 
| 25 35 |  | 
| 26 | 
            -
            @functools.lru_cache
         | 
| 27 | 
            -
            def get_artifact_dir(artifact_id: str) -> Path:
         | 
| 28 | 
            -
                """Returns the directory for a specific artifact."""
         | 
| 29 | 
            -
                cache_dir = get_cache_dir() / artifact_id
         | 
| 30 | 
            -
                cache_dir.mkdir(parents=True, exist_ok=True)
         | 
| 31 | 
            -
                return cache_dir
         | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 36 | 
             
            @functools.lru_cache
         | 
| 35 37 | 
             
            def get_api_root() -> str:
         | 
| 36 38 | 
             
                """Returns the root URL for the K-Scale WWW API."""
         | 
| @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            kscale/__init__.py,sha256= | 
| 1 | 
            +
            kscale/__init__.py,sha256=yhQJ7pWCzruLSpCCQHN706gdrMsRyf9UFGe9NRP-QPM,172
         | 
| 2 2 | 
             
            kscale/api.py,sha256=jmiuFurTN_Gj_-k-6asqxw8wp-_bgJUXgMPFgJ4lqHA,230
         | 
| 3 3 | 
             
            kscale/cli.py,sha256=PMHLKR5UwdbbReVmqHXpJ-K9-mGHv_0I7KQkwxmFcUA,881
         | 
| 4 4 | 
             
            kscale/conf.py,sha256=x9jJ8pmSweUsLScN741B21SySCrnRZioV-1ZkhmERbI,1585
         | 
| @@ -13,24 +13,24 @@ kscale/utils/api_base.py,sha256=Kk_WtRDdJHmOg6NtHmVxVrcfARSUkhfr29ypLch_pO0,112 | |
| 13 13 | 
             
            kscale/utils/checksum.py,sha256=jt6QmmQND9zrOEnUtOfZpLYROhgto4Gh3OpdUWk4tZA,1093
         | 
| 14 14 | 
             
            kscale/utils/cli.py,sha256=JoaY5x5SdUx97KmMM9j5AjRRUqqrTlJ9qVckZptEsYA,827
         | 
| 15 15 | 
             
            kscale/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 16 | 
            -
            kscale/web/api.py,sha256= | 
| 17 | 
            -
            kscale/web/utils.py,sha256= | 
| 16 | 
            +
            kscale/web/api.py,sha256=oyW0XLfX96RPe1xNgdf8ejfATdLlNlP0CL1lP0FN1nM,593
         | 
| 17 | 
            +
            kscale/web/utils.py,sha256=kZcOYw5oa1vDe2wNn8ZaHRGhTElIJ8tSq-vVhtY9dzM,1047
         | 
| 18 18 | 
             
            kscale/web/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 19 19 | 
             
            kscale/web/cli/robot.py,sha256=rI-A4_0uvJPeA71Apl4Z3mV5fIfWkgmzT9JRmJYxz3A,3307
         | 
| 20 20 | 
             
            kscale/web/cli/robot_class.py,sha256=ymC5phUqofvOXv5P6f51b9lMK5eRDaavvnzS0x9rDbU,3574
         | 
| 21 21 | 
             
            kscale/web/cli/token.py,sha256=1rFC8MYKtqbNsQa2KIqwW1tqpaMtFaxuNsallwejXTU,787
         | 
| 22 | 
            -
            kscale/web/cli/user.py,sha256= | 
| 22 | 
            +
            kscale/web/cli/user.py,sha256=aaJJCL1P5lfhK6ZC9OwOHXKA-I3MWqVZ_k7TYnx33CY,1303
         | 
| 23 23 | 
             
            kscale/web/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 24 | 
            -
            kscale/web/clients/base.py,sha256= | 
| 24 | 
            +
            kscale/web/clients/base.py,sha256=eHdHH6uYLLN7lKRU78JiyOxtzflvYwQ9f7oxYlDwmK4,14594
         | 
| 25 25 | 
             
            kscale/web/clients/client.py,sha256=QjBicdHQYNoUG9XRjAYmGu3THae9DzWa_hQox3OO1Gw,214
         | 
| 26 26 | 
             
            kscale/web/clients/robot.py,sha256=HMfJnkDxaJ_o7X2vdYYS9iob1JRoBG2qiGmQpCQZpAk,1485
         | 
| 27 | 
            -
            kscale/web/clients/robot_class.py,sha256= | 
| 28 | 
            -
            kscale/web/clients/user.py,sha256= | 
| 27 | 
            +
            kscale/web/clients/robot_class.py,sha256=O_6lAKAcdNGFVtojuiQlgPCVP5MQlYPZG5Z-awwauho,5206
         | 
| 28 | 
            +
            kscale/web/clients/user.py,sha256=jsa1_s6qXRM-AGBbHlPhd1NierUtynjY9tVAPNr6_Os,568
         | 
| 29 29 | 
             
            kscale/web/gen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 30 30 | 
             
            kscale/web/gen/api.py,sha256=SovcII36JFgK9jd2CXlLPMjiUROGB4vEnapOsYMUrkU,2188
         | 
| 31 | 
            -
            kscale-0.1. | 
| 32 | 
            -
            kscale-0.1. | 
| 33 | 
            -
            kscale-0.1. | 
| 34 | 
            -
            kscale-0.1. | 
| 35 | 
            -
            kscale-0.1. | 
| 36 | 
            -
            kscale-0.1. | 
| 31 | 
            +
            kscale-0.1.4.dist-info/LICENSE,sha256=HCN2bImAzUOXldAZZI7JZ9PYq6OwMlDAP_PpX1HnuN0,1071
         | 
| 32 | 
            +
            kscale-0.1.4.dist-info/METADATA,sha256=BIuP-bpb_ed4i0R1LeQ1UAU5UUst3eU3G4E1FkrrT2k,2340
         | 
| 33 | 
            +
            kscale-0.1.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
         | 
| 34 | 
            +
            kscale-0.1.4.dist-info/entry_points.txt,sha256=N_0pCpPnwGDYVzOeuaSOrbJkS5L3lS9d8CxpJF1f8UI,62
         | 
| 35 | 
            +
            kscale-0.1.4.dist-info/top_level.txt,sha256=C2ynjYwopg6YjgttnI2dJjasyq3EKNmYp-IfQg9Xms4,7
         | 
| 36 | 
            +
            kscale-0.1.4.dist-info/RECORD,,
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         |