Skip to content
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

Allow specifying base_url as a BaseClient class attribute #1125

Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

Added
~~~~~

- Subclasses of ``BaseClient`` may now specify ``base_url`` as class attribute. (:pr:`NUMBER`)


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
Loading