Skip to content

API tokens authentication in SDK (APPS-14746)#191

Open
jszymanski-rtbh wants to merge 5 commits intomasterfrom
APPS-14746-api-tokens-in-sdk
Open

API tokens authentication in SDK (APPS-14746)#191
jszymanski-rtbh wants to merge 5 commits intomasterfrom
APPS-14746-api-tokens-in-sdk

Conversation

@jszymanski-rtbh
Copy link
Contributor

  • Added ApiTokenAuth for authenticating with a fixed API token.
  • Introduced DynamicApiTokenAuth / AsyncDynamicApiTokenAuth for provider-based authentication with per-request token resolution. Tokens are persisted in storage and automatically rotated when entering the rotation window.
  • Implemented token endpoints clients (sync + async): heartbeat() and rotate().
  • Added token managers (sync + async) providing:
    • rotation/expiration handling,
    • configure() and configure_from_env() helpers.
  • Added token storage backends:
    • InMemoryApiTokenStorage and JsonFileApiTokenStorage,
    • async variants for both.

@jszymanski-rtbh jszymanski-rtbh requested a review from a team as a code owner February 2, 2026 09:20
@jszymanski-rtbh jszymanski-rtbh removed the request for review from a team February 2, 2026 09:21
@jszymanski-rtbh
Copy link
Contributor Author

Testy oraz więcej przykładów używania w changelog, dodam po wstępnym zaakceptowaniu architektury rozwiązania.

- Introduced `DynamicApiTokenAuth` / `AsyncDynamicApiTokenAuth` for provider-based authentication with per-request token resolution. Tokens are persisted in storage and automatically rotated when entering the rotation window.
- Implemented token endpoints clients (sync + async): `heartbeat()` and `rotate()`.
- Added token managers (sync + async) providing:
  - rotation/expiration handling,
  - `configure()` and `configure_from_env()` helpers.
- Added token storage backends:
  - `InMemoryApiTokenStorage` and `JsonFileApiTokenStorage`,
  - async variants for both.
@jszymanski-rtbh jszymanski-rtbh force-pushed the APPS-14746-api-tokens-in-sdk branch from 562ac52 to 366d5b3 Compare February 2, 2026 09:31
Comment on lines +84 to +112
def _atomic_write_text(self, text: str) -> None:
descriptor, temp_path = tempfile.mkstemp(
prefix=self._path.name + ".",
dir=self._path.parent,
)
try:
with os.fdopen(
descriptor,
"w",
encoding="utf-8",
) as file:
file.write(text)
file.flush()
os.fsync(file.fileno())

try:
os.chmod(temp_path, 0o600)
except OSError:
pass

os.replace(temp_path, self._path)

finally:
try:
os.remove(temp_path)
except FileNotFoundError:
pass
except OSError:
pass
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@peku33 peku33 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wygląda nieźle, zostawiłem trochę komentarzy 💪

Na pewno warto unormować sposób inicjalizacji i deklaracji zmiennych w klasach:

  • czasami wołasz super().__init__(), a czasami nie. Proponuję wołać zawsze, nawet jeśli dziedziczymy tylko po object. Dzięki temu jeśli przypadkiem pojawi nam się kiedyś klasa bazowa z jakimiś zmiennymi - nie pominiemy przypadkiem wywołania, bo linter nam to złapie
  • proponuję generalnie zadeklarować składowe w klasie, żeby patrząc na samą klasę było mniej więcej widać z czego się składa.

except FileNotFoundError:
pass

def _atomic_write_text(self, text: str) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wydaje mi się, że to nie rozwiązuje nam do końca problemu. O ile lock na managerze jest sensowny, to chyba nie chroni nas nijak przed różnymi wątkami/procesami aktualizaującymi ten sam plik. To chyba nie jest jakiś straszny problem, ale warto mieć go z tyłu głowy.

Swoją drogą - może powinniśmy w ogóle mieć tu metodę typu update_with() która jest contextmanagerem zapewniającym lock na źródle?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chyba kwestię tego locka na pliku możemy odpuścić. Manager uruchamia zapis do pliku (save) tylko po udanej rotacji. Nasze API przepuści tylko jeden request z rotacją.

Tutaj tym atomic_write chciałem poradzić sobie z sytuacją jakiegoś połowicznego odczytu/zapisu który skończyłby się błędem

@jszymanski-rtbh jszymanski-rtbh force-pushed the APPS-14746-api-tokens-in-sdk branch from 4b52b7e to 83c6afc Compare March 9, 2026 09:28
@jszymanski-rtbh jszymanski-rtbh requested a review from peku33 March 9, 2026 09:44
Comment on lines +31 to +32
cachetools = "^7.0.3"
types-cachetools = "^6.2.0.20251022"
Copy link
Contributor

Choose a reason for hiding this comment

The 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

@@ -1,3 +1,29 @@
# v16.0.0
Copy link
Contributor

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Czy potrzebujemy tego pliku? Może jednak wystarczy __main__.py w api_tokens wywołany bezpośrednio?

Comment on lines +117 to +121
try:
resp_json = response.json()
return resp_json["data"]
except (ValueError, KeyError) as exc:
raise ApiException("Invalid response format") from exc
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To nam się w sumie powtarza pomiędzy miejscami, więc można:

  • zamknąć to w _validate_response
  • zamiast _get i _post mieć `_request(method, path, params, data)

except (ValueError, KeyError) as exc:
raise ApiException("Invalid response format") from exc

def _post(self, path: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None) -> Any:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Na logikę params powinno iść po path.

Comment on lines +129 to +130
def _post_dict(
self, path: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

analogicznie tu i w drugim kliencie

Comment on lines +492 to +498
async def get_current_api_token(self) -> schema.ApiToken:
data = await self._get_dict("/tokens/current")
return schema.ApiToken(**data)

async def rotate_current_api_token(self) -> schema.RotatedApiToken:
data = await self._post_dict("/tokens/current/rotate")
return schema.RotatedApiToken(**data)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Te modele to nie jest bardziej jakiś "ApiTokenDetails" vs "ApiTokenRotateResult"?

return schema.RotatedApiToken(**data)


class _HttpxApiTokenAuth(httpx.Auth):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prośba o podeklarowanie składowych w klasach i wołanie super.init()

Copy link
Contributor

Choose a reason for hiding this comment

The 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 clicka (opcjonalnie typera, ale ten jest zdaje się dużo cięższy i m awięcej zależności).

Mamy obecnie coś takiego:

  • handler 1
  • handler 2
  • handler 3
  • builder
  • argsy 1
  • argsy 2
  • argsy 3
  • wołanie buildera

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants