Skip to content

Commit

Permalink
Allow specifying base_url as a BaseClient class attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
derek-globus committed Jan 9, 2025
1 parent d614201 commit 7b9421e
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 29 deletions.
69 changes: 42 additions & 27 deletions src/globus_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import typing as t
import urllib.parse

from globus_sdk import config, exc, utils
from globus_sdk import GlobusSDKUsageError, config, exc, utils
from globus_sdk._types import ScopeCollectionType
from globus_sdk.authorizers import GlobusAuthorizer
from globus_sdk.paging import PaginatorTable
Expand Down Expand Up @@ -39,7 +39,8 @@ class BaseClient:
If both``app`` and ``app_name`` are given, this value takes priority.
:param base_url: The URL for the service. Most client types initialize this value
intelligently by default. Set it when inheriting from BaseClient or
communicating through a proxy.
communicating through a proxy. This value takes precedence over the class
attribute of the same name.
:param transport_params: Options to pass to the transport for this client
All other parameters are for internal use and should be ignored.
Expand All @@ -48,6 +49,11 @@ class BaseClient:
# service name is used to lookup a service URL from config
service_name: str = "_base"

# the URL for the service
# NOTE: this is not the only way to define a base url. See the docstring of the
# `BaseClient._resolve_base_url` method for more details.
base_url: str = "_base"

# path under the client base URL
# NOTE: using this attribute is now considered bad practice for client definitions,
# as it prevents calls to new routes at the root of an API's base_url
Expand Down Expand Up @@ -92,31 +98,10 @@ def __init__(
# GLOBUS_SDK_ENVIRONMENT environment variable.
self.environment = config.get_environment_name(environment)

if self.service_name == "_base":
# service_name=="_base" means that either there was no inheritance (direct
# instantiation of BaseClient), or the inheriting class operates outside of
# the existing service_name->URL mapping paradigm
# in these cases, base_url must be set explicitly
log.info(f"Creating client of type {type(self)}")
if base_url is None:
raise NotImplementedError(
"Cannot instantiate clients which do not set a 'service_name' "
"unless they explicitly set 'base_url'."
)
else:
# if service_name is set, it can be used to deduce a base_url
# *if* necessary
log.info(
f"Creating client of type {type(self)} for "
f'service "{self.service_name}"'
)
if base_url is None:
base_url = config.get_service_url(
self.service_name, environment=self.environment
)

# append the base_path to the base URL
self.base_url: str = utils.slash_join(base_url, self.base_path)
# resolve the base_url for the client (see docstring for resolution precedence)
self.base_url = self._resolve_base_url(base_url, self.environment)
# append the base_path to the base_url if necessary
self.base_url = utils.slash_join(self.base_url, self.base_path)

self.transport = self.transport_class(**(transport_params or {}))
log.debug(f"initialized transport of type {type(self.transport)}")
Expand Down Expand Up @@ -161,6 +146,36 @@ def default_scope_requirements(self) -> list[Scope]:
"""
raise NotImplementedError

@classmethod
def _resolve_base_url(cls, init_base_url: str | None, environment: str) -> str:
"""
Resolve the client's base url.
Precedence (this evaluation will fall through if an option is not set):
1. [Highest] Constructor `base_url` value.
2. Class `base_url` attribute.
3. Class `service_name` attribute (computed).
:param init_base_url: The `base_url` value supplied to the constructor.
:param environment: The environment to use for service URL resolution.
:returns: The resolved base URL.
:raises: GlobusSDKUsageError if base_url cannot be resolved.
"""
if init_base_url is not None:
log.info(f"Creating client of type {cls}")
return init_base_url
elif cls.base_url != "_base":
log.info(f"Creating client of type {cls}")
return cls.base_url
elif cls.service_name != "_base":
log.info(f'Creating client of type {cls} for service "{cls.service_name}"')
return config.get_service_url(cls.service_name, environment)

raise GlobusSDKUsageError(
f"Unable to resolve base_url in client {cls}. "
f"Clients must define either one or both of 'base_url' and 'service_name'."
)

def _finalize_app(self) -> None:
if self._app:
if self.resource_server is None:
Expand Down
32 changes: 30 additions & 2 deletions tests/unit/test_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest

import globus_sdk
from globus_sdk import GlobusApp, GlobusAppConfig, UserApp
from globus_sdk import GlobusApp, GlobusAppConfig, GlobusSDKUsageError, UserApp
from globus_sdk._testing import RegisteredResponse, get_last_request
from globus_sdk.authorizers import NullAuthorizer
from globus_sdk.scopes import Scope, TransferScopes
Expand Down Expand Up @@ -42,7 +42,7 @@ def base_client(base_client_class):

def test_cannot_instantiate_plain_base_client():
# attempting to instantiate a BaseClient errors
with pytest.raises(NotImplementedError):
with pytest.raises(GlobusSDKUsageError):
globus_sdk.BaseClient()


Expand All @@ -54,6 +54,34 @@ def test_can_instantiate_base_client_with_explicit_url():
assert client.base_url == "https://example.org/"


def test_can_instantiate_with_base_url_class_attribute():
class MyCoolClient(globus_sdk.BaseClient):
base_url = "https://example.org"

client = MyCoolClient()
assert client.base_url == "https://example.org/"


def test_base_url_resolution_precedence():
"""
Base URL can come from one of 3 different places; this test asserts that we maintain
a consistent precedence between the three
(init-base_url > class-base_url > class-service_name)
"""

class BothAttributesClient(globus_sdk.BaseClient):
base_url = "class-base"
service_name = "service-name"

class OnlyServiceClient(globus_sdk.BaseClient):
service_name = "service-name"

# All 3 are set
assert BothAttributesClient(base_url="init-base").base_url == "init-base/"
assert BothAttributesClient().base_url == "class-base/"
assert OnlyServiceClient().base_url == "https://service-name.api.globus.org/"


def test_set_http_timeout(base_client):
class FooClient(globus_sdk.BaseClient):
service_name = "foo"
Expand Down

0 comments on commit 7b9421e

Please sign in to comment.