-
Notifications
You must be signed in to change notification settings - Fork 12
API tokens authentication in SDK (APPS-14746) #191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
366d5b3
cb4f3ff
79aa9bf
b6f2f64
83c6afc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| [tool.poetry] | ||
| name = "rtbhouse-sdk" | ||
| version = "15.0.0" | ||
| version = "16.0.0" | ||
| description = "RTB House SDK" | ||
| authors = ["RTB House Apps Team <apps@rtbhouse.com>"] | ||
| license = "BSD License" | ||
|
|
@@ -28,6 +28,8 @@ python = ">=3.10, <4.0" | |
|
|
||
| httpx = "^0.28.0" | ||
| pydantic = ">=1.9, <3.0" | ||
| cachetools = "^7.0.3" | ||
| types-cachetools = "^6.2.0.20251022" | ||
|
Comment on lines
+31
to
+32
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prośba o posortowanie importów, samo types-cachetools może iść do grupki .dev |
||
|
|
||
| [tool.poetry.group.dev.dependencies] | ||
| pydantic = "^2.0.0" # required for tests | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| """RTB House Python SDK.""" | ||
|
|
||
| __version__ = "15.0.0" | ||
| __version__ = "16.0.0" |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Czy potrzebujemy tego pliku? Może jednak wystarczy |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| """Entry point for the RTB House Python SDK CLI.""" | ||
|
|
||
| from .api_tokens.cli import main | ||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ponieważ dość mocno nam się rozmyła struktura w tym module, proponuję zrealizować go przy użyciu Mamy obecnie coś takiego:
Helpery przerzućmy na dół. Przekazywaniem tokena przez argsy cli / env proponuję zastąpić przekazywaniem tokena przez stdin/prompt, systemowy shell obsłuży natywnie dwa pozostałe przypadki. Proponuję spojrzeć co click/typer oferuje na takie okazje, być może po swojemu obsługuje stdin. Zamiast zaślepiać pustym stringiem - proponuję wziąć pod uwagę stałą długość tokenu (albo przynajmniej założyć że nie może być pusty). Mamy też None. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| """CLI for API token management.""" | ||
|
|
||
| import argparse | ||
| import getpass | ||
| import os | ||
| import sys | ||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
| from ..client import ApiTokenAuth, Client | ||
| from ..exceptions import ApiRequestException | ||
| from ..schema import ApiToken as ApiTokenResponse | ||
| from .managers import ApiTokenExpiredException, ApiTokenManager | ||
| from .models import ApiToken | ||
| from .storages.base import ApiTokenStorageException | ||
| from .storages.json_file import DEFAULT_JSON_FILE_PATH, JsonFileApiTokenStorage | ||
|
|
||
|
|
||
| def _read_token_from_stdin() -> str: | ||
| if sys.stdin.isatty(): | ||
| token = getpass.getpass("Paste API token: ").strip() | ||
| return token | ||
|
|
||
| token = sys.stdin.read().strip() | ||
|
|
||
| return token | ||
|
|
||
|
|
||
| def _resolve_token( | ||
| *, | ||
| token_arg: str | None = None, | ||
| env_var: str | None = None, | ||
| ) -> str: | ||
| print(token_arg, env_var) | ||
| if token_arg: | ||
| return token_arg.strip() | ||
|
|
||
| if env_var: | ||
| env_val = os.getenv(env_var) | ||
| return env_val.strip() if env_val else "" | ||
|
|
||
| return _read_token_from_stdin() | ||
|
|
||
|
|
||
| def _get_token(token: str) -> ApiTokenResponse: | ||
| with Client(auth=ApiTokenAuth(token=token)) as client: | ||
| return client.get_current_api_token() | ||
|
|
||
|
|
||
| def cmd_init_json(args: argparse.Namespace) -> int: | ||
| path = Path(args.path).expanduser() | ||
|
|
||
| token = _resolve_token(token_arg=args.token, env_var=args.env_var) | ||
| if not token: | ||
| print("ERROR: Empty token. Aborting.", file=sys.stderr) | ||
| return 1 | ||
|
|
||
| try: | ||
| api_token_response = _get_token(token) | ||
| except ApiRequestException as e: | ||
| print(f"ERROR: Could not verify token via API or token is invalid. Original error: {e}.", file=sys.stderr) | ||
| return 1 | ||
|
|
||
| storage = JsonFileApiTokenStorage(path) | ||
| api_token = ApiToken( | ||
| token=token, | ||
| expires_at=api_token_response.expires_at, | ||
| ) | ||
| storage.save(api_token) | ||
|
|
||
| print(f"OK: Token successfully initialized in {path}", file=sys.stdout) | ||
| return 0 | ||
|
|
||
|
|
||
| def cmd_keep_alive_json(args: argparse.Namespace) -> int: | ||
| path = Path(args.path).expanduser() | ||
| storage = JsonFileApiTokenStorage(path) | ||
| manager = ApiTokenManager(storage=storage) | ||
|
|
||
| try: | ||
| manager.keep_alive(args.auto_rotate) | ||
| except (ApiTokenStorageException, ApiRequestException, ApiTokenExpiredException) as e: | ||
| print(f"ERROR: Keep-alive failed. Original error: {e}", file=sys.stderr) | ||
| return 1 | ||
|
|
||
| print("OK: Token keep-alive successful", file=sys.stdout) | ||
| return 0 | ||
|
|
||
|
|
||
| def cmd_keep_alive(args: argparse.Namespace) -> int: | ||
| token = _resolve_token(token_arg=args.token, env_var=args.env_var) | ||
| if not token: | ||
| print("ERROR: Empty token. Aborting.", file=sys.stderr) | ||
| return 1 | ||
|
|
||
| try: | ||
| api_token_response = _get_token(token) | ||
| except ApiRequestException as e: | ||
| print(f"ERROR: Could not verify token via API or token is invalid. Original error: {e}.", file=sys.stderr) | ||
| return 1 | ||
|
|
||
| if api_token_response.can_rotate: | ||
| print("WARNING: token can be rotated. Consider rotating it before it expires.", file=sys.stderr) | ||
|
|
||
| print("OK: Token keep-alive successful", file=sys.stdout) | ||
| return 0 | ||
|
|
||
|
|
||
| def build_parser() -> argparse.ArgumentParser: | ||
| parser = argparse.ArgumentParser( | ||
| prog="rtbhouse_sdk", | ||
| description="RTB House Python SDK CLI", | ||
| ) | ||
| sub = parser.add_subparsers(dest="command", required=True) | ||
|
|
||
| api_token = sub.add_parser( | ||
| "api-token", | ||
| help="API token utilities", | ||
| ) | ||
| api_token_sub = api_token.add_subparsers( | ||
| dest="api_token_cmd", | ||
| required=True, | ||
| ) | ||
| build_cmd_init_json(api_token_sub) | ||
| build_cmd_keep_alive_json(api_token_sub) | ||
| build_cmd_keep_alive(api_token_sub) | ||
|
|
||
| return parser | ||
|
|
||
|
|
||
| def build_cmd_init_json(parser: Any) -> None: | ||
| init_json = parser.add_parser( | ||
| "init-json", | ||
| help="Initialize API token JSON file storage", | ||
| ) | ||
| init_json.add_argument( | ||
| "--token", | ||
| default=None, | ||
| help="Token value (discouraged). Prefer stdin/env/pipe.", | ||
| ) | ||
| init_json.add_argument( | ||
| "--path", | ||
| default=DEFAULT_JSON_FILE_PATH, | ||
| help=f"Path to token JSON file (default: {DEFAULT_JSON_FILE_PATH})", | ||
| ) | ||
| init_json.add_argument( | ||
| "--env-var", | ||
| help="Environment variable to read token from.", | ||
| ) | ||
| init_json.set_defaults(func=cmd_init_json) | ||
|
|
||
|
|
||
| def build_cmd_keep_alive_json(parser: Any) -> None: | ||
| keep_alive_json = parser.add_parser( | ||
| "keep-alive-json", | ||
| help="Keep alive API token stored in JSON file storage by bumping its usage. Allows automatic rotation.", | ||
| ) | ||
| keep_alive_json.add_argument( | ||
| "--path", | ||
| default=DEFAULT_JSON_FILE_PATH, | ||
| help=f"Path to token JSON file (default: {DEFAULT_JSON_FILE_PATH})", | ||
| ) | ||
| keep_alive_json.add_argument( | ||
| "--auto-rotate", | ||
| action="store_true", | ||
| help="Enable automatic rotation if token is in rotation window", | ||
| ) | ||
| keep_alive_json.set_defaults(func=cmd_keep_alive_json) | ||
|
|
||
|
|
||
| def build_cmd_keep_alive(parser: Any) -> None: | ||
| keep_alive = parser.add_parser( | ||
| "keep-alive", | ||
| help="Keep alive API token by bumping its usage.", | ||
| ) | ||
| keep_alive.add_argument( | ||
| "--token", | ||
| default=None, | ||
| help="Token value (discouraged). Prefer stdin/env/pipe.", | ||
| ) | ||
| keep_alive.add_argument( | ||
| "--env-var", | ||
| default=None, | ||
| help="Environment variable to read token from.", | ||
| ) | ||
| keep_alive.set_defaults(func=cmd_keep_alive) | ||
|
|
||
|
|
||
| def main(argv: list[str] | None = None) -> int: | ||
| parser = build_parser() | ||
| args = parser.parse_args(argv) | ||
| return int(args.func(args)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Czy mamy jakiś breaking change? Jeśli tak - warto go tu opisać wraz z instrukcją migracji. Jeśli nie - może wystarczy zrobić z tego wersję 15.1.0?