Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions flareio/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,35 @@
from flareio.version import __version__ as _flareio_version


_API_DOMAIN_DEFAULT: str = "api.flare.io"
_ALLOWED_API_DOMAINS: t.Tuple[str, ...] = (
_API_DOMAIN_DEFAULT,
"api.eu.flare.io",
)


class FlareApiClient:
def __init__(
self,
*,
api_key: str,
tenant_id: t.Optional[int] = None,
session: t.Optional[requests.Session] = None,
api_domain: t.Optional[str] = None,
_enable_beta_features: bool = False,
) -> None:
if not api_key:
raise Exception("API Key cannot be empty.")

api_domain = api_domain or _API_DOMAIN_DEFAULT
if api_domain not in _ALLOWED_API_DOMAINS:
raise Exception(

Choose a reason for hiding this comment

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

I would raise ValueError instead of a generic Exception

Copy link
Member Author

Choose a reason for hiding this comment

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

Does it matter? Is it an exception that will be caught?

Choose a reason for hiding this comment

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

It matters for the tests. The test actually catches a generic Exception and will fail to fail if the exception is raised by a coding error, whereas with a ValueError code errors can be caught. Our API users can also better understand that the exception was raised because of a value that was provided by them instead of being an error that we caused in our code.

Copy link
Member Author

@aviau aviau Sep 16, 2025

Choose a reason for hiding this comment

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

But the tests match the exact exception message:

with pytest.raises(Exception, match="Invalid API domain"):

Copy link
Member Author

Choose a reason for hiding this comment

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

I would argue that setting a specific exception type will increase our API surface? Meaning that our users might start writing code that handles this exception differently. Are we sure we want to support that? Are we willing to commit to it forever?

f"Invalid API domain: {api_domain}. Only {_ALLOWED_API_DOMAINS} are supported."
)
if api_domain != _API_DOMAIN_DEFAULT and not _enable_beta_features:
raise Exception("Custom API domains considered a beta feature.")
self._api_domain: str = api_domain

self._api_key: str = api_key
self._tenant_id: t.Optional[int] = tenant_id

Expand Down Expand Up @@ -93,7 +112,7 @@ def generate_token(self) -> str:
}

resp = self._session.post(
"https://api.flare.io/tokens/generate",
f"https://{self._api_domain}/tokens/generate",
json=payload,
headers={
"Authorization": self._api_key,
Expand Down Expand Up @@ -128,12 +147,12 @@ def _request(
json: t.Optional[t.Dict[str, t.Any]] = None,
headers: t.Optional[t.Dict[str, t.Any]] = None,
) -> requests.Response:
url = urljoin("https://api.flare.io", url)
url = urljoin(f"https://{self._api_domain}", url)

netloc: str = urlparse(url).netloc
if not netloc == "api.flare.io":
if not netloc == self._api_domain:
raise Exception(
f"Client was used to access {netloc=} at {url=}. Only the domain api.flare.io is supported."
f"Client was used to access {netloc=} at {url=}. Only the domain {self._api_domain} is supported."
)

headers = {
Expand Down
13 changes: 13 additions & 0 deletions tests/test_api_client_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ def test_create_client() -> None:
FlareApiClient(api_key="test")


def test_create_client_eu() -> None:
FlareApiClient(
api_key="test",
api_domain="api.eu.flare.io",
_enable_beta_features=True,
)


def test_create_client_bad_api_domain() -> None:
with pytest.raises(Exception, match="Invalid API domain"):
FlareApiClient(api_key="test", api_domain="bad.com")


def test_create_client_empty_api_key() -> None:
with pytest.raises(Exception, match="API Key cannot be empty."):
FlareApiClient(
Expand Down
15 changes: 15 additions & 0 deletions tests/test_api_client_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ def test_get_path_only() -> None:
assert mocker.last_request.url == "https://api.flare.io/hello/test"


def test_get_eu_domain() -> None:
client = get_test_client(
api_domain="api.eu.flare.io",
_enable_beta_features=True,
)
with requests_mock.Mocker() as mocker:
mocker.register_uri(
"GET",
"https://api.eu.flare.io/hello/test",
status_code=200,
)
client.get("/hello/test")
assert mocker.last_request.url == "https://api.eu.flare.io/hello/test"


def test_get_user_agent() -> None:
client = get_test_client()
with requests_mock.Mocker() as mocker:
Expand Down
6 changes: 5 additions & 1 deletion tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@ def get_test_client(
*,
tenant_id: t.Optional[int] = None,
authenticated: bool = True,
api_domain: t.Optional[str] = None,
_enable_beta_features: bool = False,
) -> FlareApiClient:
client = FlareApiClient(
api_key="test-api-key",
tenant_id=tenant_id,
api_domain=api_domain,
_enable_beta_features=_enable_beta_features,
)

if authenticated:
with requests_mock.Mocker() as mocker:
mocker.register_uri(
"POST",
"https://api.flare.io/tokens/generate",
f"https://{client._api_domain}/tokens/generate",
json={
"token": "test-token-hello",
},
Expand Down