Skip to content
Open
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
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ account [here](https://www.checkout.com/get-test-account).

**PLEASE NEVER SHARE OR PUBLISH YOUR CHECKOUT CREDENTIALS.**

### Subdomain value

Requests must be made through your merchant-specific subdomain (MSSD): the first 8 characters of your client ID (excluding `cli_`). For example, if your client ID is `cli_vkuhvk4vjn2edkps7dfsq6emqm`, your subdomain is `vkuhvk4v`. When `environment_subdomain` is set the SDK sends requests to `https://vkuhvk4v.api.checkout.com`. See [Base URLs](https://api-reference.checkout.com/#section/Base-URLs) and [API endpoints](https://www.checkout.com/docs/developer-resources/api/api-endpoints) for further details, and for where to find your unique client ID.

### Default

Default keys client instantiation can be done as follows:
Expand All @@ -82,7 +86,7 @@ def default():
.secret_key('secret_key')
.public_key('public_key') # optional, only required for operations related with tokens
.environment(Environment.sandbox()) # or production()
.environment_subdomain("subdomain") # optional, Merchant-specific DNS name
.environment_subdomain("subdomain") # required, Merchant-specific DNS name, the first 8 characters of your client ID
.build()

payments_client = checkout_api.payments
Expand All @@ -105,7 +109,7 @@ def oauth():
.oauth()
.client_credentials(client_id='client_id', client_secret='client_secret')
.environment(Environment.sandbox()) # or production()
.environment_subdomain("subdomain") # optional, Merchant-specific DNS name
.environment_subdomain("subdomain") # required, Merchant-specific DNS name, the first 8 characters of your client ID
.scopes([OAuthScopes.GATEWAY_PAYMENT_REFUNDS, OAuthScopes.FILES]) # optional, array of scopes
.build()

Expand All @@ -129,7 +133,7 @@ def previous():
.secret_key('secret_key')
.public_key('public_key') # optional, only required for operations related with tokens
.environment(Environment.sandbox()) # or production()
.environment_subdomain("subdomain") # optional, Merchant-specific DNS name
.environment_subdomain("subdomain") # optional for the Previous platform, Merchant-specific DNS name
.build()

payments_client = checkout_api.payments
Expand Down Expand Up @@ -175,7 +179,7 @@ def oauth():
.oauth()
.client_credentials(client_id='client_id', client_secret='client_secret')
.environment(Environment.sandbox()) # or production()
.environment_subdomain("subdomain") # optional, Merchant-specific DNS name
.environment_subdomain("subdomain") # required, Merchant-specific DNS name, the first 8 characters of your client ID
.http_client_builder(CustomHttpClientBuilder()) # optional
.scopes([OAuthScopes.GATEWAY_PAYMENT_REFUNDS, OAuthScopes.FILES]) # optional, array of scopes
.build()
Expand Down Expand Up @@ -267,6 +271,22 @@ The execution of integration tests require the following environment variables s
* For Previous account systems: `CHECKOUT_PREVIOUS_PUBLIC_KEY` & `CHECKOUT_PREVIOUS_SECRET_KEY`
* Processing channel: `CHECKOUT_PROCESSING_CHANNEL_ID`

## Legacy domain (emergency use only)

> :warning: **Only use if merchant specific sub domains are causing issues.** Connecting through your merchant-specific subdomain (see [Subdomain value](#subdomain-value)) is the supported way of using the Checkout.com API, and non-subdomain usage will be deprecated.

If, in exceptional circumstances, you cannot use your merchant-specific subdomain, you can explicitly opt out by calling `use_legacy_domain()` instead of `environment_subdomain(...)`:

```python
checkout_api = CheckoutSdk.builder() \
.secret_key("secret_key") \
.environment(Environment.sandbox()) \
.use_legacy_domain() \
.build()
```

This routes requests to `api.checkout.com` (or `api.sandbox.checkout.com`) and `access.checkout.com` (or `access.sandbox.checkout.com`). The method raises a `DeprecationWarning`, so `python -W error::DeprecationWarning` and most linters will flag it. Exactly one of `environment_subdomain(...)` or `use_legacy_domain()` must be set: the SDK raises a `CheckoutArgumentException` if both, or neither, are. The Previous (ABC) platform predates merchant-specific subdomains and is exempt from this requirement.

## Code of Conduct

Please refer to [Code of Conduct](CODE_OF_CONDUCT.md)
Expand Down
57 changes: 52 additions & 5 deletions checkout_sdk/checkout_sdk_builder.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,81 @@
from __future__ import absolute_import

import warnings
from typing import Optional

from checkout_sdk.default_http_client import DefaultHttpClientBuilder
from checkout_sdk.environment import Environment
from checkout_sdk.environment_subdomain import EnvironmentSubdomain
from checkout_sdk.exception import CheckoutArgumentException
from checkout_sdk.http_client_interface import HttpClientBuilderInterface


class CheckoutSdkBuilder:

def __init__(self):
self._environment = Environment.sandbox()
self._environment_subdomain = None
self._subdomain = None
self._use_legacy_domain = False
self._http_client = DefaultHttpClientBuilder().get_client()

def environment(self, environment: Environment):
self._environment = environment
return self

def environment_subdomain(self, subdomain: Optional[str]):
if subdomain:
self._environment_subdomain = EnvironmentSubdomain(self._environment, subdomain)
else:
self._environment_subdomain = None
self._subdomain = subdomain
return self

def use_legacy_domain(self):
"""
Opts out of the merchant-specific subdomain, sending every request to the shared hosts
instead (api.checkout.com and access.checkout.com, or their sandbox equivalents).

Deprecated: this is an emergency fallback for the rare case where the merchant-specific
subdomain cannot be used, and will be removed in a future release. Call
environment_subdomain() instead.
See https://api-reference.checkout.com/#section/Base-URLs
"""
warnings.warn(
'use_legacy_domain() is deprecated and will be removed in a future release. It is '
'intended only as an emergency fallback when the merchant-specific subdomain cannot '
'be used. Call environment_subdomain() instead. See '
'https://api-reference.checkout.com/#section/Base-URLs',
DeprecationWarning,
stacklevel=2)
self._use_legacy_domain = True
return self

def http_client_builder(self, http_client_builder: HttpClientBuilderInterface):
self._http_client = http_client_builder.get_client()
return self

@property
def _environment_subdomain(self) -> Optional[EnvironmentSubdomain]:
if self._subdomain is None:
return None
return EnvironmentSubdomain(self._environment, self._subdomain)

def _requires_environment_subdomain(self) -> bool:
"""
Whether this builder requires the merchant-specific subdomain to be configured. The
Previous (ABC) platform predates merchant-specific subdomains, so it overrides this to
False.
"""
return True

def _validate_environment_settings(self):
if self._subdomain is not None and self._use_legacy_domain:
raise CheckoutArgumentException(
'environment_subdomain and use_legacy_domain cannot both be set - provide only '
'your merchant-specific subdomain')
if self._subdomain is None and not self._use_legacy_domain and self._requires_environment_subdomain():
raise CheckoutArgumentException(
'environment_subdomain is required - provide your merchant-specific subdomain '
'(the first 8 characters of your client ID, see '
'https://api-reference.checkout.com/#section/Base-URLs), or call '
'use_legacy_domain() to opt out only if merchant specific sub domains are '
'causing issues')

def build(self):
raise NotImplementedError()
17 changes: 6 additions & 11 deletions checkout_sdk/default_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,10 @@ def oauth():
def build(self):
validate_secret_key(self._SECRET_KEY_PATTERN, self._secret_key)
validate_public_key(self._PUBLIC_KEY_PATTERN, self._public_key)
if self._environment_subdomain is not None:
configuration = CheckoutConfiguration(
credentials=DefaultKeysSdkCredentials(secret_key=self._secret_key, public_key=self._public_key),
environment=self._environment,
http_client=self._http_client,
environment_subdomain=self._environment_subdomain)
else:
configuration = CheckoutConfiguration(
credentials=DefaultKeysSdkCredentials(secret_key=self._secret_key, public_key=self._public_key),
environment=self._environment,
http_client=self._http_client)
self._validate_environment_settings()
configuration = CheckoutConfiguration(
credentials=DefaultKeysSdkCredentials(secret_key=self._secret_key, public_key=self._public_key),
environment=self._environment,
http_client=self._http_client,
environment_subdomain=self._environment_subdomain)
return CheckoutApi(configuration)
53 changes: 28 additions & 25 deletions checkout_sdk/environment_subdomain.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from urllib.parse import urlparse, urlunparse

from checkout_sdk.environment import Environment
from checkout_sdk.exception import CheckoutArgumentException


class EnvironmentSubdomain:
Expand All @@ -12,39 +13,41 @@ def __init__(self, environment: Environment, subdomain: str):
@staticmethod
def create_url_with_subdomain(original_url: str, subdomain: str):
"""
Applies subdomain transformation to any given URL.
If the subdomain is valid (alphanumeric pattern), prepends it to the host.
Otherwise, returns the original URL unchanged.
Applies subdomain transformation to any given URL, prepending the subdomain to the host.

Args:
original_url: the original URL to transform
subdomain: the subdomain to prepend

Returns:
the transformed URL with subdomain, or original URL if subdomain is invalid
"""
new_environment = original_url
the transformed URL with subdomain

Raises:
CheckoutArgumentException: if the subdomain is not a valid merchant-specific subdomain
"""
regex = r'^(?:pl-)?[a-z0-9]+$'
if re.match(regex, subdomain):
url_parts = urlparse(original_url)
if url_parts.port:
new_host = subdomain + '.' + url_parts.hostname + ':' + str(url_parts.port)
else:
new_host = subdomain + '.' + url_parts.hostname

new_url_parts = (
url_parts.scheme,
new_host,
url_parts.path,
url_parts.params,
url_parts.query,
url_parts.fragment
)

new_environment = urlunparse(new_url_parts)

return new_environment
if subdomain is None or not re.match(regex, subdomain):
raise CheckoutArgumentException(
'invalid environment subdomain - provide your merchant-specific subdomain, the '
'first 8 characters of your client ID (see '
'https://api-reference.checkout.com/#section/Base-URLs)')

url_parts = urlparse(original_url)
if url_parts.port:
new_host = subdomain + '.' + url_parts.hostname + ':' + str(url_parts.port)
else:
new_host = subdomain + '.' + url_parts.hostname

new_url_parts = (
url_parts.scheme,
new_host,
url_parts.path,
url_parts.params,
url_parts.query,
url_parts.fragment
)

return urlunparse(new_url_parts)

def base_uri(self) -> str:
return self.base_uri
38 changes: 15 additions & 23 deletions checkout_sdk/oauth_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,35 +31,27 @@ def scopes(self, scopes: list):
return self

def build(self):
self._validate_environment_settings()
environment_subdomain = self._environment_subdomain

# Determine the authorization URI based on subdomain configuration
if self._environment_subdomain is not None:
authorization_uri = self._environment_subdomain.authorization_uri
if environment_subdomain is not None:
authorization_uri = environment_subdomain.authorization_uri
else:
authorization_uri = self._environment.authorization_uri

# Use custom authorization URI if explicitly provided
if self._authorization_uri:
authorization_uri = self._authorization_uri

if self._environment_subdomain is not None:
configuration = CheckoutConfiguration(
credentials=OAuthSdkCredentials.init(http_client=self._http_client,
environment=self._environment,
client_id=self._client_id,
client_secret=self._client_secret,
scopes=self._scopes,
authorization_uri=authorization_uri),
environment=self._environment,
http_client=self._http_client,
environment_subdomain=self._environment_subdomain)
else:
configuration = CheckoutConfiguration(
credentials=OAuthSdkCredentials.init(http_client=self._http_client,
environment=self._environment,
client_id=self._client_id,
client_secret=self._client_secret,
scopes=self._scopes,
authorization_uri=authorization_uri),
environment=self._environment,
http_client=self._http_client)
configuration = CheckoutConfiguration(
credentials=OAuthSdkCredentials.init(http_client=self._http_client,
environment=self._environment,
client_id=self._client_id,
client_secret=self._client_secret,
scopes=self._scopes,
authorization_uri=authorization_uri),
environment=self._environment,
http_client=self._http_client,
environment_subdomain=environment_subdomain)
return CheckoutApi(configuration)
22 changes: 11 additions & 11 deletions checkout_sdk/previous/previous_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,18 @@ class PreviousSdk(PreviousStaticKeys):
def __init__(self):
super().__init__()

# The Previous (ABC) platform predates merchant-specific subdomains, so it is exempt from
# the mandatory environment_subdomain/use_legacy_domain configuration.
def _requires_environment_subdomain(self) -> bool:
return False

def build(self):
validate_secret_key(self._SECRET_KEY_PATTERN, self._secret_key)
validate_public_key(self._PUBLIC_KEY_PATTERN, self._public_key)
if self._environment_subdomain is not None:
configuration = CheckoutConfiguration(
credentials=PreviousKeysSdkCredentials(secret_key=self._secret_key, public_key=self._public_key),
environment=self._environment,
http_client=self._http_client,
environment_subdomain=self._environment_subdomain)
else:
configuration = CheckoutConfiguration(
credentials=PreviousKeysSdkCredentials(secret_key=self._secret_key, public_key=self._public_key),
environment=self._environment,
http_client=self._http_client)
self._validate_environment_settings()
configuration = CheckoutConfiguration(
credentials=PreviousKeysSdkCredentials(secret_key=self._secret_key, public_key=self._public_key),
environment=self._environment,
http_client=self._http_client,
environment_subdomain=self._environment_subdomain)
return CheckoutApi(ApiClient(configuration, configuration.environment.base_uri), configuration)
11 changes: 8 additions & 3 deletions tests/accounts/accounts_integration_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import absolute_import

import warnings
import os
from datetime import datetime, timedelta, timezone

Expand All @@ -21,13 +22,17 @@

@pytest.fixture(scope='class')
def accounts_checkout_api():
return CheckoutSdk \
builder = CheckoutSdk \
.builder() \
.oauth() \
.client_credentials(client_id=os.environ.get('CHECKOUT_DEFAULT_OAUTH_ACCOUNTS_CLIENT_ID'),
client_secret=os.environ.get('CHECKOUT_DEFAULT_OAUTH_ACCOUNTS_CLIENT_SECRET')) \
.scopes([OAuthScopes.ACCOUNTS, OAuthScopes.FILES]) \
.build()
.scopes([OAuthScopes.ACCOUNTS, OAuthScopes.FILES])
# The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so the
# token request would come back invalid_client. Opting out explicitly until they are.
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
return builder.use_legacy_domain().build()


def test_should_create_get_and_update_onboard_entity(accounts_checkout_api):
Expand Down
11 changes: 8 additions & 3 deletions tests/accounts/accounts_payout_schedules_integration_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import absolute_import

import warnings
import os

import pytest
Expand All @@ -14,13 +15,17 @@

@pytest.fixture(scope='class')
def payout_schedules_api():
return CheckoutSdk \
builder = CheckoutSdk \
.builder() \
.oauth() \
.client_credentials(client_id=os.environ.get('CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_ID'),
client_secret=os.environ.get('CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_SECRET')) \
.scopes([OAuthScopes.MARKETPLACE]) \
.build()
.scopes([OAuthScopes.MARKETPLACE])
# The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so the
# token request would come back invalid_client. Opting out explicitly until they are.
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
return builder.use_legacy_domain().build()


@pytest.mark.skip(reason='not available')
Expand Down
Loading
Loading