Skip to content

API Reference

This part of the documentation covers the classes and interfaces of the 10Duke Scale SDk.

The main functionality of the library is access via the various API clients.

There are helpers for configuring the SDK to access the API, authorization of API calls, and authentication of users via OAuth/Open ID Connect flows.

Configuration

The TendukeConfig class can be used to set configuration values. It can additionally read configuration items from a file or environment variables.

Authorization

Authorization for the 10Duke Scale API is achieved using ID Tokens, Scale JWT tokens, or using license keys in the API requests.

Classes are provided to implement ID token or Scale JWT Authorization.

Additionally, helpers are provided for OAuth and Open ID Connect flows:

Other flows are left for the application to implement. Sample code is included for reference. These can be used with or without deriving from OAuthClient

Authentication and Authorization.

ScaleJwtAuth

Bases: AuthBase

Scale JWT Auth hook - for use in confidential applications.

Source code in tenduke_scale/auth/scale_jwt_auth_provider.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
class ScaleJwtAuth(AuthBase):
    """Scale JWT Auth hook - for use in confidential applications."""

    def __init__(
        self,
        key_id: str,
        private_key: str,
        claims: ScaleJwtClaims,
        config: TendukeConfig,
    ) -> None:
        """Construct a ScaleJwtAuth provider (hook).

        Args:
            key_id: The id of the key pair to use to sign the JWT.
            private_key: The private half of of the key pair to use to sign the JWT.
            claims: The authorization claims to be included in the JWT.
            config: Configuration object specifying token_refresh_leeway_seconds.
        """
        _validate_claims(claims)
        self._key_id = key_id
        self._private_key = private_key
        self._claims = claims
        self._make_jwt()
        self._token_leeway_seconds = config.token_refresh_leeway_seconds

    def _make_jwt(self):
        payload = _get_payload(self._claims)
        self._expiry = datetime.fromtimestamp(payload["exp"], UTC)
        self.jwt = encode(
            payload, self._private_key, algorithm="RS256", headers={"kid": self._key_id}
        )

    def __call__(self, r: PreparedRequest):
        """Mutate outgoing request (adding authorization header).

        Args:
            r: Outgoing request.
        """
        if (
            self._expiry - datetime.now(UTC)
        ).total_seconds() < self._token_leeway_seconds:
            self._make_jwt()
        r.headers["Authorization"] = f"ScaleJwt {self.jwt}"
        return r

    def __eq__(self, other):
        """Return True if instances are equal; otherwise False.

        Equality is tested by comparing the JWTs.
        """
        return self.jwt == getattr(other, "jwt", None)

    def __ne__(self, other):
        """Return True if instances are not equal; otherwise False.

        Equality is tested by comparing the JWTs.
        """
        return not self == other

__call__(r)

Mutate outgoing request (adding authorization header).

Parameters:

Name Type Description Default
r PreparedRequest

Outgoing request.

required
Source code in tenduke_scale/auth/scale_jwt_auth_provider.py
112
113
114
115
116
117
118
119
120
121
122
123
def __call__(self, r: PreparedRequest):
    """Mutate outgoing request (adding authorization header).

    Args:
        r: Outgoing request.
    """
    if (
        self._expiry - datetime.now(UTC)
    ).total_seconds() < self._token_leeway_seconds:
        self._make_jwt()
    r.headers["Authorization"] = f"ScaleJwt {self.jwt}"
    return r

__eq__(other)

Return True if instances are equal; otherwise False.

Equality is tested by comparing the JWTs.

Source code in tenduke_scale/auth/scale_jwt_auth_provider.py
125
126
127
128
129
130
def __eq__(self, other):
    """Return True if instances are equal; otherwise False.

    Equality is tested by comparing the JWTs.
    """
    return self.jwt == getattr(other, "jwt", None)

__init__(key_id, private_key, claims, config)

Construct a ScaleJwtAuth provider (hook).

Parameters:

Name Type Description Default
key_id str

The id of the key pair to use to sign the JWT.

required
private_key str

The private half of of the key pair to use to sign the JWT.

required
claims ScaleJwtClaims

The authorization claims to be included in the JWT.

required
config TendukeConfig

Configuration object specifying token_refresh_leeway_seconds.

required
Source code in tenduke_scale/auth/scale_jwt_auth_provider.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def __init__(
    self,
    key_id: str,
    private_key: str,
    claims: ScaleJwtClaims,
    config: TendukeConfig,
) -> None:
    """Construct a ScaleJwtAuth provider (hook).

    Args:
        key_id: The id of the key pair to use to sign the JWT.
        private_key: The private half of of the key pair to use to sign the JWT.
        claims: The authorization claims to be included in the JWT.
        config: Configuration object specifying token_refresh_leeway_seconds.
    """
    _validate_claims(claims)
    self._key_id = key_id
    self._private_key = private_key
    self._claims = claims
    self._make_jwt()
    self._token_leeway_seconds = config.token_refresh_leeway_seconds

__ne__(other)

Return True if instances are not equal; otherwise False.

Equality is tested by comparing the JWTs.

Source code in tenduke_scale/auth/scale_jwt_auth_provider.py
132
133
134
135
136
137
def __ne__(self, other):
    """Return True if instances are not equal; otherwise False.

    Equality is tested by comparing the JWTs.
    """
    return not self == other

ScaleJwtClaims dataclass

Claims for Scale JWT.

https://docs.scale.10duke.com/api/api-authorization/#confidential-applications-10duke-jwts

Attributes:

Name Type Description
sub str

Subject, used to identify calling identity. The value is matched to connectedIdentityId field in LicenseConsumer objects.

iss str

Issuer of the token.

valid_for timedelta

Number of seconds the token should be considered valid for.

permissions Sequence[str]

Array, value syntax: [Resource.permission,...].

license_consumer_id Optional[UUID]

The id of the consumer of the license.

Source code in tenduke_scale/auth/scale_jwt_auth_provider.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@dataclass
class ScaleJwtClaims:
    """Claims for Scale JWT.

    https://docs.scale.10duke.com/api/api-authorization/#confidential-applications-10duke-jwts

    Attributes:
        sub: Subject, used to identify calling identity. The value is matched to
             connectedIdentityId field in LicenseConsumer objects.
        iss: Issuer of the token.
        valid_for: Number of seconds the token should be considered valid for.
        permissions: Array, value syntax: [Resource.permission,...].
        license_consumer_id: The id of the consumer of the license.
    """

    sub: str
    iss: str
    valid_for: timedelta
    permissions: Sequence[str] = "*.*"
    license_consumer_id: Optional[UUID] = None

Checking out licenses

The 10Duke Scale License Checkout API is accessed using the subclasses of [tenduke_scale.license_checkout.LicenseCheckoutClientABC].

License checkout, release, heartbeat related types.

ClientDetails dataclass

Bases: Model

Model for providing client details to checkout operations.

Attributes:

Name Type Description
country Optional[str]

Country code of OS environment that the client app runs on.

host_name Optional[str]

Hostname of device that the client app runs on.

hardware_architecture Optional[str]

Architecture of of device / platform that the client app runs on.

hardware_id Optional[str]

Hardware id of device that the client app runs on. This claim is required to implement concurrency rules over a user's devices.

hardware_label Optional[str]

Hardware label of device that the client app runs on.

installation_id Optional[str]

Installation id the client app.

language Optional[str]

Language code of the OS environment that the client app runs on.

process_id Optional[str]

Process id of client app process in OS environment that the client app runs on. This claim is required to implement concurrency rules over a user's application instances (processes).

version Optional[str]

Client app version. This claim is required to implement rules about allowed versions and enforcing that a license is usable only for certain client application versions. The specified version is compared lexicographically to lower and upper bounds in available licenses. If a license id is used in checkout then the client version must be within the allowed version range in that specific license. Version is enforced only if the license requires it.

Source code in tenduke_scale/license_checkout/client_details.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@dataclass
class ClientDetails(Model):
    """Model for providing client details to checkout operations.

    Attributes:
        country: Country code of OS environment that the client app runs on.
        host_name: Hostname of device that the client app runs on.
        hardware_architecture: Architecture of of device / platform that the client app runs on.
        hardware_id:
            Hardware id of device that the client app runs on. This claim is required to implement
            concurrency rules over a user's devices.
        hardware_label: Hardware label of device that the client app runs on.
        installation_id: Installation id the client app.
        language: Language code of the OS environment that the client app runs on.
        process_id:
            Process id of client app process in OS environment that the client app runs on. This
            claim is required to implement concurrency rules over a user's application instances
            (processes).
        version:
            Client app version. This claim is required to implement rules about allowed versions
            and enforcing that a license is usable only for certain client application versions.
            The specified version is compared lexicographically to lower and upper bounds in
            available licenses. If a license id is used in checkout then the client version must be
            within the allowed version range in that specific license. Version is enforced only if
            the license requires it.
    """

    country: Optional[str] = field(
        init=True, metadata={"api_name": "cliCountry"}, default=None
    )
    host_name: Optional[str] = field(
        init=True, metadata={"api_name": "cliHostName"}, default=None
    )
    hardware_architecture: Optional[str] = field(
        init=True, metadata={"api_name": "cliHwArch"}, default=None
    )
    hardware_id: Optional[str] = field(
        init=True, metadata={"api_name": "cliHwId"}, default=None
    )
    hardware_label: Optional[str] = field(
        init=True, metadata={"api_name": "cliHwLabel"}, default=None
    )
    installation_id: Optional[str] = field(
        init=True, metadata={"api_name": "cliInstallationId"}, default=None
    )
    language: Optional[str] = field(
        init=True, metadata={"api_name": "cliLang"}, default=None
    )
    process_id: Optional[str] = field(
        init=True, metadata={"api_name": "cliProcessId"}, default=None
    )
    version: Optional[str] = field(
        init=True, metadata={"api_name": "cliVersion"}, default=None
    )

DefaultTokenStore

Bases: TokenStoreABC

Default (non-persistent) implementation of TokenStore.

Source code in tenduke_scale/license_checkout/token_store.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
class DefaultTokenStore(TokenStoreABC):
    """Default (non-persistent) implementation of TokenStore."""

    def __init__(self):
        """Construct instance of the DefaultTokenStore."""
        self._tokens = []

    def save(self, tokens: Sequence[LicenseToken]):
        """Save LicenseTokens to the store.

        Args:
            tokens: List of tokens to save. This should be interpretted as replacing the current
                    contents of the store.
        """
        self._tokens = [*self._tokens, *tokens]

    def load(self) -> Sequence[LicenseToken]:
        """Load any currently stored LicenseTokens.

        Returns:
            The sequence of LicenseTokens currently stored or persisted in the token store.
        """
        return self._tokens

    def remove_all(self):
        """Clear the contents of the store."""
        self._tokens = []

__init__()

Construct instance of the DefaultTokenStore.

Source code in tenduke_scale/license_checkout/token_store.py
 99
100
101
def __init__(self):
    """Construct instance of the DefaultTokenStore."""
    self._tokens = []

load()

Load any currently stored LicenseTokens.

Returns:

Type Description
Sequence[LicenseToken]

The sequence of LicenseTokens currently stored or persisted in the token store.

Source code in tenduke_scale/license_checkout/token_store.py
112
113
114
115
116
117
118
def load(self) -> Sequence[LicenseToken]:
    """Load any currently stored LicenseTokens.

    Returns:
        The sequence of LicenseTokens currently stored or persisted in the token store.
    """
    return self._tokens

remove_all()

Clear the contents of the store.

Source code in tenduke_scale/license_checkout/token_store.py
120
121
122
def remove_all(self):
    """Clear the contents of the store."""
    self._tokens = []

save(tokens)

Save LicenseTokens to the store.

Parameters:

Name Type Description Default
tokens Sequence[LicenseToken]

List of tokens to save. This should be interpretted as replacing the current contents of the store.

required
Source code in tenduke_scale/license_checkout/token_store.py
103
104
105
106
107
108
109
110
def save(self, tokens: Sequence[LicenseToken]):
    """Save LicenseTokens to the store.

    Args:
        tokens: List of tokens to save. This should be interpretted as replacing the current
                contents of the store.
    """
    self._tokens = [*self._tokens, *tokens]

EnforcedLicenseCheckoutClient

Bases: LicenseCheckoutClientABC

Client for checking out licenses using an enforced model.

Source code in tenduke_scale/license_checkout/enforced_license_checkout_client.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
class EnforcedLicenseCheckoutClient(LicenseCheckoutClientABC):
    """Client for checking out licenses using an enforced model."""

    def checkout(
        self,
        to_checkout: Sequence[LicenseCheckoutArguments],
        license_key: Optional[str] = None,
        license_consumer_id: Optional[UUID] = None,
        client_details: Optional[ClientDetails] = None,
    ) -> Sequence[LicenseToken]:
        """Checkout a license.

        Args:
            to_checkout:
                List of arguments objects describing the options when checking out each license.
            license_key: Scale License Key identifying licence(s) to checkout.
            license_consumer_id: Sets a header identifying the license consumer.
            client_details: Client claims object for checkout.

        Returns:
            List of license tokens representing successful and failed license checkouts.

        Raises:
            ApiError: Checkout request failed.
        """
        return self._start(
            "checkout",
            to_checkout,
            license_key,
            license_consumer_id,
            client_details,
        )

    def release(
        self,
        to_release: Sequence[LicenseReleaseArguments],
        license_key: Optional[str] = None,
        license_consumer_id: Optional[UUID] = None,
    ) -> Sequence[LicenseReleaseResult]:
        """Release a license.

        Args:
            to_release: List of arguments objects describing the licenses to release.
            license_key: Scale License Key identifying licence(s) to release.
            license_consumer_id: Sets a header identifying the license consumer.

        Returns:
            List of LicenseReleaseResult objects representing the licenses successfully released.

        Raises:
            ApiError: Release request failed.
        """
        return self._end("release", to_release, license_key, license_consumer_id)

    def heartbeat(
        self,
        to_heartbeat: Sequence[LicenseHeartbeatArguments],
        license_key: Optional[str] = None,
        license_consumer_id: Optional[UUID] = None,
    ) -> Sequence[LicenseToken]:
        """Verify one or more license checkout is still valid.

        Args:
            to_heartbeat: List of arguments objects describing the licenses to heartbeat.
            license_key: Scale License Key identifying licence(s) to heartbeat.
            license_consumer_id: Sets a header identifying the license consumer.

        Returns:
            List of LicenseToken objects for the successful and failed heartbeats.

        Raises:
            ApiError: Heartbeat request failed.
        """
        return self._heartbeat(
            "heartbeat", to_heartbeat, license_key, license_consumer_id
        )

    def checkout_single_by_license_key(
        self,
        license_key: str,
        to_checkout: LicenseCheckoutArguments,
        client_details: Optional[ClientDetails] = None,
    ) -> LicenseToken:
        """Checkout a license by license key.

        Args:
            license_key: Scale License Key identifying licence(s) to checkout.
            to_checkout: An object describing the options for checking out the license.
            client_details: Client claims object for checkout.

        Returns:
            A license token describing the success or failure of the checkout.

        Raises:
            ApiError: Checkout request failed.
        """
        tokens = self._start(
            "checkout", [to_checkout], license_key, client_details=client_details
        )
        return tokens[0]

    def checkout_multiple_by_license_key(
        self,
        license_key: str,
        to_checkout: Sequence[LicenseCheckoutArguments],
        client_details: Optional[ClientDetails] = None,
    ) -> Sequence[LicenseToken]:
        """Checkout multiple licenses using a license key.

        Args:
            license_key: Scale License Key identifying licence(s) to checkout.
            to_checkout:
                A list of objects describing the options for checking out the licenses.
            client_details: Client claims object for checkout.

        Returns:
            List of license tokens representing successful and failed license checkouts.

        Raises:
            ApiError: Checkout request failed.
        """
        return self._start(
            "checkout", to_checkout, license_key, client_details=client_details
        )

    def release_single_by_license_key(
        self, license_key: str, args: LicenseReleaseArguments
    ) -> LicenseReleaseResult:
        """Release a license by license key.

        Args:
            license_key: Scale License Key identifying licence(s) to release.
            args: An arguments object describing the license to release.

        Returns:
            LicenseReleaseResult object representing the license released.

        Raises:
            ApiError: Release request failed.
        """
        release_results = self._end("release", [args], license_key=license_key)
        return release_results[0]

    def release_multiple_by_license_key(
        self, license_key: str, to_release: Sequence[LicenseReleaseArguments]
    ) -> Sequence[LicenseReleaseResult]:
        """Release multiple licenses by license key.

        Args:
            license_key: Scale License Key identifying licence(s) to release.
            to_release: List of arguments objects describing the licenses to release.

        Returns:
            List of LicenseReleaseResult objects representing the licenses successfully released.

        Raises:
            ApiError: Release request failed.
        """
        return self._end("release", args=to_release, license_key=license_key)

    def heartbeat_single_by_license_key(
        self, license_key: str, to_heartbeat: LicenseHeartbeatArguments
    ) -> LicenseToken:
        """Heartbeat a license by license key.

        Args:
            license_key: Scale License Key identifying licence(s) to heartbeat.
            to_heartbeat: An arguments object describing the license to heartbeat.

        Returns:
            LicenseToken object describing the successful or failed heartbeat attempt.

        Raises:
            ApiError: Heartbeat request failed.
        """
        heartbeat_results = self._heartbeat(
            "heartbeat", [to_heartbeat], license_key=license_key
        )
        return heartbeat_results[0]

    def heartbeat_multiple_by_license_key(
        self,
        license_key: str,
        to_heartbeat: Sequence[LicenseHeartbeatArguments],
    ) -> Sequence[LicenseToken]:
        """Heartbeat multiple licenses by license key.

        Args:
            license_key: Scale License Key identifying licence(s) to heartbeat.
            to_heartbeat:
                List of arguments objects describing the licenses to heartbeat.

        Returns:
            List of LicenseToken objects for the successful and failed heartbeats.

        Raises:
            ApiError: Heartbeat request failed.
        """
        return self._heartbeat(
            "heartbeat",
            args=to_heartbeat,
            license_key=license_key,
        )

checkout(to_checkout, license_key=None, license_consumer_id=None, client_details=None)

Checkout a license.

Parameters:

Name Type Description Default
to_checkout Sequence[LicenseCheckoutArguments]

List of arguments objects describing the options when checking out each license.

required
license_key Optional[str]

Scale License Key identifying licence(s) to checkout.

None
license_consumer_id Optional[UUID]

Sets a header identifying the license consumer.

None
client_details Optional[ClientDetails]

Client claims object for checkout.

None

Returns:

Type Description
Sequence[LicenseToken]

List of license tokens representing successful and failed license checkouts.

Raises:

Type Description
ApiError

Checkout request failed.

Source code in tenduke_scale/license_checkout/enforced_license_checkout_client.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def checkout(
    self,
    to_checkout: Sequence[LicenseCheckoutArguments],
    license_key: Optional[str] = None,
    license_consumer_id: Optional[UUID] = None,
    client_details: Optional[ClientDetails] = None,
) -> Sequence[LicenseToken]:
    """Checkout a license.

    Args:
        to_checkout:
            List of arguments objects describing the options when checking out each license.
        license_key: Scale License Key identifying licence(s) to checkout.
        license_consumer_id: Sets a header identifying the license consumer.
        client_details: Client claims object for checkout.

    Returns:
        List of license tokens representing successful and failed license checkouts.

    Raises:
        ApiError: Checkout request failed.
    """
    return self._start(
        "checkout",
        to_checkout,
        license_key,
        license_consumer_id,
        client_details,
    )

checkout_multiple_by_license_key(license_key, to_checkout, client_details=None)

Checkout multiple licenses using a license key.

Parameters:

Name Type Description Default
license_key str

Scale License Key identifying licence(s) to checkout.

required
to_checkout Sequence[LicenseCheckoutArguments]

A list of objects describing the options for checking out the licenses.

required
client_details Optional[ClientDetails]

Client claims object for checkout.

None

Returns:

Type Description
Sequence[LicenseToken]

List of license tokens representing successful and failed license checkouts.

Raises:

Type Description
ApiError

Checkout request failed.

Source code in tenduke_scale/license_checkout/enforced_license_checkout_client.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def checkout_multiple_by_license_key(
    self,
    license_key: str,
    to_checkout: Sequence[LicenseCheckoutArguments],
    client_details: Optional[ClientDetails] = None,
) -> Sequence[LicenseToken]:
    """Checkout multiple licenses using a license key.

    Args:
        license_key: Scale License Key identifying licence(s) to checkout.
        to_checkout:
            A list of objects describing the options for checking out the licenses.
        client_details: Client claims object for checkout.

    Returns:
        List of license tokens representing successful and failed license checkouts.

    Raises:
        ApiError: Checkout request failed.
    """
    return self._start(
        "checkout", to_checkout, license_key, client_details=client_details
    )

checkout_single_by_license_key(license_key, to_checkout, client_details=None)

Checkout a license by license key.

Parameters:

Name Type Description Default
license_key str

Scale License Key identifying licence(s) to checkout.

required
to_checkout LicenseCheckoutArguments

An object describing the options for checking out the license.

required
client_details Optional[ClientDetails]

Client claims object for checkout.

None

Returns:

Type Description
LicenseToken

A license token describing the success or failure of the checkout.

Raises:

Type Description
ApiError

Checkout request failed.

Source code in tenduke_scale/license_checkout/enforced_license_checkout_client.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def checkout_single_by_license_key(
    self,
    license_key: str,
    to_checkout: LicenseCheckoutArguments,
    client_details: Optional[ClientDetails] = None,
) -> LicenseToken:
    """Checkout a license by license key.

    Args:
        license_key: Scale License Key identifying licence(s) to checkout.
        to_checkout: An object describing the options for checking out the license.
        client_details: Client claims object for checkout.

    Returns:
        A license token describing the success or failure of the checkout.

    Raises:
        ApiError: Checkout request failed.
    """
    tokens = self._start(
        "checkout", [to_checkout], license_key, client_details=client_details
    )
    return tokens[0]

heartbeat(to_heartbeat, license_key=None, license_consumer_id=None)

Verify one or more license checkout is still valid.

Parameters:

Name Type Description Default
to_heartbeat Sequence[LicenseHeartbeatArguments]

List of arguments objects describing the licenses to heartbeat.

required
license_key Optional[str]

Scale License Key identifying licence(s) to heartbeat.

None
license_consumer_id Optional[UUID]

Sets a header identifying the license consumer.

None

Returns:

Type Description
Sequence[LicenseToken]

List of LicenseToken objects for the successful and failed heartbeats.

Raises:

Type Description
ApiError

Heartbeat request failed.

Source code in tenduke_scale/license_checkout/enforced_license_checkout_client.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def heartbeat(
    self,
    to_heartbeat: Sequence[LicenseHeartbeatArguments],
    license_key: Optional[str] = None,
    license_consumer_id: Optional[UUID] = None,
) -> Sequence[LicenseToken]:
    """Verify one or more license checkout is still valid.

    Args:
        to_heartbeat: List of arguments objects describing the licenses to heartbeat.
        license_key: Scale License Key identifying licence(s) to heartbeat.
        license_consumer_id: Sets a header identifying the license consumer.

    Returns:
        List of LicenseToken objects for the successful and failed heartbeats.

    Raises:
        ApiError: Heartbeat request failed.
    """
    return self._heartbeat(
        "heartbeat", to_heartbeat, license_key, license_consumer_id
    )

heartbeat_multiple_by_license_key(license_key, to_heartbeat)

Heartbeat multiple licenses by license key.

Parameters:

Name Type Description Default
license_key str

Scale License Key identifying licence(s) to heartbeat.

required
to_heartbeat Sequence[LicenseHeartbeatArguments]

List of arguments objects describing the licenses to heartbeat.

required

Returns:

Type Description
Sequence[LicenseToken]

List of LicenseToken objects for the successful and failed heartbeats.

Raises:

Type Description
ApiError

Heartbeat request failed.

Source code in tenduke_scale/license_checkout/enforced_license_checkout_client.py
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
def heartbeat_multiple_by_license_key(
    self,
    license_key: str,
    to_heartbeat: Sequence[LicenseHeartbeatArguments],
) -> Sequence[LicenseToken]:
    """Heartbeat multiple licenses by license key.

    Args:
        license_key: Scale License Key identifying licence(s) to heartbeat.
        to_heartbeat:
            List of arguments objects describing the licenses to heartbeat.

    Returns:
        List of LicenseToken objects for the successful and failed heartbeats.

    Raises:
        ApiError: Heartbeat request failed.
    """
    return self._heartbeat(
        "heartbeat",
        args=to_heartbeat,
        license_key=license_key,
    )

heartbeat_single_by_license_key(license_key, to_heartbeat)

Heartbeat a license by license key.

Parameters:

Name Type Description Default
license_key str

Scale License Key identifying licence(s) to heartbeat.

required
to_heartbeat LicenseHeartbeatArguments

An arguments object describing the license to heartbeat.

required

Returns:

Type Description
LicenseToken

LicenseToken object describing the successful or failed heartbeat attempt.

Raises:

Type Description
ApiError

Heartbeat request failed.

Source code in tenduke_scale/license_checkout/enforced_license_checkout_client.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def heartbeat_single_by_license_key(
    self, license_key: str, to_heartbeat: LicenseHeartbeatArguments
) -> LicenseToken:
    """Heartbeat a license by license key.

    Args:
        license_key: Scale License Key identifying licence(s) to heartbeat.
        to_heartbeat: An arguments object describing the license to heartbeat.

    Returns:
        LicenseToken object describing the successful or failed heartbeat attempt.

    Raises:
        ApiError: Heartbeat request failed.
    """
    heartbeat_results = self._heartbeat(
        "heartbeat", [to_heartbeat], license_key=license_key
    )
    return heartbeat_results[0]

release(to_release, license_key=None, license_consumer_id=None)

Release a license.

Parameters:

Name Type Description Default
to_release Sequence[LicenseReleaseArguments]

List of arguments objects describing the licenses to release.

required
license_key Optional[str]

Scale License Key identifying licence(s) to release.

None
license_consumer_id Optional[UUID]

Sets a header identifying the license consumer.

None

Returns:

Type Description
Sequence[LicenseReleaseResult]

List of LicenseReleaseResult objects representing the licenses successfully released.

Raises:

Type Description
ApiError

Release request failed.

Source code in tenduke_scale/license_checkout/enforced_license_checkout_client.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def release(
    self,
    to_release: Sequence[LicenseReleaseArguments],
    license_key: Optional[str] = None,
    license_consumer_id: Optional[UUID] = None,
) -> Sequence[LicenseReleaseResult]:
    """Release a license.

    Args:
        to_release: List of arguments objects describing the licenses to release.
        license_key: Scale License Key identifying licence(s) to release.
        license_consumer_id: Sets a header identifying the license consumer.

    Returns:
        List of LicenseReleaseResult objects representing the licenses successfully released.

    Raises:
        ApiError: Release request failed.
    """
    return self._end("release", to_release, license_key, license_consumer_id)

release_multiple_by_license_key(license_key, to_release)

Release multiple licenses by license key.

Parameters:

Name Type Description Default
license_key str

Scale License Key identifying licence(s) to release.

required
to_release Sequence[LicenseReleaseArguments]

List of arguments objects describing the licenses to release.

required

Returns:

Type Description
Sequence[LicenseReleaseResult]

List of LicenseReleaseResult objects representing the licenses successfully released.

Raises:

Type Description
ApiError

Release request failed.

Source code in tenduke_scale/license_checkout/enforced_license_checkout_client.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
def release_multiple_by_license_key(
    self, license_key: str, to_release: Sequence[LicenseReleaseArguments]
) -> Sequence[LicenseReleaseResult]:
    """Release multiple licenses by license key.

    Args:
        license_key: Scale License Key identifying licence(s) to release.
        to_release: List of arguments objects describing the licenses to release.

    Returns:
        List of LicenseReleaseResult objects representing the licenses successfully released.

    Raises:
        ApiError: Release request failed.
    """
    return self._end("release", args=to_release, license_key=license_key)

release_single_by_license_key(license_key, args)

Release a license by license key.

Parameters:

Name Type Description Default
license_key str

Scale License Key identifying licence(s) to release.

required
args LicenseReleaseArguments

An arguments object describing the license to release.

required

Returns:

Type Description
LicenseReleaseResult

LicenseReleaseResult object representing the license released.

Raises:

Type Description
ApiError

Release request failed.

Source code in tenduke_scale/license_checkout/enforced_license_checkout_client.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def release_single_by_license_key(
    self, license_key: str, args: LicenseReleaseArguments
) -> LicenseReleaseResult:
    """Release a license by license key.

    Args:
        license_key: Scale License Key identifying licence(s) to release.
        args: An arguments object describing the license to release.

    Returns:
        LicenseReleaseResult object representing the license released.

    Raises:
        ApiError: Release request failed.
    """
    release_results = self._end("release", [args], license_key=license_key)
    return release_results[0]

FeatureFlagsResponse dataclass

Bases: Model

Feature flags for a product.

Attributes:

Name Type Description
product_name str

Name of the product.

features Sequence[str]

List of feature names.

Source code in tenduke_scale/license_checkout/feature_flags_response.py
 8
 9
10
11
12
13
14
15
16
17
18
@dataclass
class FeatureFlagsResponse(Model):
    """Feature flags for a product.

    Attributes:
        product_name: Name of the product.
        features: List of feature names.
    """

    product_name: str = field(init=True, metadata={"api_name": "productName"})
    features: Sequence[str] = field(init=True)

LicenseCheckoutArguments dataclass

Bases: Model

Model for checking out enforced licenses or starting metered licenses.

Attributes:

Name Type Description
product_name str

Product name for which license checkout is requested.

quantity_dimension QD

Enum: "SEATS" "USE_COUNT" "USE_TIME" Quantity dimension, related units of measurement: seats and use count = unitless, use time = seconds.

quantity int

Quantity defines how much to consume. NOTE: does not apply for seat based licensing. When using seats quantity is always equals to 1 (maximum qty = 1 when requesting to consume a seat, qtyDimension = SEATS).

client_version Optional[str]

Client application version. NOTE: required when consuming licenses that have an allowed version range specified. Recommendation: client applications are encouraged to always send their version.

license_id Optional[UUID]

Optional id of a specific license to consume. No license selection fallback logic kicks in if this value is defined. This means consumption either succeeds based on the specific license or fails with no further reason. Using this field is redunant if a license key is used.

Source code in tenduke_scale/license_checkout/license_checkout_arguments.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@dataclass
class LicenseCheckoutArguments(Model):
    """Model for checking out enforced licenses or starting metered licenses.

    Attributes:
        product_name: Product name for which license checkout is requested.
        quantity_dimension:
            Enum: "SEATS" "USE_COUNT" "USE_TIME" Quantity dimension, related units of measurement:
            seats and use count = unitless, use time = seconds.
        quantity:
            Quantity defines how much to consume. NOTE: does not apply for seat based licensing.
            When using seats quantity is always equals to 1 (maximum qty = 1 when requesting to
            consume a seat, qtyDimension = SEATS).
        client_version:
            Client application version. NOTE: required when consuming licenses that have an allowed
            version range specified. Recommendation: client applications are encouraged to always
            send their version.
        license_id:
            Optional id of a specific license to consume. No license selection fallback logic kicks
            in if this value is defined. This means consumption either succeeds based on the
            specific license or fails with no further reason. Using this field is redunant if a
            license key is used.
    """

    product_name: str = field(init=True, metadata={"api_name": "productName"})
    quantity_dimension: QD = field(
        init=True,
        metadata={
            "api_name": "qtyDimension",
            "transform": "enum",
            "type": QuantityDimension,
        },
        default=QD.SEATS,
    )
    quantity: int = field(init=True, metadata={"api_name": "qty"}, default=1)
    client_version: Optional[str] = field(
        init=True, metadata={"api_name": "clientVersion"}, default=None
    )
    license_id: Optional[UUID] = field(
        init=True, metadata={"api_name": "licenseId", "transform": "uuid"}, default=None
    )

LicenseCheckoutClientABC

Bases: ABC

Base class for license checkout clients.

Source code in tenduke_scale/license_checkout/license_checkout_client.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
class LicenseCheckoutClientABC(ABC):
    """Base class for license checkout clients."""

    start_action = "checkout"
    end_action = "release"
    heartbeat_action = "heartbeat"

    def __init__(
        self,
        api_url: str,
        session: Session,
        token_store: Optional[TokenStoreABC] = None,
    ):
        """Construct the LicenseCheckoutClientABC.

        Args:
            api_url: Base API URL of the Scale tenant being accessed.
            session: requests.Session object configured for use by client.
            token_store: Used to store and manage tokens.
        """
        self.api_url = api_url
        uri = f"{api_url}/licensing-signing-keys/.well-known/jwks.json"
        # lifespan: 96 hours (as seconds)
        self._jwk_client = PyJWKClient(uri, cache_keys=True, lifespan=345000)
        self.session = session
        self.store = token_store or DefaultTokenStore()

    def describe_licensees(
        self,
        license_consumer_id: Optional[UUID] = None,
        paging_args: Optional[PagingOptions] = None,
    ) -> Sequence[LicenseConsumer]:
        """Retrieve the set of licensees that the consumer is connected to.

        Args:
            license_consumer_id: The license consumer to describe licensees for.
            paging_args: Sets limit, offset, and sorting for the result.

        Returns:
            List of license consumer objects describing the licensees that the consumer can access
            licenses from.

        Raises:
            ApiError: The request failed.
        """
        path = "/licensing/actions/describe-license-consumer-licensees"
        json = _describe_request_json(
            self.session,
            self.api_url,
            path,
            license_consumer_id=license_consumer_id,
            paging_args=paging_args,
        )
        return [LicenseConsumer.from_api(item) for item in json]

    def describe_licenses(
        self,
        licensee_id: UUID,
        license_consumer_id: Optional[UUID] = None,
        describe_license_options: Optional[DescribeLicenseOptions] = None,
        paging: Optional[PagingOptions] = None,
    ) -> LicenseConsumerLicensesStatus:
        """Retrieve the licenses the license consumer currently has access to from the licensee.

        Args:
            licensee_id: The licensee to describe licenses for.
            license_consumer_id: The license consumer to describe licenses for.
            describe_license_options: Options setting what information to return.
            paging: Sets limit, offset, and sorting for the result.

        Returns:
            LicenseConsumerLicensesStatus listing the licenses that the consumer has access to.

        Raises:
            ApiError: The request failed.
        """
        path = "/licensing/actions/describe-license-consumer-licenses"
        json = _describe_request_json(
            self.session,
            self.api_url,
            path,
            paging_args=paging,
            licensee_id=licensee_id,
            license_consumer_id=license_consumer_id,
            describe_license_options=describe_license_options,
        )
        return LicenseConsumerLicensesStatus.from_api(json)

    def raise_if_consumer_unknown(self, license_consumer_id: Optional[UUID] = None):
        """Raise an error is the license consumer is not provided and cannot be inferred.

        Return if the license_consumer_id is set, or auth is using ID Token; otherwise raise error.

        Args:
            license_consumer_id: license consumer id provided to client method.

        Raises:
            ValueError: The license consumer id is not set and there is no id token.
        """
        if license_consumer_id or isinstance(self.session.auth, IdTokenAuth):
            return
        raise InvalidArgumentError("license_consumer_id")

    def describe_licenses_in_use(
        self,
        licensee_id: UUID,
        license_consumer_id: Optional[UUID] = None,
        describe_license_options: Optional[DescribeLicenseOptions] = None,
        paging: Optional[PagingOptions] = None,
    ) -> LicenseConsumerClientBindingStatus:
        """Retrieve the currently checked out licenses.

        Get the list of licenses that are known to be in use by a specific license consumer (user).
        These are then returned to the client application as a list of client bindings.

        Args:
            licensee_id:
                The licensee to describe licenses for (only client bindings for licenses scoped to
                this licensee will be included in the result).
            license_consumer_id:
                The license consumer to describe licenses for. If omitted, client bindings for the
                currently logged in user are returned. Must be provided if using 10Duke ScaleJWT
                Authorization.
            describe_license_options: Options setting what information to return.
            paging: Sets limit, offset, and sorting for the result.

        Returns:
            LicenseConsumerClientBindingStatus listing the current client bindings for the
            specified license consumer.
        """
        path = "/licensing/actions/describe-license-consumer-client-bindings"
        json = _describe_request_json(
            self.session,
            self.api_url,
            path,
            paging_args=paging,
            licensee_id=licensee_id,
            license_consumer_id=license_consumer_id,
            describe_license_options=describe_license_options,
        )
        return LicenseConsumerClientBindingStatus.from_api(json)

    # pylint: disable=too-many-arguments
    def describe_license_usage(
        self,
        licensee_id: UUID,
        license_id: UUID,
        license_consumer_id: Optional[UUID] = None,
        describe_license_options: Optional[DescribeLicenseOptions] = None,
        paging: Optional[PagingOptions] = None,
    ) -> LicenseConsumerClientBindingStatus:
        """Retrieve the current usage for a license.

        Get the client bindings (existing checkouts or consumptions) of a specific license
        within a licensee.
        This provides information about what devices a license is currently checked out on and
        which license consumers (users) are using the license.

        Args:
            licensee_id:
                The licensee to describe licenses for (only client bindings for licenses scoped to
                this licensee will be included in the result).
            license_id: Identifier of the license that the information is scoped to.
            license_consumer_id:
                Optional identifier of the license consumer the the information is scoped to. Use
                only with Scale JWT authorization.
            describe_license_options: Options setting what information to return.
            paging: Sets limit, offset, and sorting for the result.

        Returns:
            LicenseConsumerClientBindingStatus listing the current client bindings for the
            specified license.
        """
        path = "/licensing/actions/describe-license-client-bindings"
        json = _describe_request_json(
            self.session,
            self.api_url,
            path,
            paging_args=paging,
            licensee_id=licensee_id,
            license_id=license_id,
            license_consumer_id=license_consumer_id,
            describe_license_options=describe_license_options,
        )

        return LicenseConsumerClientBindingStatus.from_api(json)

    def find_client_binding(
        self, licensee_id: UUID, client_binding_id: UUID, with_metadata: bool = False
    ) -> LicenseConsumerClientBindingStatus:
        """Retrieve a specific client binding.

        Get the details of a client binding, within a licensee, by its unique identifier.

        Args:
            licensee_id:
                The licensee to describe licenses for (only client bindings for licenses scoped to
                this licensee will be included in the result).
            client_binding_id: Identifier of the client binding requested.
            with_metadata:
                Flag to control including verbose information about the licenses and client
                bindings. Setting this option to true will fetch contract, order, subscription and
                external reference information at time of original license grant, the license
                container, a possible license key and related product information. For client
                bindings the additional information is related to license consumption objects and
                license consumers. Defaults to false.

        Returns:
            LicenseConsumerClientBindingStatus listing the requested client binding if found.
        """
        path = "/licensing/actions/find-license-client-binding"
        json = _describe_request_json(
            self.session,
            self.api_url,
            path,
            licensee_id=licensee_id,
            client_binding_id=client_binding_id,
            with_metadata=with_metadata,
        )

        return LicenseConsumerClientBindingStatus.from_api(json)

    def feature_flags(
        self,
        licensee_id: UUID,
        license_consumer_id: Optional[UUID] = None,
        filter_value: Optional[str] = None,
    ) -> Sequence[FeatureFlagsResponse]:
        """Retrieve licensed features that a license consumer (user) has access to.

        The list of features returned is scoped to the licensee and license consumer.

        Args:
            licensee_id:
                The licensee to describe licenses for (only licenses scoped to this licensee will
                be used to produce the result).
            license_consumer_id:
                The license consumer to describe licenses for. If omitted, licenses for the
                currently logged in user are returned. Must be provided if using 10Duke ScaleJWT
                Authorization.
            filter_value:
                Product name to match in result. Defaults to null (no filtering). The match is full
                name, case insensitive.
        """
        path = "/licensing/actions/describe-as-feature-flags"
        json = _describe_request_json(
            self.session,
            self.api_url,
            path,
            licensee_id=licensee_id,
            license_consumer_id=license_consumer_id,
            filter_value=filter_value,
        )
        flags = [FeatureFlagsResponse.from_api(item) for item in json["featureFlags"]]
        return flags

    def feature_flags_as_jwt(
        self,
        licensee_id: UUID,
        license_consumer_id: Optional[UUID] = None,
        filter_value: Optional[str] = None,
    ) -> FeatureFlagsToken:
        """Retrieve licensed features that a license consumer (user) has access to as JWT.

        The list of features returned is scoped to the licensee and license consumer.

        Args:
            licensee_id:
                The licensee to describe licenses for (only licenses scoped to this licensee will
                be used to produce the result).
            license_consumer_id:
                The license consumer to describe licenses for. If omitted, licenses for the
                currently logged in user are returned. Must be provided if using 10Duke ScaleJWT
                Authorization.
            filter_value:
                Product name to match in result. Defaults to null (no filtering). The match is full
                name, case insensitive.
        """
        path = "/licensing/actions/describe-as-feature-flags-jwt"
        jwt_text = _describe_request_text(
            self.session,
            self.api_url,
            path,
            licensee_id=licensee_id,
            license_consumer_id=license_consumer_id,
            filter_value=filter_value,
        )
        pub_key = self._jwk_client.get_signing_key_from_jwt(jwt_text)
        flags = FeatureFlagsToken(jwt_text, pub_key.key)
        return flags

    def parse_jwt(self, jwt: str) -> LicenseToken:
        """Parse JWT using public key for Scale API.

        Args:
            jwt: Raw jwt to parse.

        Returns:
            LicenseToken containing the details from the JWT.
        """
        pub_key = self._jwk_client.get_signing_key_from_jwt(jwt)
        return LicenseToken(jwt, pub_key.key)

    def describe_license_key(
        self,
        license_key: str,
        with_metadata: bool = False,
        paging: Optional[PagingOptions] = None,
    ) -> Sequence[License]:
        """Retrieve the licenses that a license key grants usage rights to.

        Args:
            license_key: The license key to get license information for.
            with_metadata:
                Flag to control including verbose information about the licenses and client
                bindings. Setting this option to true will fetch contract, order, subscription and
                external reference information at time of original license grant, the license
                container, a possible license key and related product information. For client
                bindings the additional information is related to license consumption objects and
                license consumers. Defaults to false.
            paging: Sets limit, offset, and sorting for the result.
        """
        path = "/licensing/actions/describe-license-key"
        result = _describe_request_json(
            self.session,
            self.api_url,
            path,
            license_key=license_key,
            with_metadata=with_metadata,
            paging_args=paging,
        )
        return [License.from_api(license_) for license_ in result["licenses"]]

    def describe_license_key_client_bindings(
        self,
        license_key: str,
        license_id: UUID,
        describe_license_options: Optional[DescribeLicenseOptions] = None,
        paging: Optional[PagingOptions] = None,
    ) -> LicenseConsumerClientBindingStatus:
        """Retrieve license use for a license checked out using a license key.

        Args:
            license_key: The license key to get information for.
            license_id: The license id to get checkout information for.
            describe_license_options: Options setting what information to return.
            paging: Sets limit, offset, and sorting for the result.
        """
        path = "/licensing/actions/describe-license-key-client-bindings"

        result = _describe_request_json(
            self.session,
            self.api_url,
            path,
            license_key=license_key,
            license_id=license_id,
            paging_args=paging,
            describe_license_options=describe_license_options,
        )
        return LicenseConsumerClientBindingStatus.from_api(result)

    def _start(
        self,
        action: str,
        args: Sequence[LicenseCheckoutArguments],
        license_key: Optional[str] = None,
        license_consumer_id: Optional[UUID] = None,
        client_details: Optional[ClientDetails] = None,
    ):
        self.raise_if_consumer_unknown(license_consumer_id)
        key_fragment = _license_key_fragment(license_key)
        uri = f"{self.api_url}/licensing/actions/{action}{key_fragment}"
        headers = _concat_checkout_headers(client_details, license_consumer_id)
        body = [co.to_api() for co in args]
        response = self.session.post(uri, headers=headers, json=body)
        raise_for_response(response, uri)
        jwt_list = response.json()
        tokens = [self.parse_jwt(jwt) for jwt in jwt_list]
        self.store.save(tokens)
        return tokens

    def _end(
        self,
        action: str,
        args: Sequence[LicenseReleaseArguments],
        license_key: Optional[str] = None,
        license_consumer_id: Optional[UUID] = None,
    ):
        self.raise_if_consumer_unknown(license_consumer_id)
        key_fragment = _license_key_fragment(license_key)
        uri = f"{self.api_url}/licensing/actions/{action}{key_fragment}"
        headers = _concat_checkout_headers(None, license_consumer_id)
        body = [rel.to_api() for rel in args]
        response = self.session.post(uri, headers=headers, json=body)
        raise_for_response(response, uri)
        results = response.json()
        result_models = [LicenseReleaseResult.from_api(res) for res in results]
        to_release = [
            model.released_lease_id
            for model in result_models
            if model.released and model.released_lease_id is not None
        ]
        if any(to_release):
            self.store.remove(to_release)
        return result_models

    def _raise_if_heartbeat_too_early(self, license_heartbeat_arguments):
        old_lease_ids = [arg.lease_id for arg in license_heartbeat_arguments]
        tokens = self.store.load()
        if any(
            token
            for token in tokens
            if token.lease_id in old_lease_ids
            and token.heartbeat_not_before > datetime.utcnow()
        ):
            raise HeartbeatTooEarlyError()

    def _heartbeat(
        self,
        action: str,
        args: Sequence[LicenseHeartbeatArguments],
        license_key: Optional[str] = None,
        license_consumer_id: Optional[UUID] = None,
    ) -> Sequence[LicenseToken]:
        self.raise_if_consumer_unknown(license_consumer_id)
        self._raise_if_heartbeat_too_early(args)
        key_fragment = _license_key_fragment(license_key)
        uri = f"{self.api_url}/licensing/actions/{action}{key_fragment}"
        headers = {}
        if license_consumer_id:
            headers["licenseConsumerId"] = str(license_consumer_id)
        body = [args.to_api() for args in args]
        response = self.session.post(uri, json=body, headers=headers)
        raise_for_response(response, uri)
        jwt_list = response.json()
        tokens = [self.parse_jwt(jwt) for jwt in jwt_list]
        self.store.update(tokens)
        return tokens

__init__(api_url, session, token_store=None)

Construct the LicenseCheckoutClientABC.

Parameters:

Name Type Description Default
api_url str

Base API URL of the Scale tenant being accessed.

required
session Session

requests.Session object configured for use by client.

required
token_store Optional[TokenStoreABC]

Used to store and manage tokens.

None
Source code in tenduke_scale/license_checkout/license_checkout_client.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def __init__(
    self,
    api_url: str,
    session: Session,
    token_store: Optional[TokenStoreABC] = None,
):
    """Construct the LicenseCheckoutClientABC.

    Args:
        api_url: Base API URL of the Scale tenant being accessed.
        session: requests.Session object configured for use by client.
        token_store: Used to store and manage tokens.
    """
    self.api_url = api_url
    uri = f"{api_url}/licensing-signing-keys/.well-known/jwks.json"
    # lifespan: 96 hours (as seconds)
    self._jwk_client = PyJWKClient(uri, cache_keys=True, lifespan=345000)
    self.session = session
    self.store = token_store or DefaultTokenStore()

describe_license_key(license_key, with_metadata=False, paging=None)

Retrieve the licenses that a license key grants usage rights to.

Parameters:

Name Type Description Default
license_key str

The license key to get license information for.

required
with_metadata bool

Flag to control including verbose information about the licenses and client bindings. Setting this option to true will fetch contract, order, subscription and external reference information at time of original license grant, the license container, a possible license key and related product information. For client bindings the additional information is related to license consumption objects and license consumers. Defaults to false.

False
paging Optional[PagingOptions]

Sets limit, offset, and sorting for the result.

None
Source code in tenduke_scale/license_checkout/license_checkout_client.py
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
def describe_license_key(
    self,
    license_key: str,
    with_metadata: bool = False,
    paging: Optional[PagingOptions] = None,
) -> Sequence[License]:
    """Retrieve the licenses that a license key grants usage rights to.

    Args:
        license_key: The license key to get license information for.
        with_metadata:
            Flag to control including verbose information about the licenses and client
            bindings. Setting this option to true will fetch contract, order, subscription and
            external reference information at time of original license grant, the license
            container, a possible license key and related product information. For client
            bindings the additional information is related to license consumption objects and
            license consumers. Defaults to false.
        paging: Sets limit, offset, and sorting for the result.
    """
    path = "/licensing/actions/describe-license-key"
    result = _describe_request_json(
        self.session,
        self.api_url,
        path,
        license_key=license_key,
        with_metadata=with_metadata,
        paging_args=paging,
    )
    return [License.from_api(license_) for license_ in result["licenses"]]

describe_license_key_client_bindings(license_key, license_id, describe_license_options=None, paging=None)

Retrieve license use for a license checked out using a license key.

Parameters:

Name Type Description Default
license_key str

The license key to get information for.

required
license_id UUID

The license id to get checkout information for.

required
describe_license_options Optional[DescribeLicenseOptions]

Options setting what information to return.

None
paging Optional[PagingOptions]

Sets limit, offset, and sorting for the result.

None
Source code in tenduke_scale/license_checkout/license_checkout_client.py
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
def describe_license_key_client_bindings(
    self,
    license_key: str,
    license_id: UUID,
    describe_license_options: Optional[DescribeLicenseOptions] = None,
    paging: Optional[PagingOptions] = None,
) -> LicenseConsumerClientBindingStatus:
    """Retrieve license use for a license checked out using a license key.

    Args:
        license_key: The license key to get information for.
        license_id: The license id to get checkout information for.
        describe_license_options: Options setting what information to return.
        paging: Sets limit, offset, and sorting for the result.
    """
    path = "/licensing/actions/describe-license-key-client-bindings"

    result = _describe_request_json(
        self.session,
        self.api_url,
        path,
        license_key=license_key,
        license_id=license_id,
        paging_args=paging,
        describe_license_options=describe_license_options,
    )
    return LicenseConsumerClientBindingStatus.from_api(result)

describe_license_usage(licensee_id, license_id, license_consumer_id=None, describe_license_options=None, paging=None)

Retrieve the current usage for a license.

Get the client bindings (existing checkouts or consumptions) of a specific license within a licensee. This provides information about what devices a license is currently checked out on and which license consumers (users) are using the license.

Parameters:

Name Type Description Default
licensee_id UUID

The licensee to describe licenses for (only client bindings for licenses scoped to this licensee will be included in the result).

required
license_id UUID

Identifier of the license that the information is scoped to.

required
license_consumer_id Optional[UUID]

Optional identifier of the license consumer the the information is scoped to. Use only with Scale JWT authorization.

None
describe_license_options Optional[DescribeLicenseOptions]

Options setting what information to return.

None
paging Optional[PagingOptions]

Sets limit, offset, and sorting for the result.

None

Returns:

Type Description
LicenseConsumerClientBindingStatus

LicenseConsumerClientBindingStatus listing the current client bindings for the

LicenseConsumerClientBindingStatus

specified license.

Source code in tenduke_scale/license_checkout/license_checkout_client.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
def describe_license_usage(
    self,
    licensee_id: UUID,
    license_id: UUID,
    license_consumer_id: Optional[UUID] = None,
    describe_license_options: Optional[DescribeLicenseOptions] = None,
    paging: Optional[PagingOptions] = None,
) -> LicenseConsumerClientBindingStatus:
    """Retrieve the current usage for a license.

    Get the client bindings (existing checkouts or consumptions) of a specific license
    within a licensee.
    This provides information about what devices a license is currently checked out on and
    which license consumers (users) are using the license.

    Args:
        licensee_id:
            The licensee to describe licenses for (only client bindings for licenses scoped to
            this licensee will be included in the result).
        license_id: Identifier of the license that the information is scoped to.
        license_consumer_id:
            Optional identifier of the license consumer the the information is scoped to. Use
            only with Scale JWT authorization.
        describe_license_options: Options setting what information to return.
        paging: Sets limit, offset, and sorting for the result.

    Returns:
        LicenseConsumerClientBindingStatus listing the current client bindings for the
        specified license.
    """
    path = "/licensing/actions/describe-license-client-bindings"
    json = _describe_request_json(
        self.session,
        self.api_url,
        path,
        paging_args=paging,
        licensee_id=licensee_id,
        license_id=license_id,
        license_consumer_id=license_consumer_id,
        describe_license_options=describe_license_options,
    )

    return LicenseConsumerClientBindingStatus.from_api(json)

describe_licensees(license_consumer_id=None, paging_args=None)

Retrieve the set of licensees that the consumer is connected to.

Parameters:

Name Type Description Default
license_consumer_id Optional[UUID]

The license consumer to describe licensees for.

None
paging_args Optional[PagingOptions]

Sets limit, offset, and sorting for the result.

None

Returns:

Type Description
Sequence[LicenseConsumer]

List of license consumer objects describing the licensees that the consumer can access

Sequence[LicenseConsumer]

licenses from.

Raises:

Type Description
ApiError

The request failed.

Source code in tenduke_scale/license_checkout/license_checkout_client.py
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def describe_licensees(
    self,
    license_consumer_id: Optional[UUID] = None,
    paging_args: Optional[PagingOptions] = None,
) -> Sequence[LicenseConsumer]:
    """Retrieve the set of licensees that the consumer is connected to.

    Args:
        license_consumer_id: The license consumer to describe licensees for.
        paging_args: Sets limit, offset, and sorting for the result.

    Returns:
        List of license consumer objects describing the licensees that the consumer can access
        licenses from.

    Raises:
        ApiError: The request failed.
    """
    path = "/licensing/actions/describe-license-consumer-licensees"
    json = _describe_request_json(
        self.session,
        self.api_url,
        path,
        license_consumer_id=license_consumer_id,
        paging_args=paging_args,
    )
    return [LicenseConsumer.from_api(item) for item in json]

describe_licenses(licensee_id, license_consumer_id=None, describe_license_options=None, paging=None)

Retrieve the licenses the license consumer currently has access to from the licensee.

Parameters:

Name Type Description Default
licensee_id UUID

The licensee to describe licenses for.

required
license_consumer_id Optional[UUID]

The license consumer to describe licenses for.

None
describe_license_options Optional[DescribeLicenseOptions]

Options setting what information to return.

None
paging Optional[PagingOptions]

Sets limit, offset, and sorting for the result.

None

Returns:

Type Description
LicenseConsumerLicensesStatus

LicenseConsumerLicensesStatus listing the licenses that the consumer has access to.

Raises:

Type Description
ApiError

The request failed.

Source code in tenduke_scale/license_checkout/license_checkout_client.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def describe_licenses(
    self,
    licensee_id: UUID,
    license_consumer_id: Optional[UUID] = None,
    describe_license_options: Optional[DescribeLicenseOptions] = None,
    paging: Optional[PagingOptions] = None,
) -> LicenseConsumerLicensesStatus:
    """Retrieve the licenses the license consumer currently has access to from the licensee.

    Args:
        licensee_id: The licensee to describe licenses for.
        license_consumer_id: The license consumer to describe licenses for.
        describe_license_options: Options setting what information to return.
        paging: Sets limit, offset, and sorting for the result.

    Returns:
        LicenseConsumerLicensesStatus listing the licenses that the consumer has access to.

    Raises:
        ApiError: The request failed.
    """
    path = "/licensing/actions/describe-license-consumer-licenses"
    json = _describe_request_json(
        self.session,
        self.api_url,
        path,
        paging_args=paging,
        licensee_id=licensee_id,
        license_consumer_id=license_consumer_id,
        describe_license_options=describe_license_options,
    )
    return LicenseConsumerLicensesStatus.from_api(json)

describe_licenses_in_use(licensee_id, license_consumer_id=None, describe_license_options=None, paging=None)

Retrieve the currently checked out licenses.

Get the list of licenses that are known to be in use by a specific license consumer (user). These are then returned to the client application as a list of client bindings.

Parameters:

Name Type Description Default
licensee_id UUID

The licensee to describe licenses for (only client bindings for licenses scoped to this licensee will be included in the result).

required
license_consumer_id Optional[UUID]

The license consumer to describe licenses for. If omitted, client bindings for the currently logged in user are returned. Must be provided if using 10Duke ScaleJWT Authorization.

None
describe_license_options Optional[DescribeLicenseOptions]

Options setting what information to return.

None
paging Optional[PagingOptions]

Sets limit, offset, and sorting for the result.

None

Returns:

Type Description
LicenseConsumerClientBindingStatus

LicenseConsumerClientBindingStatus listing the current client bindings for the

LicenseConsumerClientBindingStatus

specified license consumer.

Source code in tenduke_scale/license_checkout/license_checkout_client.py
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def describe_licenses_in_use(
    self,
    licensee_id: UUID,
    license_consumer_id: Optional[UUID] = None,
    describe_license_options: Optional[DescribeLicenseOptions] = None,
    paging: Optional[PagingOptions] = None,
) -> LicenseConsumerClientBindingStatus:
    """Retrieve the currently checked out licenses.

    Get the list of licenses that are known to be in use by a specific license consumer (user).
    These are then returned to the client application as a list of client bindings.

    Args:
        licensee_id:
            The licensee to describe licenses for (only client bindings for licenses scoped to
            this licensee will be included in the result).
        license_consumer_id:
            The license consumer to describe licenses for. If omitted, client bindings for the
            currently logged in user are returned. Must be provided if using 10Duke ScaleJWT
            Authorization.
        describe_license_options: Options setting what information to return.
        paging: Sets limit, offset, and sorting for the result.

    Returns:
        LicenseConsumerClientBindingStatus listing the current client bindings for the
        specified license consumer.
    """
    path = "/licensing/actions/describe-license-consumer-client-bindings"
    json = _describe_request_json(
        self.session,
        self.api_url,
        path,
        paging_args=paging,
        licensee_id=licensee_id,
        license_consumer_id=license_consumer_id,
        describe_license_options=describe_license_options,
    )
    return LicenseConsumerClientBindingStatus.from_api(json)

feature_flags(licensee_id, license_consumer_id=None, filter_value=None)

Retrieve licensed features that a license consumer (user) has access to.

The list of features returned is scoped to the licensee and license consumer.

Parameters:

Name Type Description Default
licensee_id UUID

The licensee to describe licenses for (only licenses scoped to this licensee will be used to produce the result).

required
license_consumer_id Optional[UUID]

The license consumer to describe licenses for. If omitted, licenses for the currently logged in user are returned. Must be provided if using 10Duke ScaleJWT Authorization.

None
filter_value Optional[str]

Product name to match in result. Defaults to null (no filtering). The match is full name, case insensitive.

None
Source code in tenduke_scale/license_checkout/license_checkout_client.py
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
def feature_flags(
    self,
    licensee_id: UUID,
    license_consumer_id: Optional[UUID] = None,
    filter_value: Optional[str] = None,
) -> Sequence[FeatureFlagsResponse]:
    """Retrieve licensed features that a license consumer (user) has access to.

    The list of features returned is scoped to the licensee and license consumer.

    Args:
        licensee_id:
            The licensee to describe licenses for (only licenses scoped to this licensee will
            be used to produce the result).
        license_consumer_id:
            The license consumer to describe licenses for. If omitted, licenses for the
            currently logged in user are returned. Must be provided if using 10Duke ScaleJWT
            Authorization.
        filter_value:
            Product name to match in result. Defaults to null (no filtering). The match is full
            name, case insensitive.
    """
    path = "/licensing/actions/describe-as-feature-flags"
    json = _describe_request_json(
        self.session,
        self.api_url,
        path,
        licensee_id=licensee_id,
        license_consumer_id=license_consumer_id,
        filter_value=filter_value,
    )
    flags = [FeatureFlagsResponse.from_api(item) for item in json["featureFlags"]]
    return flags

feature_flags_as_jwt(licensee_id, license_consumer_id=None, filter_value=None)

Retrieve licensed features that a license consumer (user) has access to as JWT.

The list of features returned is scoped to the licensee and license consumer.

Parameters:

Name Type Description Default
licensee_id UUID

The licensee to describe licenses for (only licenses scoped to this licensee will be used to produce the result).

required
license_consumer_id Optional[UUID]

The license consumer to describe licenses for. If omitted, licenses for the currently logged in user are returned. Must be provided if using 10Duke ScaleJWT Authorization.

None
filter_value Optional[str]

Product name to match in result. Defaults to null (no filtering). The match is full name, case insensitive.

None
Source code in tenduke_scale/license_checkout/license_checkout_client.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
def feature_flags_as_jwt(
    self,
    licensee_id: UUID,
    license_consumer_id: Optional[UUID] = None,
    filter_value: Optional[str] = None,
) -> FeatureFlagsToken:
    """Retrieve licensed features that a license consumer (user) has access to as JWT.

    The list of features returned is scoped to the licensee and license consumer.

    Args:
        licensee_id:
            The licensee to describe licenses for (only licenses scoped to this licensee will
            be used to produce the result).
        license_consumer_id:
            The license consumer to describe licenses for. If omitted, licenses for the
            currently logged in user are returned. Must be provided if using 10Duke ScaleJWT
            Authorization.
        filter_value:
            Product name to match in result. Defaults to null (no filtering). The match is full
            name, case insensitive.
    """
    path = "/licensing/actions/describe-as-feature-flags-jwt"
    jwt_text = _describe_request_text(
        self.session,
        self.api_url,
        path,
        licensee_id=licensee_id,
        license_consumer_id=license_consumer_id,
        filter_value=filter_value,
    )
    pub_key = self._jwk_client.get_signing_key_from_jwt(jwt_text)
    flags = FeatureFlagsToken(jwt_text, pub_key.key)
    return flags

find_client_binding(licensee_id, client_binding_id, with_metadata=False)

Retrieve a specific client binding.

Get the details of a client binding, within a licensee, by its unique identifier.

Parameters:

Name Type Description Default
licensee_id UUID

The licensee to describe licenses for (only client bindings for licenses scoped to this licensee will be included in the result).

required
client_binding_id UUID

Identifier of the client binding requested.

required
with_metadata bool

Flag to control including verbose information about the licenses and client bindings. Setting this option to true will fetch contract, order, subscription and external reference information at time of original license grant, the license container, a possible license key and related product information. For client bindings the additional information is related to license consumption objects and license consumers. Defaults to false.

False

Returns:

Type Description
LicenseConsumerClientBindingStatus

LicenseConsumerClientBindingStatus listing the requested client binding if found.

Source code in tenduke_scale/license_checkout/license_checkout_client.py
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
def find_client_binding(
    self, licensee_id: UUID, client_binding_id: UUID, with_metadata: bool = False
) -> LicenseConsumerClientBindingStatus:
    """Retrieve a specific client binding.

    Get the details of a client binding, within a licensee, by its unique identifier.

    Args:
        licensee_id:
            The licensee to describe licenses for (only client bindings for licenses scoped to
            this licensee will be included in the result).
        client_binding_id: Identifier of the client binding requested.
        with_metadata:
            Flag to control including verbose information about the licenses and client
            bindings. Setting this option to true will fetch contract, order, subscription and
            external reference information at time of original license grant, the license
            container, a possible license key and related product information. For client
            bindings the additional information is related to license consumption objects and
            license consumers. Defaults to false.

    Returns:
        LicenseConsumerClientBindingStatus listing the requested client binding if found.
    """
    path = "/licensing/actions/find-license-client-binding"
    json = _describe_request_json(
        self.session,
        self.api_url,
        path,
        licensee_id=licensee_id,
        client_binding_id=client_binding_id,
        with_metadata=with_metadata,
    )

    return LicenseConsumerClientBindingStatus.from_api(json)

parse_jwt(jwt)

Parse JWT using public key for Scale API.

Parameters:

Name Type Description Default
jwt str

Raw jwt to parse.

required

Returns:

Type Description
LicenseToken

LicenseToken containing the details from the JWT.

Source code in tenduke_scale/license_checkout/license_checkout_client.py
460
461
462
463
464
465
466
467
468
469
470
def parse_jwt(self, jwt: str) -> LicenseToken:
    """Parse JWT using public key for Scale API.

    Args:
        jwt: Raw jwt to parse.

    Returns:
        LicenseToken containing the details from the JWT.
    """
    pub_key = self._jwk_client.get_signing_key_from_jwt(jwt)
    return LicenseToken(jwt, pub_key.key)

raise_if_consumer_unknown(license_consumer_id=None)

Raise an error is the license consumer is not provided and cannot be inferred.

Return if the license_consumer_id is set, or auth is using ID Token; otherwise raise error.

Parameters:

Name Type Description Default
license_consumer_id Optional[UUID]

license consumer id provided to client method.

None

Raises:

Type Description
ValueError

The license consumer id is not set and there is no id token.

Source code in tenduke_scale/license_checkout/license_checkout_client.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def raise_if_consumer_unknown(self, license_consumer_id: Optional[UUID] = None):
    """Raise an error is the license consumer is not provided and cannot be inferred.

    Return if the license_consumer_id is set, or auth is using ID Token; otherwise raise error.

    Args:
        license_consumer_id: license consumer id provided to client method.

    Raises:
        ValueError: The license consumer id is not set and there is no id token.
    """
    if license_consumer_id or isinstance(self.session.auth, IdTokenAuth):
        return
    raise InvalidArgumentError("license_consumer_id")

LicenseConsumerClientBindingStatus dataclass

Bases: Model

Model for describe consumer client bindings response.

Attributes:

Name Type Description
all_client_bindings_included Optional[bool]

Indicates whether the analysis of a users license consumption found more than the maximum response size worth of client bindings. A value equal to false means the response size was capped and true means all current client bindings have been included in the response. The maximum client binding count included in the response is 5 per license.

client_bindings Optional[Sequence[LicenseConsumptionClientBinding]]

Licenses that are currently known to be associated with a license consuming user using a specific application on a specific device. Note: this list size is limited to a predefined size and provides only a view into a small set of the most recent client bindings. Capping can be inspected by checking the flag: allClientBindingsIncluded.

Source code in tenduke_scale/license_checkout/license_consumer_client_binding_status.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@dataclass
class LicenseConsumerClientBindingStatus(Model):
    """Model for describe consumer client bindings response.

    Attributes:
        all_client_bindings_included:
            Indicates whether the analysis of a users license consumption found more than the
            maximum response size worth of client bindings. A value equal to false means the
            response size was capped and true means all current client bindings have been included
            in the response. The maximum client binding count included in the response is 5 per
            license.
        client_bindings:
            Licenses that are currently known to be associated with a license consuming user using
            a specific application on a specific device. Note: this list size is limited to a
            predefined size and provides only a view into a small set of the most recent client
            bindings. Capping can be inspected by checking the flag: allClientBindingsIncluded.
    """

    all_client_bindings_included: Optional[bool] = field(
        init=True, metadata={"api_name": "allClientBindingsIncluded"}, default=None
    )
    client_bindings: Optional[Sequence[LicenseConsumptionClientBinding]] = field(
        init=True,
        metadata={
            "api_name": "clientBindings",
            "transform": "listtype",
            "type": LicenseConsumptionClientBinding,
        },
        default=None,
    )

LicenseConsumerLicensesStatus dataclass

Bases: Model

Model for describe consumer licenses response.

Attributes:

Name Type Description
licenses Optional[Sequence[License]]

Licenses that are accessible by the license consumer.

Source code in tenduke_scale/license_checkout/license_consumer_licenses_status.py
16
17
18
19
20
21
22
23
24
25
26
@dataclass
class LicenseConsumerLicensesStatus(Model):
    """Model for describe consumer licenses response.

    Attributes:
        licenses: Licenses that are accessible by the license consumer.
    """

    licenses: Optional[Sequence[License]] = field(
        init=True, metadata={"transform": "listtype", "type": License}, default=None
    )

LicenseConsumptionClientBinding dataclass

Bases: Model

Model for license consumption client binding object.

Attributes:

Name Type Description
client_api_key Optional[str]

API key specified in client claims at checkout.

client_country Optional[str]

Country code of OS environment that the client app runs on.

client_host_name Optional[str]

Hostname of device that the client app runs on.

client_hardware_architecture Optional[str]

Architecture of of device / platform that the client app runs on.

client_hardware_id Optional[str]

Hardware id of device that the client app runs on. This claim is required to implement concurrency rules over a user's devices.

client_hardware_label Optional[str]

Hardware label of device that the client app runs on.

client_installation_id Optional[str]

Installation id the client app.

client_language Optional[str]

Language code of the OS environment that the client app runs on.

client_network_ip_address Optional[str]
client_os_user_name Optional[str]

User name from OS environment session client app is running in.

client_os Optional[str]

Name of OS environment that the client app runs on.

client_process_id Optional[str]

Process id of client app process in OS environment that the client app runs on. This claim is required to implement concurrency rules over a user's application instances (processes).

client_version Optional[str]

Client app version. This claim is required to implement rules about allowed versions and enforcing that a license is usable only for certain client application versions. The specified version is compared lexicographically to lower and upper bounds in available licenses. If a license id is used in checkout then the client version must be within the allowed version range in that specific license. Version is enforced only if the license requires it.

consume_duration_ended_at Optional[datetime]

When the consumption ended.

consume_duration_granted_at Optional[datetime]

When the consumption was granted.

created Optional[datetime]

When the client binding was created.

id Optional[int]

Unique identifier for the client binding.

last_heartbeat Optional[datetime]

When the last successful heartbeat was received.

lease_id Optional[str]

Lease id of the lease associated with this binding.

license Optional[License]

The license that this binding is associated with.

license_consumer Optional[LicenseConsumer]

The consumer of the license binding.

locked Optional[bool]

Indicates whether the consumption is locked to this hardware.

modified Optional[datetime]

When the client binding was last modified.

quantity_pre_allocated Optional[int]

Consumption amount allocated at checkout. Pre-allocating a quantity amount applies to quantity types that pure deductible nature, e.g. use count and use time. Its an indicative amount of e.g. use count or use time that a client estimates it needs to complete a task (may be end user driven). Client applications may use pre-allocation to ensure a certain amount of quantity is still available when starting a task. The pre-allocation is deducted from the available quantity and the final outcome is computed at time of release. The verified quantity is set at time of release and if the factual used quantity is less than what pass pre-allocated then the difference is refunded. NOTE: does not apply when consuming seats.

request_ip_address Optional[str]

Address requesting the client binding.

triggered_seat_use Optional[bool]

Indicates if this client binding used a new seat.

valid_from Optional[datetime]

Start of binding validity period.

valid_until Optional[datetime]

End of binding validity period.

verified_quantity Optional[int]

Verified quantity is determined by the consuming client informing the licensing API of factual use. This can happen at time of heartbeat and release. The verified quantity is applies to quantity types that pure deductible nature, e.g. use count and use time. NOTE: does not apply when consuming seats.

Source code in tenduke_scale/license_checkout/license_consumption_client_binding.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
@dataclass
class LicenseConsumptionClientBinding(Model):
    """Model for license consumption client binding object.

    Attributes:
        client_api_key: API key specified in client claims at checkout.
        client_country: Country code of OS environment that the client app runs on.
        client_host_name: Hostname of device that the client app runs on.
        client_hardware_architecture:
            Architecture of of device / platform that the client app runs on.
        client_hardware_id:
            Hardware id of device that the client app runs on. This claim is required to implement
            concurrency rules over a user's devices.
        client_hardware_label: Hardware label of device that the client app runs on.
        client_installation_id: Installation id the client app.
        client_language: Language code of the OS environment that the client app runs on.
        client_network_ip_address:
        client_os_user_name: User name from OS environment session client app is running in.
        client_os: Name of OS environment that the client app runs on.
        client_process_id:
            Process id of client app process in OS environment that the client app runs on. This
            claim is required to implement concurrency rules over a user's application instances
            (processes).
        client_version:
            Client app version. This claim is required to implement rules about allowed versions
            and enforcing that a license is usable only for certain client application versions.
            The specified version is compared lexicographically to lower and upper bounds in
            available licenses. If a license id is used in checkout then the client version must be
            within the allowed version range in that specific license. Version is enforced only if
            the license requires it.
        consume_duration_ended_at: When the consumption ended.
        consume_duration_granted_at: When the consumption was granted.
        created: When the client binding was created.
        id: Unique identifier for the client binding.
        last_heartbeat: When the last successful heartbeat was received.
        lease_id: Lease id of the lease associated with this binding.
        license: The license that this binding is associated with.
        license_consumer: The consumer of the license binding.
        locked: Indicates whether the consumption is locked to this hardware.
        modified: When the client binding was last modified.
        quantity_pre_allocated:
            Consumption amount allocated at checkout.
            Pre-allocating a quantity amount applies to quantity types that pure deductible nature,
            e.g. use count and use time. Its an indicative amount of e.g. use count or use time
            that a client estimates it needs to complete a task (may be end user driven). Client
            applications may use pre-allocation to ensure a certain amount of quantity is still
            available when starting a task. The pre-allocation is deducted from the available
            quantity and the final outcome is computed at time of release. The verified quantity is
            set at time of release and if the factual used quantity is less than what pass
            pre-allocated then the difference is refunded. NOTE: does not apply when consuming
            seats.
        request_ip_address: Address requesting the client binding.
        triggered_seat_use: Indicates if this client binding used a new seat.
        valid_from: Start of binding validity period.
        valid_until: End of binding validity period.
        verified_quantity:
            Verified quantity is determined by the consuming client informing the licensing API of
            factual use. This can happen at time of heartbeat and release. The verified quantity is
            applies to quantity types that pure deductible nature, e.g. use count and use time.
            NOTE: does not apply when consuming seats.
    """

    client_api_key: Optional[str] = field(
        init=True, metadata={"api_name": "cliApiKey"}, default=None
    )
    client_country: Optional[str] = field(
        init=True, metadata={"api_name": "cliCountry"}, default=None
    )
    client_host_name: Optional[str] = field(
        init=True, metadata={"api_name": "cliHostName"}, default=None
    )
    client_hardware_architecture: Optional[str] = field(
        init=True, metadata={"api_name": "cliHwArch"}, default=None
    )
    client_hardware_id: Optional[str] = field(
        init=True, metadata={"api_name": "cliHwId"}, default=None
    )
    client_hardware_label: Optional[str] = field(
        init=True, metadata={"api_name": "cliHwLabel"}, default=None
    )
    client_installation_id: Optional[str] = field(
        init=True, metadata={"api_name": "cliInstallationId"}, default=None
    )
    client_language: Optional[str] = field(
        init=True, metadata={"api_name": "cliLang"}, default=None
    )
    client_network_ip_address: Optional[str] = field(
        init=True, metadata={"api_name": "cliNetworkIpAddress"}, default=None
    )
    client_os_user_name: Optional[str] = field(
        init=True, metadata={"api_name": "cliOSUserName"}, default=None
    )
    client_os: Optional[str] = field(
        init=True, metadata={"api_name": "cliOs"}, default=None
    )
    client_process_id: Optional[str] = field(
        init=True, metadata={"api_name": "cliProcessId"}, default=None
    )
    client_version: Optional[str] = field(
        init=True, metadata={"api_name": "cliVersion"}, default=None
    )
    consume_duration_ended_at: Optional[datetime] = field(
        init=True,
        metadata={"api_name": "consumeDurationEndedAt", "transform": "datetime"},
        default=None,
    )
    consume_duration_granted_at: Optional[datetime] = field(
        init=True,
        metadata={"api_name": "consumeDurationGrantedAt", "transform": "datetime"},
        default=None,
    )
    created: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )
    id: Optional[int] = None
    last_heartbeat: Optional[datetime] = field(
        init=True,
        metadata={"api_name": "lastHeartbeat", "transform": "datetime"},
        default=None,
    )
    lease_id: Optional[str] = field(
        init=True, metadata={"api_name": "leaseId"}, default=None
    )
    license: Optional[License] = field(
        init=True, metadata={"transform": "type", "type": License}, default=None
    )
    license_consumer: Optional[LicenseConsumer] = field(
        init=True,
        metadata={
            "api_name": "licenseConsumer",
            "transform": "type",
            "type": LicenseConsumer,
        },
        default=None,
    )
    locked: Optional[bool] = None
    modified: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )
    quantity_pre_allocated: Optional[int] = field(
        init=True, metadata={"api_name": "qtyPreAlloc"}, default=None
    )
    request_ip_address: Optional[str] = field(
        init=True, metadata={"api_name": "requestIpAddress"}, default=None
    )
    triggered_seat_use: Optional[bool] = field(
        init=True, metadata={"api_name": "triggeredSeatUse"}, default=None
    )
    valid_from: Optional[datetime] = field(
        init=True,
        metadata={"api_name": "validFrom", "transform": "datetime"},
        default=None,
    )
    valid_until: Optional[datetime] = field(
        init=True,
        metadata={"api_name": "validUntil", "transform": "datetime"},
        default=None,
    )
    verified_quantity: Optional[int] = field(
        init=True, metadata={"api_name": "verifiedQty"}, default=None
    )

LicenseHeartbeatArguments dataclass

Bases: Model

Arguments for license heartbeat API operation.

Attributes:

Name Type Description
lease_id str

Lease id value as it was returned in last heartbeat response or initial license checkout if no heartbeat has been done before this heartbeat request.

treat_as_incremental_quantity bool

Default: False. Flag that tells if the usedQty value is absolute or should be treated as an increment. NOTE: does NOT APPLY when using SEAT based licensing.

used_quantity int

The amount used since initial license checkout / previous heartbeat. The usage quantity is an absolute value by default. Set field treatAsIncrementalQty = true to use an incremental tracking algorithm instead. NOTE: does NOT APPLY when using SEAT based licensing.

Source code in tenduke_scale/license_checkout/license_heartbeat_arguments.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@dataclass
class LicenseHeartbeatArguments(Model):
    """Arguments for license heartbeat API operation.

    Attributes:
        lease_id:
            Lease id value as it was returned in last heartbeat response or initial license
            checkout if no heartbeat has been done before this heartbeat request.
        treat_as_incremental_quantity:
            Default: False. Flag that tells if the usedQty value is absolute or should be treated
            as an increment. NOTE: does NOT APPLY when using SEAT based licensing.
        used_quantity:
            The amount used since initial license checkout / previous heartbeat. The usage quantity
            is an absolute value by default. Set field treatAsIncrementalQty = true to use an
            incremental tracking algorithm instead. NOTE: does NOT APPLY when using SEAT based
            licensing.
    """

    lease_id: str = field(init=True, metadata={"api_name": "leaseId"})
    treat_as_incremental_quantity: bool = field(
        init=True,
        metadata={"api_name": "treatAsIncrementalQty", "transform": "bool"},
        default=False,
    )
    used_quantity: int = field(init=True, metadata={"api_name": "usedQty"}, default=1)

LicenseReleaseArguments dataclass

Bases: Model

Model for releasing enforced licenses or ending use of metered licenses.

Attributes:

Name Type Description
lease_id str

Lease id value as it was returned in last heartbeat response or initial license checkout if no heartbeat has been done before release.

final_quantity_used Optional[int]

The final amount used since initial license checkout. NOTE: does not apply when using seat based licensing.

Source code in tenduke_scale/license_checkout/license_release_arguments.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@dataclass
class LicenseReleaseArguments(Model):
    """Model for releasing enforced licenses or ending use of metered licenses.

    Attributes:
        lease_id:
            Lease id value as it was returned in last heartbeat response or initial license
            checkout if no heartbeat has been done before release.
        final_quantity_used:
            The final amount used since initial license checkout. NOTE: does not apply when using
            seat based licensing.
    """

    lease_id: str = field(init=True, metadata={"api_name": "leaseId"})
    final_quantity_used: Optional[int] = field(
        init=True, metadata={"api_name": "finalUsedQty"}, default=None
    )

LicenseReleaseResult dataclass

Bases: Model

Model for LicenseReleaseResult object.

Attributes:

Name Type Description
error_code Optional[str]

Short identifier for error condition.

error_description Optional[str]

Textual description of error condition.

final_used_quantity Optional[int]

The final amount used since initial license checkout.

license_consumer_id Optional[UUID]

The consumer of the license/

product_name Optional[str]

Product name for which license checkout was for.

quantity_dimension Optional[QD]

Enum: "SEATS" "USE_COUNT" "USE_TIME" Quantity dimension, related units of measurement: seats and use count = unitless, use time = seconds.

released Optional[bool]

Was the license released?

released_lease_id Optional[str]

Lease id that has been released.

released_license_id Optional[UUID]

License id of the release checkout.

remaining_quantity Optional[int]

Remaining amount available for the license.

Source code in tenduke_scale/license_checkout/license_release_result.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@dataclass
class LicenseReleaseResult(Model):
    """Model for LicenseReleaseResult object.

    Attributes:
        error_code: Short identifier for error condition.
        error_description: Textual description of error condition.
        final_used_quantity: The final amount used since initial license checkout.
        license_consumer_id: The consumer of the license/
        product_name: Product name for which license checkout was for.
        quantity_dimension:
            Enum: "SEATS" "USE_COUNT" "USE_TIME" Quantity dimension, related units of measurement:
            seats and use count = unitless, use time = seconds.
        released: Was the license released?
        released_lease_id: Lease id that has been released.
        released_license_id: License id of the release checkout.
        remaining_quantity: Remaining amount available for the license.
    """

    error_code: Optional[str] = field(
        init=True,
        metadata={"api_name": "errorCode"},
        default=None,
    )
    error_description: Optional[str] = field(
        init=True,
        metadata={"api_name": "errorDescription"},
        default=None,
    )
    final_used_quantity: Optional[int] = field(
        init=True,
        metadata={"api_name": "finalUsedQty"},
        default=None,
    )
    license_consumer_id: Optional[UUID] = field(
        init=True,
        metadata={"api_name": "licenseConsumerId", "transform": "uuid"},
        default=None,
    )
    product_name: Optional[str] = field(
        init=True,
        metadata={"api_name": "productName"},
        default=None,
    )
    quantity_dimension: Optional[QD] = field(
        init=True,
        metadata={
            "api_name": "qtyDimension",
            "transform": "enum",
            "type": QuantityDimension,
        },
        default=None,
    )
    released: Optional[bool] = None
    released_lease_id: Optional[str] = field(
        init=True,
        metadata={"api_name": "releasedLeaseId"},
        default=None,
    )
    released_license_id: Optional[UUID] = field(
        init=True,
        metadata={"api_name": "releasedLicenseId", "transform": "uuid"},
        default=None,
    )
    remaining_quantity: Optional[int] = field(
        init=True,
        metadata={"api_name": "remainingQty"},
        default=None,
    )

LicenseToken

License token model.

Source code in tenduke_scale/license_checkout/license_token.py
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
class LicenseToken:
    """License token model."""

    def __init__(self, raw_jwt: str, public_key: str):
        """Construct an instance of a LicenseToken from a JWT and public key.

        Args:
            raw_jwt: The base64 encoded string containing the token.
            public_key: The public part of the key pair used to sign the token.
        """
        self.raw_jwt = raw_jwt
        self._public_key = public_key
        self._claims = jwt.decode(raw_jwt, public_key, algorithms=["RS256"])

    @property
    def lease_id(self) -> Optional[str]:
        """Lease id, used to refresh or release license."""
        return self._claims.get("leaseId")

    @property
    def product_name(self) -> str:
        """Product name, identifies what permission is granted."""
        return self._claims["productName"]

    @property
    def features(self) -> str:
        """Features, lists sub features granted in this license."""
        return self._claims["features"]

    @property
    def quantity(self) -> int:
        """Number of seats, uses, or time granted."""
        return self._claims["qty"]

    @property
    def quantity_dimension(self) -> QD:
        """Describes whether this is seats, uses, or use time."""
        return QuantityDimension[self._claims["qtyDimension"]]

    @property
    def jwt_id(self) -> str:
        """JWT Id (jti) claim."""
        return self._claims["jti"]

    @property
    def claims(self) -> Dict[str, Any]:
        """Raw claims from JWT."""
        return self._claims

    @property
    def heartbeat_not_before(self) -> datetime:
        """Heartbeat not before (hbnbf) claim."""
        return datetime.fromtimestamp(self._claims["hbnbf"], UTC)

    @property
    def old_lease_id(self) -> Optional[str]:
        """Old lease id (if this is a continuation of a previous lease)."""
        return self._claims.get("oldLeaseId")

    @property
    def status(self) -> Optional[str]:
        """Status of checkout."""
        return self._claims.get("status")

    @property
    def error_code(self) -> Optional[str]:
        """Error code for a failed consumtpion."""
        return self._claims.get("errorCode")

    @property
    def error_description(self) -> Optional[str]:
        """Error description for a failed consumtpion."""
        return self._claims.get("errorDescription")

    def to_release_argument(
        self, final_quantity_used: Optional[int] = None
    ) -> Optional[LicenseReleaseArguments]:
        """Construct a LicenseReleaseArgument, if this was a successful checkout.

        Args:
            final_quantity_used: Specifies the quantity to send in the release call.

        Returns:
            LicenseReleaseArguments object to release the lease represented by this license token.
            If this license token is for a failed or unsuccessful checkout, then the method returns
            None.
        """
        return (
            LicenseReleaseArguments(
                lease_id=self.lease_id, final_quantity_used=final_quantity_used
            )
            if self.lease_id
            else None
        )

    @classmethod
    def make_release_arguments(
        cls: Type[T], tokens: Sequence[T]
    ) -> Sequence[LicenseReleaseArguments]:
        """Return LicenseReleaseArguments for successful checkouts.

        Any license tokens representing failed or unsuccessful checkouts are dropped from the
        returned sequence.

        Args:
            tokens: List of license tokens that may include one or more successful checkouts.

        Returns:
            Sequence of LicenseReleaseArguments that can be used to release the license tokens that
            represent successful checkouts.
        """
        return [
            arg for token in tokens if (arg := token.to_release_argument()) is not None
        ]

claims: Dict[str, Any] property

Raw claims from JWT.

error_code: Optional[str] property

Error code for a failed consumtpion.

error_description: Optional[str] property

Error description for a failed consumtpion.

features: str property

Features, lists sub features granted in this license.

heartbeat_not_before: datetime property

Heartbeat not before (hbnbf) claim.

jwt_id: str property

JWT Id (jti) claim.

lease_id: Optional[str] property

Lease id, used to refresh or release license.

old_lease_id: Optional[str] property

Old lease id (if this is a continuation of a previous lease).

product_name: str property

Product name, identifies what permission is granted.

quantity: int property

Number of seats, uses, or time granted.

quantity_dimension: QD property

Describes whether this is seats, uses, or use time.

status: Optional[str] property

Status of checkout.

__init__(raw_jwt, public_key)

Construct an instance of a LicenseToken from a JWT and public key.

Parameters:

Name Type Description Default
raw_jwt str

The base64 encoded string containing the token.

required
public_key str

The public part of the key pair used to sign the token.

required
Source code in tenduke_scale/license_checkout/license_token.py
26
27
28
29
30
31
32
33
34
35
def __init__(self, raw_jwt: str, public_key: str):
    """Construct an instance of a LicenseToken from a JWT and public key.

    Args:
        raw_jwt: The base64 encoded string containing the token.
        public_key: The public part of the key pair used to sign the token.
    """
    self.raw_jwt = raw_jwt
    self._public_key = public_key
    self._claims = jwt.decode(raw_jwt, public_key, algorithms=["RS256"])

make_release_arguments(tokens) classmethod

Return LicenseReleaseArguments for successful checkouts.

Any license tokens representing failed or unsuccessful checkouts are dropped from the returned sequence.

Parameters:

Name Type Description Default
tokens Sequence[T]

List of license tokens that may include one or more successful checkouts.

required

Returns:

Type Description
Sequence[LicenseReleaseArguments]

Sequence of LicenseReleaseArguments that can be used to release the license tokens that

Sequence[LicenseReleaseArguments]

represent successful checkouts.

Source code in tenduke_scale/license_checkout/license_token.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
@classmethod
def make_release_arguments(
    cls: Type[T], tokens: Sequence[T]
) -> Sequence[LicenseReleaseArguments]:
    """Return LicenseReleaseArguments for successful checkouts.

    Any license tokens representing failed or unsuccessful checkouts are dropped from the
    returned sequence.

    Args:
        tokens: List of license tokens that may include one or more successful checkouts.

    Returns:
        Sequence of LicenseReleaseArguments that can be used to release the license tokens that
        represent successful checkouts.
    """
    return [
        arg for token in tokens if (arg := token.to_release_argument()) is not None
    ]

to_release_argument(final_quantity_used=None)

Construct a LicenseReleaseArgument, if this was a successful checkout.

Parameters:

Name Type Description Default
final_quantity_used Optional[int]

Specifies the quantity to send in the release call.

None

Returns:

Type Description
Optional[LicenseReleaseArguments]

LicenseReleaseArguments object to release the lease represented by this license token.

Optional[LicenseReleaseArguments]

If this license token is for a failed or unsuccessful checkout, then the method returns

Optional[LicenseReleaseArguments]

None.

Source code in tenduke_scale/license_checkout/license_token.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def to_release_argument(
    self, final_quantity_used: Optional[int] = None
) -> Optional[LicenseReleaseArguments]:
    """Construct a LicenseReleaseArgument, if this was a successful checkout.

    Args:
        final_quantity_used: Specifies the quantity to send in the release call.

    Returns:
        LicenseReleaseArguments object to release the lease represented by this license token.
        If this license token is for a failed or unsuccessful checkout, then the method returns
        None.
    """
    return (
        LicenseReleaseArguments(
            lease_id=self.lease_id, final_quantity_used=final_quantity_used
        )
        if self.lease_id
        else None
    )

MeteredLicenseCheckoutClient

Bases: LicenseCheckoutClientABC

Client for consuming licenses using a metered use model.

Source code in tenduke_scale/license_checkout/metered_license_checkout_client.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
class MeteredLicenseCheckoutClient(LicenseCheckoutClientABC):
    """Client for consuming licenses using a metered use model."""

    def start(
        self,
        to_checkout: Sequence[LicenseCheckoutArguments],
        license_key: Optional[str] = None,
        license_consumer_id: Optional[UUID] = None,
        client_details: Optional[ClientDetails] = None,
    ) -> Sequence[LicenseToken]:
        """Start metered license use.

        Metered use of a license is based on the product name and a signal that use of the license
        has started (calling this method).
        A license ID can optionally be specified to record usage of a specific license.

        Args:
            to_checkout:
                List of arguments objects describing the options when checking out each license.
            license_key: Scale License Key identifying licence(s) to checkout.
            license_consumer_id:
                Sets a header identifying the license consumer. Mandatory if using Scale JWT API
                authorization; otherwise optional.
            client_details: Client claims object for checkout.

        Returns:
            List of license tokens representing successful and failed license checkouts.

        Raises:
            ApiError: Checkout request failed.
        """
        return self._start(
            "start-metered-use",
            to_checkout,
            license_key,
            license_consumer_id,
            client_details,
        )

    def end(
        self,
        to_release: Sequence[LicenseReleaseArguments],
        license_key: Optional[str] = None,
        license_consumer_id: Optional[UUID] = None,
    ) -> Sequence[LicenseReleaseResult]:
        """End metered license use.

        Ending metered use must send the final used quantity, which is recorded by the API as part
        of ending the metered usage session.

        Args:
            to_release:
                List of arguments objects describing the options when releasing each license.
            license_key: Scale License Key identifying licence(s) to release.
            license_consumer_id:
                Sets a header identifying the license consumer. Mandatory if using Scale JWT API
                authorization; otherwise optional.

        Returns:
            List of LicenseReleaseResult objects representing the licenses successfully released.

        Raises:
            ApiError: Release request failed.
        """
        return self._end(
            "end-metered-use", to_release, license_key, license_consumer_id
        )

    def heartbeat(
        self,
        to_heartbeat: Sequence[LicenseHeartbeatArguments],
        license_key: Optional[str] = None,
        license_consumer_id: Optional[UUID] = None,
    ) -> Sequence[LicenseToken]:
        """Update the consumed quantity for a license.

        The heartbeat operation also re-authorizes the use of a license.

        Args:
            to_heartbeat: List of arguments objects describing the licenses to heartbeat.
            license_key: Scale License Key identifying licence(s) to heartbeat.
            license_consumer_id:
                Sets a header identifying the license consumer. Mandatory if using Scale JWT API
                authorization; otherwise optional.

        Returns:
            List of LicenseToken objects for the successful and failed heartbeats.

        Raises:
            ApiError: Heartbeat request failed.
        """
        return self._heartbeat(
            "heartbeat-metered-use",
            to_heartbeat,
            license_key,
            license_consumer_id,
        )

    def start_single_by_license_key(
        self,
        license_key: str,
        to_checkout: LicenseCheckoutArguments,
        client_details: Optional[ClientDetails] = None,
    ) -> LicenseToken:
        """Start use of a single license using a license key.

        Args:
            license_key: Scale License Key identifying licence(s) to checkout.
            to_checkout: An object describing the options for checking out the license.
            client_details: Client claims object for checkout.

        Returns:
            A license token describing the success or failure of the checkout.

        Raises:
            ApiError: Checkout request failed.
        """
        tokens = self._start(
            "start-metered-use",
            [to_checkout],
            license_key,
            client_details=client_details,
        )
        return tokens[0]

    def start_multiple_by_license_key(
        self,
        license_key: str,
        to_checkout: Sequence[LicenseCheckoutArguments],
        client_details: Optional[ClientDetails] = None,
    ) -> Sequence[LicenseToken]:
        """Start use of a multiple licenses using a license key.

        Args:
            license_key: Scale License Key identifying licence(s) to checkout.
            to_checkout:
                A list of objects describing the options for checking out the licenses.
            client_details: Client claims object for checkout.

        Returns:
            List of license tokens representing successful and failed license checkouts.

        Raises:
            ApiError: Checkout request failed.
        """
        return self._start(
            "start-metered-use",
            to_checkout,
            license_key,
            client_details=client_details,
        )

    def end_single_by_license_key(
        self, license_key: str, to_end: LicenseReleaseArguments
    ) -> LicenseReleaseResult:
        """End metered use of a single license using a license key.

        Args:
            license_key: Scale License Key identifying licence(s) to release.
            to_end: An arguments object describing the license to release.

        Returns:
            LicenseReleaseResult object representing the license released.

        Raises:
            ApiError: Release request failed.
        """
        release_results = self._end(
            "end-metered-use", [to_end], license_key=license_key
        )
        return release_results[0]

    def end_multiple_by_license_key(
        self, license_key: str, to_end: Sequence[LicenseReleaseArguments]
    ) -> Sequence[LicenseReleaseResult]:
        """End metered use of a multiple licenses using a license key.

        Args:
            license_key: Scale License Key identifying licence(s) to release.
            to_end: List of arguments objects describing the licenses to release.

        Returns:
            List of LicenseReleaseResult objects representing the licenses successfully released.

        Raises:
            ApiError: Release request failed.
        """
        return self._end("end-metered-use", to_end, license_key=license_key)

    def heartbeat_single_by_license_key(
        self, license_key: str, to_heartbeat: LicenseHeartbeatArguments
    ) -> LicenseToken:
        """Heartbeat a license by license key.

        Args:
            license_key: Scale License Key identifying licence(s) to heartbeat.
            to_heartbeat: An arguments object describing the license to heartbeat.

        Returns:
            LicenseToken object describing the successful or failed heartbeat attempt.

        Raises:
            ApiError: Heartbeat request failed.
        """
        heartbeat_results = self._heartbeat(
            "heartbeat-metered-use", [to_heartbeat], license_key=license_key
        )
        return heartbeat_results[0]

    def heartbeat_multiple_by_license_key(
        self,
        license_key: str,
        to_heartbeat: Sequence[LicenseHeartbeatArguments],
    ) -> Sequence[LicenseToken]:
        """Heartbeat multiple licenses by license key.

        Args:
            license_key: Scale License Key identifying licence(s) to heartbeat.
            to_heartbeat:
                List of arguments objects describing the licenses to heartbeat.

        Returns:
            List of LicenseToken objects for the successful and failed heartbeats.

        Raises:
            ApiError: Heartbeat request failed.
        """
        return self._heartbeat(
            "heartbeat-metered-use",
            args=to_heartbeat,
            license_key=license_key,
        )

end(to_release, license_key=None, license_consumer_id=None)

End metered license use.

Ending metered use must send the final used quantity, which is recorded by the API as part of ending the metered usage session.

Parameters:

Name Type Description Default
to_release Sequence[LicenseReleaseArguments]

List of arguments objects describing the options when releasing each license.

required
license_key Optional[str]

Scale License Key identifying licence(s) to release.

None
license_consumer_id Optional[UUID]

Sets a header identifying the license consumer. Mandatory if using Scale JWT API authorization; otherwise optional.

None

Returns:

Type Description
Sequence[LicenseReleaseResult]

List of LicenseReleaseResult objects representing the licenses successfully released.

Raises:

Type Description
ApiError

Release request failed.

Source code in tenduke_scale/license_checkout/metered_license_checkout_client.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def end(
    self,
    to_release: Sequence[LicenseReleaseArguments],
    license_key: Optional[str] = None,
    license_consumer_id: Optional[UUID] = None,
) -> Sequence[LicenseReleaseResult]:
    """End metered license use.

    Ending metered use must send the final used quantity, which is recorded by the API as part
    of ending the metered usage session.

    Args:
        to_release:
            List of arguments objects describing the options when releasing each license.
        license_key: Scale License Key identifying licence(s) to release.
        license_consumer_id:
            Sets a header identifying the license consumer. Mandatory if using Scale JWT API
            authorization; otherwise optional.

    Returns:
        List of LicenseReleaseResult objects representing the licenses successfully released.

    Raises:
        ApiError: Release request failed.
    """
    return self._end(
        "end-metered-use", to_release, license_key, license_consumer_id
    )

end_multiple_by_license_key(license_key, to_end)

End metered use of a multiple licenses using a license key.

Parameters:

Name Type Description Default
license_key str

Scale License Key identifying licence(s) to release.

required
to_end Sequence[LicenseReleaseArguments]

List of arguments objects describing the licenses to release.

required

Returns:

Type Description
Sequence[LicenseReleaseResult]

List of LicenseReleaseResult objects representing the licenses successfully released.

Raises:

Type Description
ApiError

Release request failed.

Source code in tenduke_scale/license_checkout/metered_license_checkout_client.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def end_multiple_by_license_key(
    self, license_key: str, to_end: Sequence[LicenseReleaseArguments]
) -> Sequence[LicenseReleaseResult]:
    """End metered use of a multiple licenses using a license key.

    Args:
        license_key: Scale License Key identifying licence(s) to release.
        to_end: List of arguments objects describing the licenses to release.

    Returns:
        List of LicenseReleaseResult objects representing the licenses successfully released.

    Raises:
        ApiError: Release request failed.
    """
    return self._end("end-metered-use", to_end, license_key=license_key)

end_single_by_license_key(license_key, to_end)

End metered use of a single license using a license key.

Parameters:

Name Type Description Default
license_key str

Scale License Key identifying licence(s) to release.

required
to_end LicenseReleaseArguments

An arguments object describing the license to release.

required

Returns:

Type Description
LicenseReleaseResult

LicenseReleaseResult object representing the license released.

Raises:

Type Description
ApiError

Release request failed.

Source code in tenduke_scale/license_checkout/metered_license_checkout_client.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
def end_single_by_license_key(
    self, license_key: str, to_end: LicenseReleaseArguments
) -> LicenseReleaseResult:
    """End metered use of a single license using a license key.

    Args:
        license_key: Scale License Key identifying licence(s) to release.
        to_end: An arguments object describing the license to release.

    Returns:
        LicenseReleaseResult object representing the license released.

    Raises:
        ApiError: Release request failed.
    """
    release_results = self._end(
        "end-metered-use", [to_end], license_key=license_key
    )
    return release_results[0]

heartbeat(to_heartbeat, license_key=None, license_consumer_id=None)

Update the consumed quantity for a license.

The heartbeat operation also re-authorizes the use of a license.

Parameters:

Name Type Description Default
to_heartbeat Sequence[LicenseHeartbeatArguments]

List of arguments objects describing the licenses to heartbeat.

required
license_key Optional[str]

Scale License Key identifying licence(s) to heartbeat.

None
license_consumer_id Optional[UUID]

Sets a header identifying the license consumer. Mandatory if using Scale JWT API authorization; otherwise optional.

None

Returns:

Type Description
Sequence[LicenseToken]

List of LicenseToken objects for the successful and failed heartbeats.

Raises:

Type Description
ApiError

Heartbeat request failed.

Source code in tenduke_scale/license_checkout/metered_license_checkout_client.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def heartbeat(
    self,
    to_heartbeat: Sequence[LicenseHeartbeatArguments],
    license_key: Optional[str] = None,
    license_consumer_id: Optional[UUID] = None,
) -> Sequence[LicenseToken]:
    """Update the consumed quantity for a license.

    The heartbeat operation also re-authorizes the use of a license.

    Args:
        to_heartbeat: List of arguments objects describing the licenses to heartbeat.
        license_key: Scale License Key identifying licence(s) to heartbeat.
        license_consumer_id:
            Sets a header identifying the license consumer. Mandatory if using Scale JWT API
            authorization; otherwise optional.

    Returns:
        List of LicenseToken objects for the successful and failed heartbeats.

    Raises:
        ApiError: Heartbeat request failed.
    """
    return self._heartbeat(
        "heartbeat-metered-use",
        to_heartbeat,
        license_key,
        license_consumer_id,
    )

heartbeat_multiple_by_license_key(license_key, to_heartbeat)

Heartbeat multiple licenses by license key.

Parameters:

Name Type Description Default
license_key str

Scale License Key identifying licence(s) to heartbeat.

required
to_heartbeat Sequence[LicenseHeartbeatArguments]

List of arguments objects describing the licenses to heartbeat.

required

Returns:

Type Description
Sequence[LicenseToken]

List of LicenseToken objects for the successful and failed heartbeats.

Raises:

Type Description
ApiError

Heartbeat request failed.

Source code in tenduke_scale/license_checkout/metered_license_checkout_client.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
def heartbeat_multiple_by_license_key(
    self,
    license_key: str,
    to_heartbeat: Sequence[LicenseHeartbeatArguments],
) -> Sequence[LicenseToken]:
    """Heartbeat multiple licenses by license key.

    Args:
        license_key: Scale License Key identifying licence(s) to heartbeat.
        to_heartbeat:
            List of arguments objects describing the licenses to heartbeat.

    Returns:
        List of LicenseToken objects for the successful and failed heartbeats.

    Raises:
        ApiError: Heartbeat request failed.
    """
    return self._heartbeat(
        "heartbeat-metered-use",
        args=to_heartbeat,
        license_key=license_key,
    )

heartbeat_single_by_license_key(license_key, to_heartbeat)

Heartbeat a license by license key.

Parameters:

Name Type Description Default
license_key str

Scale License Key identifying licence(s) to heartbeat.

required
to_heartbeat LicenseHeartbeatArguments

An arguments object describing the license to heartbeat.

required

Returns:

Type Description
LicenseToken

LicenseToken object describing the successful or failed heartbeat attempt.

Raises:

Type Description
ApiError

Heartbeat request failed.

Source code in tenduke_scale/license_checkout/metered_license_checkout_client.py
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def heartbeat_single_by_license_key(
    self, license_key: str, to_heartbeat: LicenseHeartbeatArguments
) -> LicenseToken:
    """Heartbeat a license by license key.

    Args:
        license_key: Scale License Key identifying licence(s) to heartbeat.
        to_heartbeat: An arguments object describing the license to heartbeat.

    Returns:
        LicenseToken object describing the successful or failed heartbeat attempt.

    Raises:
        ApiError: Heartbeat request failed.
    """
    heartbeat_results = self._heartbeat(
        "heartbeat-metered-use", [to_heartbeat], license_key=license_key
    )
    return heartbeat_results[0]

start(to_checkout, license_key=None, license_consumer_id=None, client_details=None)

Start metered license use.

Metered use of a license is based on the product name and a signal that use of the license has started (calling this method). A license ID can optionally be specified to record usage of a specific license.

Parameters:

Name Type Description Default
to_checkout Sequence[LicenseCheckoutArguments]

List of arguments objects describing the options when checking out each license.

required
license_key Optional[str]

Scale License Key identifying licence(s) to checkout.

None
license_consumer_id Optional[UUID]

Sets a header identifying the license consumer. Mandatory if using Scale JWT API authorization; otherwise optional.

None
client_details Optional[ClientDetails]

Client claims object for checkout.

None

Returns:

Type Description
Sequence[LicenseToken]

List of license tokens representing successful and failed license checkouts.

Raises:

Type Description
ApiError

Checkout request failed.

Source code in tenduke_scale/license_checkout/metered_license_checkout_client.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def start(
    self,
    to_checkout: Sequence[LicenseCheckoutArguments],
    license_key: Optional[str] = None,
    license_consumer_id: Optional[UUID] = None,
    client_details: Optional[ClientDetails] = None,
) -> Sequence[LicenseToken]:
    """Start metered license use.

    Metered use of a license is based on the product name and a signal that use of the license
    has started (calling this method).
    A license ID can optionally be specified to record usage of a specific license.

    Args:
        to_checkout:
            List of arguments objects describing the options when checking out each license.
        license_key: Scale License Key identifying licence(s) to checkout.
        license_consumer_id:
            Sets a header identifying the license consumer. Mandatory if using Scale JWT API
            authorization; otherwise optional.
        client_details: Client claims object for checkout.

    Returns:
        List of license tokens representing successful and failed license checkouts.

    Raises:
        ApiError: Checkout request failed.
    """
    return self._start(
        "start-metered-use",
        to_checkout,
        license_key,
        license_consumer_id,
        client_details,
    )

start_multiple_by_license_key(license_key, to_checkout, client_details=None)

Start use of a multiple licenses using a license key.

Parameters:

Name Type Description Default
license_key str

Scale License Key identifying licence(s) to checkout.

required
to_checkout Sequence[LicenseCheckoutArguments]

A list of objects describing the options for checking out the licenses.

required
client_details Optional[ClientDetails]

Client claims object for checkout.

None

Returns:

Type Description
Sequence[LicenseToken]

List of license tokens representing successful and failed license checkouts.

Raises:

Type Description
ApiError

Checkout request failed.

Source code in tenduke_scale/license_checkout/metered_license_checkout_client.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def start_multiple_by_license_key(
    self,
    license_key: str,
    to_checkout: Sequence[LicenseCheckoutArguments],
    client_details: Optional[ClientDetails] = None,
) -> Sequence[LicenseToken]:
    """Start use of a multiple licenses using a license key.

    Args:
        license_key: Scale License Key identifying licence(s) to checkout.
        to_checkout:
            A list of objects describing the options for checking out the licenses.
        client_details: Client claims object for checkout.

    Returns:
        List of license tokens representing successful and failed license checkouts.

    Raises:
        ApiError: Checkout request failed.
    """
    return self._start(
        "start-metered-use",
        to_checkout,
        license_key,
        client_details=client_details,
    )

start_single_by_license_key(license_key, to_checkout, client_details=None)

Start use of a single license using a license key.

Parameters:

Name Type Description Default
license_key str

Scale License Key identifying licence(s) to checkout.

required
to_checkout LicenseCheckoutArguments

An object describing the options for checking out the license.

required
client_details Optional[ClientDetails]

Client claims object for checkout.

None

Returns:

Type Description
LicenseToken

A license token describing the success or failure of the checkout.

Raises:

Type Description
ApiError

Checkout request failed.

Source code in tenduke_scale/license_checkout/metered_license_checkout_client.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def start_single_by_license_key(
    self,
    license_key: str,
    to_checkout: LicenseCheckoutArguments,
    client_details: Optional[ClientDetails] = None,
) -> LicenseToken:
    """Start use of a single license using a license key.

    Args:
        license_key: Scale License Key identifying licence(s) to checkout.
        to_checkout: An object describing the options for checking out the license.
        client_details: Client claims object for checkout.

    Returns:
        A license token describing the success or failure of the checkout.

    Raises:
        ApiError: Checkout request failed.
    """
    tokens = self._start(
        "start-metered-use",
        [to_checkout],
        license_key,
        client_details=client_details,
    )
    return tokens[0]

TokenStoreABC

Bases: ABC

License token store base class.

This type defines the methods that will be used to read, persist, and delete license tokens.

These methods are called by :class:tenduke_scale.license_checkout.LicenseCheckoutClientABC and its subclasses to load, store, and delete :class:tenduke_scale.license_checkout.LicenseToken instances based on the operations called on the client.

Source code in tenduke_scale/license_checkout/token_store.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class TokenStoreABC(ABC):
    """License token store base class.

    This type defines the methods that will be used to read, persist, and delete
    license tokens.

    These methods are called by :class:`tenduke_scale.license_checkout.LicenseCheckoutClientABC`
    and its subclasses to load, store, and delete
    :class:`tenduke_scale.license_checkout.LicenseToken` instances based on the operations called on
    the client.
    """

    @abstractmethod
    def save(self, tokens: Sequence[LicenseToken]):
        """Save LicenseTokens to the store.

        This operation should be additive.

        Args:
            tokens: List of tokens to save. This should be interpretted as replacing the current
                    contents of the store.
        """

    @abstractmethod
    def load(self) -> Sequence[LicenseToken]:
        """Load any currently stored LicenseTokens.

        Returns:
            The sequence of LicenseTokens currently stored or persisted in the token store.
        """

    @abstractmethod
    def remove_all(self):
        """Clear the contents of the store."""

    def remove(self, to_remove: Union[Sequence[str], Sequence[LicenseToken]]):
        """Remove the specified LicenseTokens, by lease id or by token.

        Args:
            to_remove: List of lease ids or tokens to remove from the store.
        """
        if not to_remove:
            return
        current_tokens = self.load()
        lease_ids = _to_lease_ids(to_remove)
        tokens_to_save = [
            token for token in current_tokens if token.lease_id not in lease_ids
        ]
        self.remove_all()
        self.save(tokens_to_save)

    def update(
        self,
        tokens: Sequence[LicenseToken],
    ):
        """Store the new tokens and remove old tokens that there are new checkouts for.

        Args:
            tokens: List of new tokens to add to the store, replacing any previously
                    held tokens that are superseded.
        """
        keep = [
            token
            for token in self.load()
            if not any(
                new_token
                for new_token in tokens
                if new_token.old_lease_id == token.lease_id
            )
        ]
        self.remove_all()
        self.save([*keep, *tokens])

load() abstractmethod

Load any currently stored LicenseTokens.

Returns:

Type Description
Sequence[LicenseToken]

The sequence of LicenseTokens currently stored or persisted in the token store.

Source code in tenduke_scale/license_checkout/token_store.py
45
46
47
48
49
50
51
@abstractmethod
def load(self) -> Sequence[LicenseToken]:
    """Load any currently stored LicenseTokens.

    Returns:
        The sequence of LicenseTokens currently stored or persisted in the token store.
    """

remove(to_remove)

Remove the specified LicenseTokens, by lease id or by token.

Parameters:

Name Type Description Default
to_remove Union[Sequence[str], Sequence[LicenseToken]]

List of lease ids or tokens to remove from the store.

required
Source code in tenduke_scale/license_checkout/token_store.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def remove(self, to_remove: Union[Sequence[str], Sequence[LicenseToken]]):
    """Remove the specified LicenseTokens, by lease id or by token.

    Args:
        to_remove: List of lease ids or tokens to remove from the store.
    """
    if not to_remove:
        return
    current_tokens = self.load()
    lease_ids = _to_lease_ids(to_remove)
    tokens_to_save = [
        token for token in current_tokens if token.lease_id not in lease_ids
    ]
    self.remove_all()
    self.save(tokens_to_save)

remove_all() abstractmethod

Clear the contents of the store.

Source code in tenduke_scale/license_checkout/token_store.py
53
54
55
@abstractmethod
def remove_all(self):
    """Clear the contents of the store."""

save(tokens) abstractmethod

Save LicenseTokens to the store.

This operation should be additive.

Parameters:

Name Type Description Default
tokens Sequence[LicenseToken]

List of tokens to save. This should be interpretted as replacing the current contents of the store.

required
Source code in tenduke_scale/license_checkout/token_store.py
34
35
36
37
38
39
40
41
42
43
@abstractmethod
def save(self, tokens: Sequence[LicenseToken]):
    """Save LicenseTokens to the store.

    This operation should be additive.

    Args:
        tokens: List of tokens to save. This should be interpretted as replacing the current
                contents of the store.
    """

update(tokens)

Store the new tokens and remove old tokens that there are new checkouts for.

Parameters:

Name Type Description Default
tokens Sequence[LicenseToken]

List of new tokens to add to the store, replacing any previously held tokens that are superseded.

required
Source code in tenduke_scale/license_checkout/token_store.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def update(
    self,
    tokens: Sequence[LicenseToken],
):
    """Store the new tokens and remove old tokens that there are new checkouts for.

    Args:
        tokens: List of new tokens to add to the store, replacing any previously
                held tokens that are superseded.
    """
    keep = [
        token
        for token in self.load()
        if not any(
            new_token
            for new_token in tokens
            if new_token.old_lease_id == token.lease_id
        )
    ]
    self.remove_all()
    self.save([*keep, *tokens])

Licensing model types

Common models used in the License Checkout API client and other API clients can be found in the [tenduke_scale.licensing] module.

Licensing types common to checkout and license management.

ConsumerType

Bases: Enum

Consumer type enumeration.

Used to indicate if this is an individual or not. All types except PERSON behave in the same way in the API.

Source code in tenduke_scale/licensing/consumer_type.py
 6
 7
 8
 9
10
11
12
13
14
15
16
class ConsumerType(Enum):
    """Consumer type enumeration.

    Used to indicate if this is an individual or not. All types except PERSON behave in the same
    way in the API.
    """

    COMPANY = "COMPANY", "The license consumer is a company."
    ORGANIZATION = "ORGANIZATION", "The license consumer is an organization."
    PERSON = "PERSON", "The license consumer is a person."
    UNDEFINED = "UNDEFINED", "The type of the license consumer is not known."

EffectiveProductConfigInfo dataclass

Bases: Model

Model for product configuration information.

Attributes:

Name Type Description
features Sequence[str]

Formal names of the enabled features in the configuration.

license_model_id UUID

Unique id of the license model.

license_model_name str

Formal name of the license model.

product_config_display_name str

Display name of the product configuration.

product_config_id UUID

Unique id of the product configuration.

product_config_name str

Formal name of the product configuration.

product_display_name str

Display name of the product.

product_id UUID

Unique id of the product.

product_name str

Formal name of the product.

quantity_dimension QD

Enum: "SEATS" "USE_COUNT" "USE_TIME". Dimension of the quantity that licenses granted based on the product configuration have. Units of measurement: seats and use count = unitless, use time = seconds.

created Optional[datetime]

When the product configuration was created.

id Optional[UUID]

Unique id of the product configuration.

modified Optional[datetime]

When the product configuration was last modified.

Source code in tenduke_scale/licensing/effective_product_config_info.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
@dataclass
class EffectiveProductConfigInfo(Model):
    """Model for product configuration information.

    Attributes:
        features: Formal names of the enabled features in the configuration.
        license_model_id: Unique id of the license model.
        license_model_name: Formal name of the license model.
        product_config_display_name: Display name of the product configuration.
        product_config_id: Unique id of the product configuration.
        product_config_name: Formal name of the product configuration.
        product_display_name: Display name of the product.
        product_id: Unique id of the product.
        product_name: Formal name of the product.
        quantity_dimension:
            Enum: "SEATS" "USE_COUNT" "USE_TIME". Dimension of the quantity that licenses granted
            based on the product configuration have. Units of measurement: seats and use count =
            unitless, use time = seconds.
        created: When the product configuration was created.
        id: Unique id of the product configuration.
        modified: When the product configuration was last modified.
    """

    features: Sequence[str]
    license_model_id: UUID = field(
        init=True, metadata={"api_name": "licenseModelId", "transform": "uuid"}
    )
    license_model_name: str = field(
        init=True, metadata={"api_name": "licenseModelName"}
    )
    product_config_display_name: str = field(
        init=True, metadata={"api_name": "productConfigDisplayName"}
    )
    product_config_id: UUID = field(
        init=True, metadata={"api_name": "productConfigId", "transform": "uuid"}
    )
    product_config_name: str = field(
        init=True, metadata={"api_name": "productConfigName"}
    )
    product_display_name: str = field(
        init=True, metadata={"api_name": "productDisplayName"}
    )
    product_id: UUID = field(
        init=True, metadata={"api_name": "productId", "transform": "uuid"}
    )
    product_name: str = field(init=True, metadata={"api_name": "productName"})
    quantity_dimension: QD = field(
        init=True,
        metadata={
            "api_name": "qtyDimension",
            "transform": "enum",
            "type": QuantityDimension,
        },
    )
    created: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )
    id: Optional[UUID] = field(init=True, metadata={"transform": "uuid"}, default=None)
    modified: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )

License dataclass

Bases: Model

Model for license object.

Attributes:

Name Type Description
product_name str
quantity_dimension QD
quantity_enforcement_type QET
allowed_version_lower_bound Optional[str]

Lower bound of allowed client application version. A null and empty value is interpreted as version = undefined (any version allowed). Note: version identifiers are compared by their natural sort order to determine if one version is more or less than another being compared to.

allowed_version_upper_bound Optional[str]

Upper bound of allowed client application version. A null and empty value is interpreted as version = undefined (any version allowed).

concurrent_user_app_instances_per_seat Optional[int]

Defines maximum concurrent application instances per user per seat. Becomes effective if value is > 0. Application instance maps usually to an operating system process. NOTE: requires cliProcessId claim to be sent by client application checking out a license.

created Optional[datetime]

When the license was created.

display_name Optional[str]

License name in the format that can be used for presentation purposes.

feature_names Optional[str]

List of feature names the license enables.

features Optional[Sequence[LicenseFeature]]

List of features this license enables. Note that the feature list may be empty and a minimum viable license works on basis of a product name

id Optional[UUID]

Unique id of the license.

license_key Optional[LicenseKey]

List of features this license enables. Note that the feature list may be empty and a minimum viable license works on basis of a product name.

license_metadata Optional[LicenseMetadata]

Information about the license and associations it has. This object is not present by default. To include metadata in a license read or list response the caller must ask for it.

modified Optional[datetime]

When the license was last modified.

quantity Optional[int]

The pure numerical part of quantity assigned to a license. Maximum qty = 1000 when qtyDimension = SEATS. Note: qty = -1 denotes metered use license and is not applicable for licenses that function in enforcing mode.

valid_from Optional[datetime]

Defines date-time when license(s) validity starts.

valid_until Optional[datetime]

Defines date-time when license(s) expires.

Source code in tenduke_scale/licensing/license.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
@dataclass
class License(Model):
    """Model for license object.

    Attributes:
        product_name:
        quantity_dimension:
        quantity_enforcement_type:
        allowed_version_lower_bound:
            Lower bound of allowed client application version. A null and empty value is
            interpreted as version = undefined (any version allowed). Note: version identifiers are
            compared by their natural sort order to determine if one version is more or less than
            another being compared to.
        allowed_version_upper_bound:
            Upper bound of allowed client application version. A null and empty value is
            interpreted as version = undefined (any version allowed).
        concurrent_user_app_instances_per_seat:
            Defines maximum concurrent application instances per user per seat. Becomes effective
            if value is > 0. Application instance maps usually to an operating system process.
            NOTE: requires cliProcessId claim to be sent by client application checking out a
            license.
        created: When the license was created.
        display_name: License name in the format that can be used for presentation purposes.
        feature_names: List of feature names the license enables.
        features:
            List of features this license enables. Note that the feature list may be empty and a
            minimum viable license works on basis of a product name
        id: Unique id of the license.
        license_key:
            List of features this license enables. Note that the feature list may be empty and a
            minimum viable license works on basis of a product name.
        license_metadata:
            Information about the license and associations it has. This object is not present by
            default. To include metadata in a license read or list response the caller must ask for
            it.
        modified: When the license was last modified.
        quantity:
            The pure numerical part of quantity assigned to a license. Maximum qty = 1000 when
            qtyDimension = SEATS. Note: qty = -1 denotes metered use license and is not applicable
            for licenses that function in enforcing mode.
        valid_from: Defines date-time when license(s) validity starts.
        valid_until: Defines date-time when license(s) expires.
    """

    product_name: str = field(init=True, metadata={"api_name": "productName"})
    quantity_dimension: QD = field(
        init=True,
        metadata={
            "api_name": "qtyDimension",
            "transform": "enum",
            "type": QuantityDimension,
        },
    )
    quantity_enforcement_type: QET = field(
        init=True,
        metadata={
            "api_name": "qtyEnforcementType",
            "transform": "enum",
            "type": QuantityEnforcementType,
        },
    )
    allowed_version_lower_bound: Optional[str] = field(
        init=True, metadata={"api_name": "allowedVersionLowerBound"}, default=None
    )
    allowed_version_upper_bound: Optional[str] = field(
        init=True, metadata={"api_name": "allowedVersionUpperBound"}, default=None
    )
    allowed_versions_display_name: Optional[str] = field(
        init=True, metadata={"api_name": "allowedVersionsDisplayName"}, default=None
    )
    concurrent_user_app_instances_per_seat: Optional[int] = field(
        init=True,
        metadata={"api_name": "concurrentUserAppInstancesPerSeat"},
        default=None,
    )
    concurrent_user_devices_per_seat: Optional[int] = field(
        init=True, metadata={"api_name": "concurrentUserDevicesPerSeat"}, default=None
    )
    created: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )
    display_name: Optional[str] = field(
        init=True, metadata={"api_name": "displayName"}, default=None
    )
    feature_names: Optional[str] = field(
        init=True, metadata={"api_name": "featureNames"}, default=None
    )
    features: Optional[Sequence[LicenseFeature]] = field(
        init=True,
        metadata={"transform": "listtype", "type": LicenseFeature},
        default=None,
    )
    id: Optional[UUID] = field(init=True, metadata={"transform": "uuid"}, default=None)
    license_key: Optional[LicenseKey] = field(
        init=True,
        metadata={"api_name": "licenseKey", "transform": "type", "type": LicenseKey},
        default=None,
    )
    license_metadata: Optional[LicenseMetadata] = field(
        init=True,
        metadata={
            "api_name": "licenseMetadata",
            "transform": "type",
            "type": LicenseMetadata,
        },
        default=None,
    )
    modified: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )
    quantity: Optional[int] = field(
        init=True, metadata={"api_name": "qty"}, default=None
    )
    valid_from: Optional[datetime] = field(
        init=True,
        metadata={"api_name": "validFrom", "transform": "datetime"},
        default=None,
    )
    valid_until: Optional[datetime] = field(
        init=True,
        metadata={"api_name": "validUntil", "transform": "datetime"},
        default=None,
    )

LicenseConsumer dataclass

Bases: Model

Represents an entity that consumes and/or owns licenses.

Attributes:

Name Type Description
name str

Technical name which carries significance for finding and matching data.

type CT

Enum: "DEVICE" "LICENSE_KEY" "PERSON".

connected_identity_id Optional[str]

Optional identifier of an identity domain entity that this object maps to. The most common example is a user id, meaning the OIDC subject value in Id Tokens. The sub claim is used to match license consumer entities in licensing when using Id Token to authorize API calls. This id field may also denote a computer component or device (technical actor generally). In this case the license consumer type would also indicate a non human type.

contact_info Optional[str]

Optional contact information data to store with identity type objects.

created Optional[datetime]

When the consumer object was created.

description Optional[str]

Optional description.

display_label Optional[str]

Optional label intended to be used for presentation purposes.

display_name Optional[str]

Optional name intended to be used for presentation purposes.

email Optional[str]

Optional email address, value must be unique when specified.

external_reference Optional[str]

Optional reference field that helps associate licensing data with data in other systems. NOTE: value must be unique per object (null allowed).

id Optional[UUID]

Unique id for license consumer.

modified Optional[datetime]

When the license consumer was last modified.

natural_id Optional[str]

A unique natural id for the licensee. The value may be e.g. a customer account id, company VAT id, a user identifier or any unique value that makes sense in the system that owns customer master data records. If value is not provided it will be generated.

Source code in tenduke_scale/licensing/license_consumer.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@dataclass
class LicenseConsumer(Model):
    """Represents an entity that consumes and/or owns licenses.

    Attributes:
        name: Technical name which carries significance for finding and matching data.
        type: Enum: "DEVICE" "LICENSE_KEY" "PERSON".
        connected_identity_id:
            Optional identifier of an identity domain entity that this object maps to. The most
            common example is a user id, meaning the OIDC subject value in Id Tokens. The sub claim
            is used to match license consumer entities in licensing when using Id Token to
            authorize API calls. This id field may also denote a computer component or device
            (technical actor generally). In this case the license consumer type would also indicate
            a non human type.
        contact_info: Optional contact information data to store with identity type objects.
        created: When the consumer object was created.
        description: Optional description.
        display_label: Optional label intended to be used for presentation purposes.
        display_name: Optional name intended to be used for presentation purposes.
        email: Optional email address, value must be unique when specified.
        external_reference:
            Optional reference field that helps associate licensing data with data in other
            systems. NOTE: value must be unique per object (null allowed).
        id: Unique id for license consumer.
        modified: When the license consumer was last modified.
        natural_id:
            A unique natural id for the licensee. The value may be e.g. a customer account id,
            company VAT id, a user identifier or any unique value that makes sense in the system
            that owns customer master data records. If value is not provided it will be generated.
    """

    name: str
    type: CT = field(init=True, metadata={"transform": "enum", "type": ConsumerType})
    connected_identity_id: Optional[str] = field(
        init=True, metadata={"api_name": "connectedIdentityId"}, default=None
    )
    contact_info: Optional[str] = field(
        init=True, metadata={"api_name": "contactInfo"}, default=None
    )
    created: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )
    description: Optional[str] = None
    display_label: Optional[str] = field(
        init=True, metadata={"api_name": "displayLabel"}, default=None
    )
    display_name: Optional[str] = field(
        init=True, metadata={"api_name": "displayName"}, default=None
    )
    email: Optional[str] = None
    external_reference: Optional[str] = field(
        init=True, metadata={"api_name": "externalReference"}, default=None
    )
    id: Optional[UUID] = field(init=True, metadata={"transform": "uuid"}, default=None)
    modified: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )
    natural_id: Optional[str] = field(
        init=True, metadata={"api_name": "naturalId"}, default=None
    )

LicenseContainer dataclass

Bases: Model

License container model.

Attributes:

Name Type Description
created Optional[datetime]

When the license container was created.

id Optional[UUID]

Unique id of the license container.

modified Optional[datetime]

When the license container was last modified.

name Optional[str]

Name to identify license container.

used_as_default Optional[bool]

Indicates if this is the default container for the licensee.

Source code in tenduke_scale/licensing/license_container.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@dataclass
class LicenseContainer(Model):
    """License container model.

    Attributes:
        created: When the license container was created.
        id: Unique id of the license container.
        modified: When the license container was last modified.
        name: Name to identify license container.
        used_as_default: Indicates if this is the default container for the licensee.
    """

    created: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )
    id: Optional[UUID] = field(init=True, metadata={"transform": "uuid"}, default=None)
    modified: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )
    name: Optional[str] = None
    used_as_default: Optional[bool] = field(
        init=True, metadata={"api_name": "usedAsDefault"}, default=None
    )

LicenseFeature dataclass

Bases: Model

License feature model.

Attributes:

Name Type Description
created Optional[datetime]

When was the feature created.

feature Optional[str]

Name of the feature.

id Optional[UUID]

Unique id of the feature.

modified Optional[datetime]

When was the feature last modified.

Source code in tenduke_scale/licensing/license_feature.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@dataclass
class LicenseFeature(Model):
    """License feature model.

    Attributes:
        created: When was the feature created.
        feature: Name of the feature.
        id: Unique id of the feature.
        modified: When was the feature last modified.
    """

    created: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )
    feature: Optional[str] = None
    id: Optional[UUID] = field(init=True, metadata={"transform": "uuid"}, default=None)
    modified: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )

LicenseKey dataclass

Bases: Model

License key model.

Attributes:

Name Type Description
license_key str

License key string representation for use in API calls.

allowed_activations Optional[int]

Number of activation codes allowed for the license key.

created Optional[datetime]

When the license key was created.

id Optional[UUID]

Unique id for the license key.

modified Optional[datetime]

When the license key was last modified.

Source code in tenduke_scale/licensing/license_key.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@dataclass
class LicenseKey(Model):
    """License key model.

    Attributes:
        license_key: License key string representation for use in API calls.
        allowed_activations: Number of activation codes allowed for the license key.
        created: When the license key was created.
        id: Unique id for the license key.
        modified: When the license key was last modified.
    """

    license_key: str = field(init=True, metadata={"api_name": "licenseKey"})
    allowed_activations: Optional[int] = field(
        init=True, metadata={"api_name": "allowedActivations"}, default=None
    )
    created: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )
    id: Optional[UUID] = field(init=True, metadata={"transform": "uuid"}, default=None)
    modified: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )

LicenseMetadata dataclass

Bases: Model

License metadata model.

Attributes:

Name Type Description
contract_reference Optional[str]

Optional reference field that associates a license with an external contract id. This field is present and has a value if it was included in the request when issuing the license.

license_container Optional[LicenseContainer]

The license container that the license(s) are contained by.

license_model Optional[LicenseModel]

The license model that was assigned to the license(s) on creation.

order_reference Optional[str]

Optional reference field that associates a license with an external order id. This field is present and has a value if it was included in the request when issuing the license.

product_config_info Optional[EffectiveProductConfigInfo]

Optional product configuration information that was used as specification to issue the new licenses. This object is null or missing if issuing licenses was done by dynamic product name and features.

product_reference Optional[str]

Optional reference field that associates a license with an external product id. This field is present and has a value if it was included in the request when issuing the license.

subscription_reference Optional[str]

Optional reference field that associates a license with an external subscription id. This field is present and has a value if it was included in the request when issuing the license.

Source code in tenduke_scale/licensing/license_metadata.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
@dataclass
class LicenseMetadata(Model):
    """License metadata model.

    Attributes:
        contract_reference:
            Optional reference field that associates a license with an external contract id. This
            field is present and has a value if it was included in the request when issuing the
            license.
        license_container: The license container that the license(s) are contained by.
        license_model: The license model that was assigned to the license(s) on creation.
        order_reference:
            Optional reference field that associates a license with an external order id. This
            field is present and has a value if it was included in the request when issuing the
            license.
        product_config_info:
            Optional product configuration information that was used as specification to issue the
            new licenses. This object is null or missing if issuing licenses was done by dynamic
            product name and features.
        product_reference:
            Optional reference field that associates a license with an external product id. This
            field is present and has a value if it was included in the request when issuing the
            license.
        subscription_reference:
            Optional reference field that associates a license with an external subscription id.
            This field is present and has a value if it was included in the request when issuing
            the license.
    """

    contract_reference: Optional[str] = field(
        init=True, metadata={"api_name": "contractReference"}, default=None
    )
    license_container: Optional[LicenseContainer] = field(
        init=True,
        metadata={
            "api_name": "licenseContainer",
            "transform": "type",
            "type": LicenseContainer,
        },
        default=None,
    )
    license_model: Optional[LicenseModel] = field(
        init=True,
        metadata={
            "api_name": "licenseModel",
            "transform": "type",
            "type": LicenseModel,
        },
        default=None,
    )
    order_reference: Optional[str] = field(
        init=True, metadata={"api_name": "orderReference"}, default=None
    )
    product_config_info: Optional[EffectiveProductConfigInfo] = field(
        init=True,
        metadata={
            "api_name": "productConfigInfo",
            "transform": "type",
            "type": EffectiveProductConfigInfo,
        },
        default=None,
    )
    product_reference: Optional[str] = field(
        init=True, metadata={"api_name": "productReference"}, default=None
    )
    subscription_reference: Optional[str] = field(
        init=True, metadata={"api_name": "subscriptionReference"}, default=None
    )

LicenseModel dataclass

Bases: Model

Model for License model object.

Attributes:

Name Type Description
name str

Name of the license model.

concurrent_user_app_instances_per_seat Optional[int]

Defines maximum concurrent application instances per user per seat. Becomes effective if value is > 0. Application instance maps usually to an operating system process. NOTE: requires cliProcessId claim to be sent by client application checking out a license.

concurrent_user_devices_per_seat Optional[int]

Defines maximum concurrent devices per user per seat. Becomes effective if value is > 0 . Device instance maps usually to a work station, laptop, mobile device or similar. NOTE: requires cliHwId claim to be sent by client application when checking out a license.

created Optional[datetime]

When the license model was created.

id Optional[UUID]

Unique id of the license model.

max_assignments_per_user Optional[int]

Defines maximum number of license consumptions (assignments) that a user can have per license.

modified Optional[datetime]

When the license model was last modified.

Source code in tenduke_scale/licensing/license_model.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@dataclass
class LicenseModel(Model):
    """Model for License model object.

    Attributes:
        name: Name of the license model.
        concurrent_user_app_instances_per_seat:
            Defines maximum concurrent application instances per user per seat. Becomes effective
            if value is > 0. Application instance maps usually to an operating system process.
            NOTE: requires cliProcessId claim to be sent by client application checking out a
            license.
        concurrent_user_devices_per_seat:
            Defines maximum concurrent devices per user per seat. Becomes effective if value is > 0
            . Device instance maps usually to a work station, laptop, mobile device or similar.
            NOTE: requires cliHwId claim to be sent by client application when checking out a
            license.
        created: When the license model was created.
        id: Unique id of the license model.
        max_assignments_per_user:
            Defines maximum number of license consumptions (assignments) that a user can have per
            license.
        modified: When the license model was last modified.
    """

    name: str
    concurrent_user_app_instances_per_seat: Optional[int] = field(
        init=True,
        metadata={"api_name": "concurrentUserAppInstancesPerSeat"},
        default=None,
    )
    concurrent_user_devices_per_seat: Optional[int] = field(
        init=True, metadata={"api_name": "concurrentUserDevicesPerSeat"}, default=None
    )
    created: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )
    id: Optional[UUID] = field(init=True, metadata={"transform": "uuid"}, default=None)
    max_assignments_per_user: Optional[int] = field(
        init=True, metadata={"api_name": "maxAssignmentsPerUser"}, default=None
    )
    modified: Optional[datetime] = field(
        init=True, metadata={"transform": "datetime"}, default=None
    )

QuantityDimension

Bases: Enum

Quantity dimension enumeration.

Source code in tenduke_scale/licensing/quantity_dimension.py
 6
 7
 8
 9
10
11
class QuantityDimension(Enum):
    """Quantity dimension enumeration."""

    SEATS = "SEATS", "Quantity in interpreted as number of seats."
    USE_COUNT = "USE_COUNT", "Quantity in interpreted as number of uses."
    USE_TIME = "USE_TIME", "Quantity in interpreted as amount of time used for."

QuantityEnforcementType

Bases: Enum

Quantity enforcement type enumeration.

Source code in tenduke_scale/licensing/quantity_enforcement_type.py
 6
 7
 8
 9
10
class QuantityEnforcementType(Enum):
    """Quantity enforcement type enumeration."""

    ENFORCED = "ENFORCED", "License checkouts enforce license constraints."
    METERED = "METERED", "License checkouts record and meter usage."

Utilities

Some utility classes are provided to help build the arguments for API calls.

Options for describing licenses and client bindings

Various describe API methods (for listing licenses and client bindings) accept optional query parameters controlling the data included in the response.

This type is used to set those options as required.

Bases: Model

Model for arguments to describe license operations.

Attributes:

Name Type Description
filter_field Optional[str]

Name of field to apply filter value on. Valid fields depend on object tyoe returned by call.

filter_value Optional[str]

Filter value to apply on licenses.

with_metadata Optional[bool]

Flag to control including verbose information about the licenses and client bindings. Setting this option to true will fetch contract, order, subscription and external reference information at time of original license grant, the license container, a possible license key and related product information. For client bindings the additional information is related to license consumption objects and license consumers. Defaults to false.

Source code in tenduke_scale/describe_license_options.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@dataclass
class DescribeLicenseOptions(Model):
    """Model for arguments to describe license operations.

    Attributes:
        filter_field:
            Name of field to apply filter value on. Valid fields depend on object tyoe returned by
            call.
        filter_value: Filter value to apply on licenses.
        with_metadata:
            Flag to control including verbose information about the licenses and client bindings.
            Setting this option to true will fetch contract, order, subscription and external
            reference information at time of original license grant, the license container, a
            possible license key and related product information. For client bindings the
            additional information is related to license consumption objects and license consumers.
            Defaults to false.
    """

    filter_field: Optional[str] = field(
        init=True, metadata={"api_name": "filterField"}, default=None
    )
    filter_value: Optional[str] = field(
        init=True, metadata={"api_name": "filterValue"}, default=None
    )
    with_metadata: Optional[bool] = field(
        init=True,
        metadata={"api_name": "withMetadata", "transform": "str"},
        default=None,
    )

    def to_query_string(self) -> str:
        """Convert object to query string fragment.

        Returns:
            Attributes of object formatted for inclusion in the query string of an HTTP request to
            the API.
        """
        data = self.to_api()
        key_values = [f"{k}={v}" for k, v in data.items()]
        return "&".join(key_values)

to_query_string()

Convert object to query string fragment.

Returns:

Type Description
str

Attributes of object formatted for inclusion in the query string of an HTTP request to

str

the API.

Source code in tenduke_scale/describe_license_options.py
38
39
40
41
42
43
44
45
46
47
def to_query_string(self) -> str:
    """Convert object to query string fragment.

    Returns:
        Attributes of object formatted for inclusion in the query string of an HTTP request to
        the API.
    """
    data = self.to_api()
    key_values = [f"{k}={v}" for k, v in data.items()]
    return "&".join(key_values)

Paging and ordering response data

When calling describe API methods to list licensees, licenses, or client bindings the matching data can include many records.

tenduke_scale.PagingOptions is provided to allow the caller to control the order and the paging of the response data.

Bases: Model

Paging args for API calls.

Any fields that are not None are added as headers to the API call.

Attributes:

Name Type Description
offset Optional[int]

Offset for paging results. Defaults to 0. Applies to licenses, not the additional information included when parameter withMetadata == true.

limit Optional[int]

Limit for controlling result size. Defaults to 5. Applies to licenses, not the additional information included when parameter withMetadata == true.

order_by Optional[str]

Field name to order results by. Valid fields depend on object tyoe returned by call.

order_asc Optional[bool]

Flag that controls ordering in ascending vs. descending order. Defaults to false, meaning descending order.

Source code in tenduke_scale/paging.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@dataclass
class PagingOptions(Model):
    """Paging args for API calls.

    Any fields that are not `None` are added as headers to the API call.

    Attributes:
        offset:
            Offset for paging results. Defaults to 0. Applies to licenses, not the additional
            information included when parameter withMetadata == true.
        limit:
            Limit for controlling result size. Defaults to 5. Applies to licenses, not the
            additional information included when parameter withMetadata == true.
        order_by:
            Field name to order results by. Valid fields depend on object tyoe returned by
            call.
        order_asc:
            Flag that controls ordering in ascending vs. descending order. Defaults to false,
            meaning descending order.
    """

    offset: Optional[int] = field(
        init=True, metadata={"transform": "str"}, default=None
    )
    limit: Optional[int] = field(init=True, metadata={"transform": "str"}, default=None)
    order_by: Optional[str] = field(
        init=True, metadata={"api_name": "order-by", "transform": "str"}, default=None
    )
    order_asc: Optional[bool] = field(
        init=True, metadata={"api_name": "order-asc", "transform": "str"}, default=None
    )

Filter and order by fields

See note below for the valid values for the filter_field and order_by attributes.

Note

The fields that can be used to order the results depend on type of objects returned.

Client bindings can be filtered or ordered by one of:

  • cliCountry
  • cliHwArch
  • cliLang
  • cliHostName
  • cliHwId
  • cliInstallationId
  • cliNetworkIpAddress
  • cliOs
  • cliOSUserName
  • cliProcessId
  • cliVersion
  • requestIpAddress
  • validFrom (default)
  • validUntil

Licensees can be filtered or ordered by one of:

  • email
  • displayName
  • name
  • naturalId (default)
  • externalReference

Licenses can be filtered or ordered by one of:

  • productName
  • displayName
  • allowedVersionLowerBound
  • allowedVersionUpperBound
  • allowedVersionsDisplayName
  • qty
  • validFrom (default)
  • validUntil

Core API

The following types are defined in 10Duke core library for Python:

Authentication and Authorization.

Includes helpers for authenticating with Open ID Connect and OAuth and authorization providers for 10Duke API calls.

BearerTokenAuth

Bases: AuthBase

Bearer Token Auth hook - used with OAuth client to connect applications.

Source code in tenduke_core/auth/auth_provider.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class BearerTokenAuth(AuthBase):
    """Bearer Token Auth hook - used with OAuth client to connect applications."""

    def __init__(self, token_callable: Callable[[], str]) -> None:
        """Construct an IdTokenAuth provider (hook).

        Args:
            token_callable: A callable that returns the latest id_token.
        """
        if token_callable is None:
            raise InvalidArgumentError("bearer_token_callable")
        self.token_callable = token_callable

    def __call__(self, r: PreparedRequest):
        """Mutate outgoing request (adding authorization header).

        Args:
            r: Outgoing request.
        """
        r.headers["Authorization"] = f"Bearer {self.token_callable()}"
        return r

    def __eq__(self, other):
        """Return True if instances are equal; otherwise False.

        Equality is tested by retrieving the bearer token for each instance and comparing them.
        """
        return (
            self.token_callable()
            == getattr(other, "token_callable", lambda: None)()
        )

    def __ne__(self, other):
        """Return True if instances are not equal; otherwise False.

        Equality is tested by retrieving the id_token for each instance and comparing them.
        """
        return not self == other

__call__(r)

Mutate outgoing request (adding authorization header).

Parameters:

Name Type Description Default
r PreparedRequest

Outgoing request.

required
Source code in tenduke_core/auth/auth_provider.py
27
28
29
30
31
32
33
34
def __call__(self, r: PreparedRequest):
    """Mutate outgoing request (adding authorization header).

    Args:
        r: Outgoing request.
    """
    r.headers["Authorization"] = f"Bearer {self.token_callable()}"
    return r

__eq__(other)

Return True if instances are equal; otherwise False.

Equality is tested by retrieving the bearer token for each instance and comparing them.

Source code in tenduke_core/auth/auth_provider.py
36
37
38
39
40
41
42
43
44
def __eq__(self, other):
    """Return True if instances are equal; otherwise False.

    Equality is tested by retrieving the bearer token for each instance and comparing them.
    """
    return (
        self.token_callable()
        == getattr(other, "token_callable", lambda: None)()
    )

__init__(token_callable)

Construct an IdTokenAuth provider (hook).

Parameters:

Name Type Description Default
token_callable Callable[[], str]

A callable that returns the latest id_token.

required
Source code in tenduke_core/auth/auth_provider.py
17
18
19
20
21
22
23
24
25
def __init__(self, token_callable: Callable[[], str]) -> None:
    """Construct an IdTokenAuth provider (hook).

    Args:
        token_callable: A callable that returns the latest id_token.
    """
    if token_callable is None:
        raise InvalidArgumentError("bearer_token_callable")
    self.token_callable = token_callable

__ne__(other)

Return True if instances are not equal; otherwise False.

Equality is tested by retrieving the id_token for each instance and comparing them.

Source code in tenduke_core/auth/auth_provider.py
46
47
48
49
50
51
def __ne__(self, other):
    """Return True if instances are not equal; otherwise False.

    Equality is tested by retrieving the id_token for each instance and comparing them.
    """
    return not self == other

DeviceAuthorizationResponse dataclass

Data from Device Authorization Response.

Callers should use this data to initiate the user interaction phase of the Device Authorization Grant flow, before or simultaneously with starting polling for the token.

See https://www.rfc-editor.org/rfc/rfc8628#section-3.2.

Attributes:

Name Type Description
user_code str

The end-user verification code.

uri str

The end-user verification URI on the authorization server. The URI should be short and easy to remember as end users will be asked to manually type it into their user agent.

uri_complete Optional[str]

A verification URI that includes the "user_code" (or other information with the same function as the "user_code"), which is designed for non-textual transmission.

Source code in tenduke_core/auth/device_auth_response.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@dataclass
class DeviceAuthorizationResponse:
    """
    Data from Device Authorization Response.

    Callers should use this data to initiate the user interaction phase
    of the Device Authorization Grant flow, before or simultaneously with
    starting polling for the token.

    See https://www.rfc-editor.org/rfc/rfc8628#section-3.2.

    Attributes:
        user_code: The end-user verification code.
        uri:
            The end-user verification URI on the authorization server. The URI should be short and
            easy to remember as end users will be asked to manually type it into their user agent.
        uri_complete:
            A verification URI that includes the "user_code" (or other information with the same
            function as the "user_code"), which is designed for non-textual transmission.
    """

    user_code: str
    uri: str
    uri_complete: Optional[str]

DeviceFlowClient

Bases: OAuthClient

Helper class to perform OAuth device flow.

Source code in tenduke_core/auth/device_flow_client.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
class DeviceFlowClient(OAuthClient):
    """Helper class to perform OAuth device flow."""

    class VerificationUri:
        """Verification URI/URL handling.

        As some implementations of device flow use URI and some use URL in the parameter name,
        we need to check for both.
        The logic in making the requests is that we should use the '_complete' variant of the URI
        if it is present/populated and use the incomplete version if it is not.
        """

        def __init__(self):
            """Construct a VerificationUri instance."""
            self._verification_uri_complete = ""
            self._verification_url_complete = ""
            self._verification_uri = ""
            self._verification_url = ""

        def read_from_dict(self, data: Dict[str, str]):
            """Read the verification uri details from a dictionary.

            RFC8628 specifies verification_uri. At least one identity provider in the
            wild sends verification_url.
            Outside of this class, we don't need to worry about verification_url, this
            class will read whichever is present.

            Args:
                data: The contents of the Device Authorization Response.
            """
            self._verification_uri = data.get("verification_uri", "")
            self._verification_url = data.get("verification_url", "")
            self._verification_uri_complete = data.get("verification_uri_complete", "")
            self._verification_url_complete = data.get("verification_url_complete", "")

        def uri(self) -> str:
            """Return the token poll uri."""
            return self._verification_uri or self._verification_url

        def uri_complete(self) -> str:
            """Return the token poll complete uri."""
            return (
                self._verification_uri_complete
                or self._verification_url_complete
                or self._verification_uri
                or self._verification_url
            )

    def __init__(self, config: TendukeConfig, session_factory: SessionFactory):
        """Construct an instance of the DeviceFlowClient.

        Args:
            config:
                Configuration parameters for interacting with the OAuth / Open ID Authorization
                Server.
            session_factory:
                Used to create requests Session configured with the settings from config and with
                the configured User-Agent header value.
        """
        self._device_code = None
        self._expires_at = None
        self._user_code: str = ""
        self._interval = 5
        self._verification_uri = self.VerificationUri()
        super().__init__(config, session_factory)

    def authorize(self) -> DeviceAuthorizationResponse:
        """Make the authorization request and return the details to display to the user.

        Returns:
            A DeviceAuthorizationResponse object containing the code and uri needed to fetch a
            token.

        Raises:
            ValueError: Required configuration item idp_oauth_device_code_url was missing.
            OAuth2Error: Device Authorization Request was unsuccessful.
        """
        params = _device_authorization_request_parameters(
            self.config.idp_oauth_client_id,
            self.config.idp_oauth_client_secret,
            self.config.idp_oauth_scope,
        )
        url = self.config.idp_oauth_device_code_url
        if not url:
            raise DeviceCodeAuthorizationUrlMissingError()

        response = self.session.post(url, params)
        if not response.ok:
            raise_error_from_error_response(response)
        response_json = response.json()
        self._parse_authorize_response(response_json)
        return DeviceAuthorizationResponse(
            self._user_code,
            self._verification_uri.uri(),
            self._verification_uri.uri_complete(),
        )

    def _parse_authorize_response(self, data):
        self._device_code = data.get("device_code")
        if "expires_in" in data:
            seconds = data["expires_in"]
            self._expires_at = datetime.now(timezone.utc) + timedelta(seconds=seconds)
        self._user_code = data.get("user_code", "")
        self._interval = data.get("interval") or 5
        self._verification_uri.read_from_dict(data)

    async def poll_for_token_async(self):
        """Poll for an OAuth device flow token.

        The method waits for the specified interval between attempts.
        This method is asynchronous (non-blocking).

        Raises:
            OAuth2Error: Device Access Token Request was unsuccessful.
        """
        poll = True
        while poll:
            response = self._make_poll_request()
            if response.ok:
                poll = False
                token = TokenResponse.from_api(response.json())
                self.token = token
                return token

            poll = await self._handle_error_token_response_async(response)

    def poll_for_token(self):
        """Poll for an OAuth device flow token.

        The method waits for the specified interval between attempts.
        This method is synchronous (blocking).

        Raises:
            OAuth2Error: Device Access Token Request was unsuccessful.
        """
        return asyncio.run(self.poll_for_token_async())

    def _make_poll_request(self):
        params = _poll_params(
            self.config.idp_oauth_client_id,
            self._device_code,
            self.config.idp_oauth_client_secret,
        )
        url = self.config.idp_oauth_token_url
        if not url:
            raise TokenUrlMissingError()
        return self.session.post(url, data=params)

    async def _handle_error_token_response_async(self, response):
        json = response.json()
        error = json.get("error")
        error_code = json.get("error_code")

        continue_codes = ["authorization_pending", "slow_down"]
        if error in continue_codes or error_code in continue_codes:
            if "slow_down" in (error, error_code):
                self._interval += 5
            await asyncio.sleep(self._interval)
            # signal loop in caller to keep polling
            return True

        return raise_error_from_error_response(response)

VerificationUri

Verification URI/URL handling.

As some implementations of device flow use URI and some use URL in the parameter name, we need to check for both. The logic in making the requests is that we should use the '_complete' variant of the URI if it is present/populated and use the incomplete version if it is not.

Source code in tenduke_core/auth/device_flow_client.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
class VerificationUri:
    """Verification URI/URL handling.

    As some implementations of device flow use URI and some use URL in the parameter name,
    we need to check for both.
    The logic in making the requests is that we should use the '_complete' variant of the URI
    if it is present/populated and use the incomplete version if it is not.
    """

    def __init__(self):
        """Construct a VerificationUri instance."""
        self._verification_uri_complete = ""
        self._verification_url_complete = ""
        self._verification_uri = ""
        self._verification_url = ""

    def read_from_dict(self, data: Dict[str, str]):
        """Read the verification uri details from a dictionary.

        RFC8628 specifies verification_uri. At least one identity provider in the
        wild sends verification_url.
        Outside of this class, we don't need to worry about verification_url, this
        class will read whichever is present.

        Args:
            data: The contents of the Device Authorization Response.
        """
        self._verification_uri = data.get("verification_uri", "")
        self._verification_url = data.get("verification_url", "")
        self._verification_uri_complete = data.get("verification_uri_complete", "")
        self._verification_url_complete = data.get("verification_url_complete", "")

    def uri(self) -> str:
        """Return the token poll uri."""
        return self._verification_uri or self._verification_url

    def uri_complete(self) -> str:
        """Return the token poll complete uri."""
        return (
            self._verification_uri_complete
            or self._verification_url_complete
            or self._verification_uri
            or self._verification_url
        )

__init__()

Construct a VerificationUri instance.

Source code in tenduke_core/auth/device_flow_client.py
54
55
56
57
58
59
def __init__(self):
    """Construct a VerificationUri instance."""
    self._verification_uri_complete = ""
    self._verification_url_complete = ""
    self._verification_uri = ""
    self._verification_url = ""

read_from_dict(data)

Read the verification uri details from a dictionary.

RFC8628 specifies verification_uri. At least one identity provider in the wild sends verification_url. Outside of this class, we don't need to worry about verification_url, this class will read whichever is present.

Parameters:

Name Type Description Default
data Dict[str, str]

The contents of the Device Authorization Response.

required
Source code in tenduke_core/auth/device_flow_client.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def read_from_dict(self, data: Dict[str, str]):
    """Read the verification uri details from a dictionary.

    RFC8628 specifies verification_uri. At least one identity provider in the
    wild sends verification_url.
    Outside of this class, we don't need to worry about verification_url, this
    class will read whichever is present.

    Args:
        data: The contents of the Device Authorization Response.
    """
    self._verification_uri = data.get("verification_uri", "")
    self._verification_url = data.get("verification_url", "")
    self._verification_uri_complete = data.get("verification_uri_complete", "")
    self._verification_url_complete = data.get("verification_url_complete", "")

uri()

Return the token poll uri.

Source code in tenduke_core/auth/device_flow_client.py
77
78
79
def uri(self) -> str:
    """Return the token poll uri."""
    return self._verification_uri or self._verification_url

uri_complete()

Return the token poll complete uri.

Source code in tenduke_core/auth/device_flow_client.py
81
82
83
84
85
86
87
88
def uri_complete(self) -> str:
    """Return the token poll complete uri."""
    return (
        self._verification_uri_complete
        or self._verification_url_complete
        or self._verification_uri
        or self._verification_url
    )

__init__(config, session_factory)

Construct an instance of the DeviceFlowClient.

Parameters:

Name Type Description Default
config TendukeConfig

Configuration parameters for interacting with the OAuth / Open ID Authorization Server.

required
session_factory SessionFactory

Used to create requests Session configured with the settings from config and with the configured User-Agent header value.

required
Source code in tenduke_core/auth/device_flow_client.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def __init__(self, config: TendukeConfig, session_factory: SessionFactory):
    """Construct an instance of the DeviceFlowClient.

    Args:
        config:
            Configuration parameters for interacting with the OAuth / Open ID Authorization
            Server.
        session_factory:
            Used to create requests Session configured with the settings from config and with
            the configured User-Agent header value.
    """
    self._device_code = None
    self._expires_at = None
    self._user_code: str = ""
    self._interval = 5
    self._verification_uri = self.VerificationUri()
    super().__init__(config, session_factory)

authorize()

Make the authorization request and return the details to display to the user.

Returns:

Type Description
DeviceAuthorizationResponse

A DeviceAuthorizationResponse object containing the code and uri needed to fetch a

DeviceAuthorizationResponse

token.

Raises:

Type Description
ValueError

Required configuration item idp_oauth_device_code_url was missing.

OAuth2Error

Device Authorization Request was unsuccessful.

Source code in tenduke_core/auth/device_flow_client.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def authorize(self) -> DeviceAuthorizationResponse:
    """Make the authorization request and return the details to display to the user.

    Returns:
        A DeviceAuthorizationResponse object containing the code and uri needed to fetch a
        token.

    Raises:
        ValueError: Required configuration item idp_oauth_device_code_url was missing.
        OAuth2Error: Device Authorization Request was unsuccessful.
    """
    params = _device_authorization_request_parameters(
        self.config.idp_oauth_client_id,
        self.config.idp_oauth_client_secret,
        self.config.idp_oauth_scope,
    )
    url = self.config.idp_oauth_device_code_url
    if not url:
        raise DeviceCodeAuthorizationUrlMissingError()

    response = self.session.post(url, params)
    if not response.ok:
        raise_error_from_error_response(response)
    response_json = response.json()
    self._parse_authorize_response(response_json)
    return DeviceAuthorizationResponse(
        self._user_code,
        self._verification_uri.uri(),
        self._verification_uri.uri_complete(),
    )

poll_for_token()

Poll for an OAuth device flow token.

The method waits for the specified interval between attempts. This method is synchronous (blocking).

Raises:

Type Description
OAuth2Error

Device Access Token Request was unsuccessful.

Source code in tenduke_core/auth/device_flow_client.py
168
169
170
171
172
173
174
175
176
177
def poll_for_token(self):
    """Poll for an OAuth device flow token.

    The method waits for the specified interval between attempts.
    This method is synchronous (blocking).

    Raises:
        OAuth2Error: Device Access Token Request was unsuccessful.
    """
    return asyncio.run(self.poll_for_token_async())

poll_for_token_async() async

Poll for an OAuth device flow token.

The method waits for the specified interval between attempts. This method is asynchronous (non-blocking).

Raises:

Type Description
OAuth2Error

Device Access Token Request was unsuccessful.

Source code in tenduke_core/auth/device_flow_client.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
async def poll_for_token_async(self):
    """Poll for an OAuth device flow token.

    The method waits for the specified interval between attempts.
    This method is asynchronous (non-blocking).

    Raises:
        OAuth2Error: Device Access Token Request was unsuccessful.
    """
    poll = True
    while poll:
        response = self._make_poll_request()
        if response.ok:
            poll = False
            token = TokenResponse.from_api(response.json())
            self.token = token
            return token

        poll = await self._handle_error_token_response_async(response)

IdTokenAuth

Bases: AuthBase

ID Token Auth hook - used with Open ID Connect in public applications.

Source code in tenduke_core/auth/auth_provider.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class IdTokenAuth(AuthBase):
    """ID Token Auth hook - used with Open ID Connect in public applications."""

    def __init__(self, id_token_callable: Callable[[], str]) -> None:
        """Construct an IdTokenAuth provider (hook).

        Args:
            id_token_callable: A callable that returns the latest id_token.
        """
        if id_token_callable is None:
            raise InvalidArgumentError("id_token_callable")
        self.id_token_callable = id_token_callable

    def __call__(self, r: PreparedRequest):
        """Mutate outgoing request (adding authorization header).

        Args:
            r: Outgoing request.
        """
        r.headers["Authorization"] = f"IdToken {self.id_token_callable()}"
        return r

    def __eq__(self, other):
        """Return True if instances are equal; otherwise False.

        Equality is tested by retrieving the id_token for each instance and comparing them.
        """
        return (
            self.id_token_callable()
            == getattr(other, "id_token_callable", lambda: None)()
        )

    def __ne__(self, other):
        """Return True if instances are not equal; otherwise False.

        Equality is tested by retrieving the id_token for each instance and comparing them.
        """
        return not self == other

__call__(r)

Mutate outgoing request (adding authorization header).

Parameters:

Name Type Description Default
r PreparedRequest

Outgoing request.

required
Source code in tenduke_core/auth/auth_provider.py
67
68
69
70
71
72
73
74
def __call__(self, r: PreparedRequest):
    """Mutate outgoing request (adding authorization header).

    Args:
        r: Outgoing request.
    """
    r.headers["Authorization"] = f"IdToken {self.id_token_callable()}"
    return r

__eq__(other)

Return True if instances are equal; otherwise False.

Equality is tested by retrieving the id_token for each instance and comparing them.

Source code in tenduke_core/auth/auth_provider.py
76
77
78
79
80
81
82
83
84
def __eq__(self, other):
    """Return True if instances are equal; otherwise False.

    Equality is tested by retrieving the id_token for each instance and comparing them.
    """
    return (
        self.id_token_callable()
        == getattr(other, "id_token_callable", lambda: None)()
    )

__init__(id_token_callable)

Construct an IdTokenAuth provider (hook).

Parameters:

Name Type Description Default
id_token_callable Callable[[], str]

A callable that returns the latest id_token.

required
Source code in tenduke_core/auth/auth_provider.py
57
58
59
60
61
62
63
64
65
def __init__(self, id_token_callable: Callable[[], str]) -> None:
    """Construct an IdTokenAuth provider (hook).

    Args:
        id_token_callable: A callable that returns the latest id_token.
    """
    if id_token_callable is None:
        raise InvalidArgumentError("id_token_callable")
    self.id_token_callable = id_token_callable

__ne__(other)

Return True if instances are not equal; otherwise False.

Equality is tested by retrieving the id_token for each instance and comparing them.

Source code in tenduke_core/auth/auth_provider.py
86
87
88
89
90
91
def __ne__(self, other):
    """Return True if instances are not equal; otherwise False.

    Equality is tested by retrieving the id_token for each instance and comparing them.
    """
    return not self == other

OAuthClient

OAuth / Open ID Connect client.

Source code in tenduke_core/auth/oauth_client.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
class OAuthClient:
    """OAuth / Open ID Connect client."""

    def __init__(self, config: TendukeConfig, session_factory: SessionFactory):
        """
        Construct an instance of the OAuth Client.

        Args:
            config:
                Configuration parameters for interacting with the OAuth / Open ID Authorization
                Server.
            session_factory:
                Used to create requests Session configured with the settings from config and with
                the configured User-Agent header value.
        """
        self.config = config
        self._idp_jwks_client = None
        if config.idp_jwks_uri:
            self._idp_jwks_client = PyJWKClient(
                config.idp_jwks_uri, cache_keys=True, lifespan=345000
            )
        self._try_to_refresh = True
        self._token: Optional[TokenResponse] = None
        self._id_token: Optional[IdToken] = None
        self.session = session_factory.create()

    @property
    def token(self) -> Optional[TokenResponse]:
        """The current token response."""
        return self._token

    @token.setter
    def token(self, value: TokenResponse):
        if value.id_token is not None:
            self._id_token = IdToken.from_token(
                value, self._idp_jwks_client, self.config.idp_oauth_client_id
            )
        self._try_to_refresh = (
            value.refresh_token is not None and value.id_token is not None
        )
        self._token = value

    @property
    def id_token(self) -> Optional[str]:
        """The current identity token (if any)."""
        if not self._token:
            return None
        self._refresh_token()
        return self._id_token.token if self._id_token else None

    def get_user_info(self) -> UserInfo:
        """Retrieve the details of the user from the userinfo endpoint.

        Returns:
            The information for the authenticated user.

        Raises:
            ValueError: User info URL missing from configuration.
            OAuth2Error: The user info request was unsuccessful.
        """
        headers = (
            {"Authorization": f"Bearer {self._token.access_token}"}
            if self._token
            else {}
        )
        if not self.config.idp_userinfo_url:
            raise UserInfoUrlMissingError()

        response = self.session.get(self.config.idp_userinfo_url, headers=headers)
        if not response.ok:
            raise_error_from_error_response(response)

        info = UserInfo.from_api(response.json())
        return info

    def auth(self) -> IdTokenAuth:
        """Get an authorization implementation based on the current identity.

        Returns:
            An authorization provider hook that sets the authorization header of HTTP requests
            using the current id_token.
        """
        if not self.id_token:
            raise IdTokenMissingError()

        return IdTokenAuth(lambda: self.id_token or "")

    def _can_refresh(self):
        return (
            self._try_to_refresh
            and self._token is not None
            and self._token.refresh_token is not None
            and self.config.idp_oauth_token_url is not None
        )

    def _refresh_token(self):
        # if we know the expiry of the id_token, check it is still valid
        # if it is close to expiry and with have a token url, see if we can get a new one
        is_expired = _is_expired(
            self._id_token, self.config.token_refresh_leeway_seconds
        )
        can_refesh = self._can_refresh()

        # type checking disabled for members already checked for None in
        #  _is_expired and _can_refresh
        if is_expired and can_refesh:
            data = {
                "grant_type": "refresh_token",
                "refresh_token": self._token.refresh_token,  # pyright: ignore[reportOptionalMemberAccess]
                "client_id": self.config.idp_oauth_client_id,
            }
            if self.config.idp_oauth_client_secret:
                data["client_secret"] = self.config.idp_oauth_client_secret
            response = self.session.post(self.config.idp_oauth_token_url, data=data)  # pyright: ignore[reportArgumentType]
            if response.ok:
                token = TokenResponse.from_api(response.json())
                self.token = token

id_token: Optional[str] property

The current identity token (if any).

token: Optional[TokenResponse] property writable

The current token response.

__init__(config, session_factory)

Construct an instance of the OAuth Client.

Parameters:

Name Type Description Default
config TendukeConfig

Configuration parameters for interacting with the OAuth / Open ID Authorization Server.

required
session_factory SessionFactory

Used to create requests Session configured with the settings from config and with the configured User-Agent header value.

required
Source code in tenduke_core/auth/oauth_client.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def __init__(self, config: TendukeConfig, session_factory: SessionFactory):
    """
    Construct an instance of the OAuth Client.

    Args:
        config:
            Configuration parameters for interacting with the OAuth / Open ID Authorization
            Server.
        session_factory:
            Used to create requests Session configured with the settings from config and with
            the configured User-Agent header value.
    """
    self.config = config
    self._idp_jwks_client = None
    if config.idp_jwks_uri:
        self._idp_jwks_client = PyJWKClient(
            config.idp_jwks_uri, cache_keys=True, lifespan=345000
        )
    self._try_to_refresh = True
    self._token: Optional[TokenResponse] = None
    self._id_token: Optional[IdToken] = None
    self.session = session_factory.create()

auth()

Get an authorization implementation based on the current identity.

Returns:

Type Description
IdTokenAuth

An authorization provider hook that sets the authorization header of HTTP requests

IdTokenAuth

using the current id_token.

Source code in tenduke_core/auth/oauth_client.py
156
157
158
159
160
161
162
163
164
165
166
def auth(self) -> IdTokenAuth:
    """Get an authorization implementation based on the current identity.

    Returns:
        An authorization provider hook that sets the authorization header of HTTP requests
        using the current id_token.
    """
    if not self.id_token:
        raise IdTokenMissingError()

    return IdTokenAuth(lambda: self.id_token or "")

get_user_info()

Retrieve the details of the user from the userinfo endpoint.

Returns:

Type Description
UserInfo

The information for the authenticated user.

Raises:

Type Description
ValueError

User info URL missing from configuration.

OAuth2Error

The user info request was unsuccessful.

Source code in tenduke_core/auth/oauth_client.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def get_user_info(self) -> UserInfo:
    """Retrieve the details of the user from the userinfo endpoint.

    Returns:
        The information for the authenticated user.

    Raises:
        ValueError: User info URL missing from configuration.
        OAuth2Error: The user info request was unsuccessful.
    """
    headers = (
        {"Authorization": f"Bearer {self._token.access_token}"}
        if self._token
        else {}
    )
    if not self.config.idp_userinfo_url:
        raise UserInfoUrlMissingError()

    response = self.session.get(self.config.idp_userinfo_url, headers=headers)
    if not response.ok:
        raise_error_from_error_response(response)

    info = UserInfo.from_api(response.json())
    return info

PkceFlowClient

Bases: OAuthClient

Client for Proof Key for Code Exchange (PKCE) flow.

Source code in tenduke_core/auth/pkce_flow_client.py
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
class PkceFlowClient(OAuthClient):
    """Client for Proof Key for Code Exchange (PKCE) flow."""

    class RedirectHttpServer(HTTPServer):
        """HTTP Server to handle redirect with code."""

        def __init__(
            self,
            *args,
            success_http:Optional[str]=AUTH_SUCCESS_MESSAGE,
            **kwargs,
        ):
            """Construct an instance of the HTTP Server.

            Args:
                success_http: HTTP message to send once redirect request is received.
            """
            self.success_http = success_http
            self.authorization_response = ""
            super().__init__(*args, **kwargs)

    class RedirectHandler(BaseHTTPRequestHandler):
        """Handler class for redirect with code."""

        def do_GET(self):  # pylint: disable=invalid-name
            """Handle OAuth redirect."""
            self.wfile.write(self.server.success_http.encode("UTF-8"))  # pyright:ignore[reportAttributeAccessIssue]
            self.wfile.flush()
            self.server.authorization_response = self.path  # pyright:ignore[reportAttributeAccessIssue]

    def __init__(self, config: TendukeConfig, session_factory: SessionFactory, *args, **kwargs):
        """Construct an instance of the PkceFlowClient.

        Args:
            config:
                Configuration parameters for interacting with the OAuth / Open ID Authorization
                Server.
            session_factory:
                Used to create requests Session configured with the settings from config and with
                the configured User-Agent header value.
        """
        self.code_verifier = kwargs.pop("code_verifier", None)
        self.state = kwargs.pop("state", None)
        self._client = None
        super().__init__(config, session_factory)

    def _build_redirect_uri(self, port: Optional[int] = None):
        port = port or self.config.auth_redirect_port
        redirect_uri = self.config.auth_redirect_uri
        parsed_result = urlparse(redirect_uri)
        if parsed_result.hostname in (
            "localhost",
            "127.0.0.1",
            "::1",
        ) and _is_default_port(parsed_result):
            # ParsedResult.host_name returns the hostname only, we need to escape as per RFC2732
            host_name = (
                "[::1]" if parsed_result.hostname == "::1" else parsed_result.hostname
            )
            # urllib.parse is more flexible with types than mypy gives it credit for
            new_uri = f"{host_name}:{port}"  # type: ignore[str-bytes-safe]
            updated = parsed_result._replace(netloc=new_uri)  # type: ignore[arg-type]
            redirect_uri = urlunparse(updated)  # type: ignore[assignment]
        if not redirect_uri:
            port = port or self.config.auth_redirect_port
            redirect_uri = urljoin(
                f"http://localhost:{port}", self.config.auth_redirect_path
            )
        return redirect_uri

    def _resolve_port(self):
        redirect_uri = self.config.auth_redirect_uri
        parsed_result = urlparse(redirect_uri)
        if parsed_result.port is not None and parsed_result.port not in (0, 80, 443):
            return parsed_result.port
        return self.config.auth_redirect_port

    def create_authorization_url(self, port: Optional[int] = None) -> str:
        """Generate and return authorization url.

        Args:
            port: The port number to use in the redirect URI

        Returns:
            URL for authorization request.
        """
        self.code_verifier = generate_token(128)

        redirect_uri = self._build_redirect_uri(port)

        self._client = OAuth2Session(
            client_id=self.config.idp_oauth_client_id,
            scope=self.config.idp_oauth_scope,
            redirect_uri=redirect_uri,
            code_challenge_method="S256",
        )
        url, state = self._client.create_authorization_url(  # type: ignore[attr-defined]
            self.config.idp_oauth_authorization_url, code_verifier=self.code_verifier
        )
        self.state = state
        return url

    def fetch_token(
        self, authorization_response: Optional[str], port: Optional[int] = None
    ) -> TokenResponse:
        """Fetch token based on authorization response.

        Args:
            authorization_response:
                Redirect URL request with authorization code to exchange for token.

        Returns:
            TokenResponse object for the authorization code in the authorization_reponse parameter.
        """
        redirect_uri = self._build_redirect_uri(port)

        if self._client is None:
            self._client = OAuth2Session(
                client_id=self.config.idp_oauth_client_id,
                scope=self.config.idp_oauth_scope,
                redirect_uri=redirect_uri,
                code_challenge_method="S256",
            )
        token = self._client.fetch_token(  # type: ignore[attr-defined]
            self.config.idp_oauth_token_url,
            authorization_response=authorization_response,
            state=self.state,
            code_verifier=self.code_verifier,
        )
        token = TokenResponse.from_api(token)
        self.token = token
        return token

    def login_with_default_browser(self) -> TokenResponse:
        """Launch system default browser for user to log in.

        Returns:
            TokenResponse object with token for the authenticated user.

        Raises:
            OAuth2Error: Authentication failed.
        """
        authorization_response = None
        success_http = AUTH_SUCCESS_MESSAGE

        if self.config.auth_success_message:
            success_http = Path(self.config.auth_success_message).read_text("UTF-8")

        port = self._resolve_port()

        with self.RedirectHttpServer(
            ("", port),
            self.RedirectHandler,
            success_http=success_http,
        ) as httpd:
            url = self.create_authorization_url(httpd.server_port)
            webbrowser.open(url)
            httpd.handle_request()
            authorization_response = httpd.authorization_response

        if authorization_response:
            return self.fetch_token(authorization_response, httpd.server_port)

        raise AuthenticationFailed()

RedirectHandler

Bases: BaseHTTPRequestHandler

Handler class for redirect with code.

Source code in tenduke_core/auth/pkce_flow_client.py
66
67
68
69
70
71
72
73
class RedirectHandler(BaseHTTPRequestHandler):
    """Handler class for redirect with code."""

    def do_GET(self):  # pylint: disable=invalid-name
        """Handle OAuth redirect."""
        self.wfile.write(self.server.success_http.encode("UTF-8"))  # pyright:ignore[reportAttributeAccessIssue]
        self.wfile.flush()
        self.server.authorization_response = self.path  # pyright:ignore[reportAttributeAccessIssue]

do_GET()

Handle OAuth redirect.

Source code in tenduke_core/auth/pkce_flow_client.py
69
70
71
72
73
def do_GET(self):  # pylint: disable=invalid-name
    """Handle OAuth redirect."""
    self.wfile.write(self.server.success_http.encode("UTF-8"))  # pyright:ignore[reportAttributeAccessIssue]
    self.wfile.flush()
    self.server.authorization_response = self.path  # pyright:ignore[reportAttributeAccessIssue]

RedirectHttpServer

Bases: HTTPServer

HTTP Server to handle redirect with code.

Source code in tenduke_core/auth/pkce_flow_client.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class RedirectHttpServer(HTTPServer):
    """HTTP Server to handle redirect with code."""

    def __init__(
        self,
        *args,
        success_http:Optional[str]=AUTH_SUCCESS_MESSAGE,
        **kwargs,
    ):
        """Construct an instance of the HTTP Server.

        Args:
            success_http: HTTP message to send once redirect request is received.
        """
        self.success_http = success_http
        self.authorization_response = ""
        super().__init__(*args, **kwargs)

__init__(*args, success_http=AUTH_SUCCESS_MESSAGE, **kwargs)

Construct an instance of the HTTP Server.

Parameters:

Name Type Description Default
success_http Optional[str]

HTTP message to send once redirect request is received.

AUTH_SUCCESS_MESSAGE
Source code in tenduke_core/auth/pkce_flow_client.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def __init__(
    self,
    *args,
    success_http:Optional[str]=AUTH_SUCCESS_MESSAGE,
    **kwargs,
):
    """Construct an instance of the HTTP Server.

    Args:
        success_http: HTTP message to send once redirect request is received.
    """
    self.success_http = success_http
    self.authorization_response = ""
    super().__init__(*args, **kwargs)

__init__(config, session_factory, *args, **kwargs)

Construct an instance of the PkceFlowClient.

Parameters:

Name Type Description Default
config TendukeConfig

Configuration parameters for interacting with the OAuth / Open ID Authorization Server.

required
session_factory SessionFactory

Used to create requests Session configured with the settings from config and with the configured User-Agent header value.

required
Source code in tenduke_core/auth/pkce_flow_client.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def __init__(self, config: TendukeConfig, session_factory: SessionFactory, *args, **kwargs):
    """Construct an instance of the PkceFlowClient.

    Args:
        config:
            Configuration parameters for interacting with the OAuth / Open ID Authorization
            Server.
        session_factory:
            Used to create requests Session configured with the settings from config and with
            the configured User-Agent header value.
    """
    self.code_verifier = kwargs.pop("code_verifier", None)
    self.state = kwargs.pop("state", None)
    self._client = None
    super().__init__(config, session_factory)

create_authorization_url(port=None)

Generate and return authorization url.

Parameters:

Name Type Description Default
port Optional[int]

The port number to use in the redirect URI

None

Returns:

Type Description
str

URL for authorization request.

Source code in tenduke_core/auth/pkce_flow_client.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def create_authorization_url(self, port: Optional[int] = None) -> str:
    """Generate and return authorization url.

    Args:
        port: The port number to use in the redirect URI

    Returns:
        URL for authorization request.
    """
    self.code_verifier = generate_token(128)

    redirect_uri = self._build_redirect_uri(port)

    self._client = OAuth2Session(
        client_id=self.config.idp_oauth_client_id,
        scope=self.config.idp_oauth_scope,
        redirect_uri=redirect_uri,
        code_challenge_method="S256",
    )
    url, state = self._client.create_authorization_url(  # type: ignore[attr-defined]
        self.config.idp_oauth_authorization_url, code_verifier=self.code_verifier
    )
    self.state = state
    return url

fetch_token(authorization_response, port=None)

Fetch token based on authorization response.

Parameters:

Name Type Description Default
authorization_response Optional[str]

Redirect URL request with authorization code to exchange for token.

required

Returns:

Type Description
TokenResponse

TokenResponse object for the authorization code in the authorization_reponse parameter.

Source code in tenduke_core/auth/pkce_flow_client.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def fetch_token(
    self, authorization_response: Optional[str], port: Optional[int] = None
) -> TokenResponse:
    """Fetch token based on authorization response.

    Args:
        authorization_response:
            Redirect URL request with authorization code to exchange for token.

    Returns:
        TokenResponse object for the authorization code in the authorization_reponse parameter.
    """
    redirect_uri = self._build_redirect_uri(port)

    if self._client is None:
        self._client = OAuth2Session(
            client_id=self.config.idp_oauth_client_id,
            scope=self.config.idp_oauth_scope,
            redirect_uri=redirect_uri,
            code_challenge_method="S256",
        )
    token = self._client.fetch_token(  # type: ignore[attr-defined]
        self.config.idp_oauth_token_url,
        authorization_response=authorization_response,
        state=self.state,
        code_verifier=self.code_verifier,
    )
    token = TokenResponse.from_api(token)
    self.token = token
    return token

login_with_default_browser()

Launch system default browser for user to log in.

Returns:

Type Description
TokenResponse

TokenResponse object with token for the authenticated user.

Raises:

Type Description
OAuth2Error

Authentication failed.

Source code in tenduke_core/auth/pkce_flow_client.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def login_with_default_browser(self) -> TokenResponse:
    """Launch system default browser for user to log in.

    Returns:
        TokenResponse object with token for the authenticated user.

    Raises:
        OAuth2Error: Authentication failed.
    """
    authorization_response = None
    success_http = AUTH_SUCCESS_MESSAGE

    if self.config.auth_success_message:
        success_http = Path(self.config.auth_success_message).read_text("UTF-8")

    port = self._resolve_port()

    with self.RedirectHttpServer(
        ("", port),
        self.RedirectHandler,
        success_http=success_http,
    ) as httpd:
        url = self.create_authorization_url(httpd.server_port)
        webbrowser.open(url)
        httpd.handle_request()
        authorization_response = httpd.authorization_response

    if authorization_response:
        return self.fetch_token(authorization_response, httpd.server_port)

    raise AuthenticationFailed()

TokenResponse dataclass

Bases: Model

Token response data model.

https://www.rfc-editor.org/rfc/rfc6749#section-4.2.2

Attributes:

Name Type Description
access_token str

The access token issued by the authorization server.

token_type str

The type of the token issued.

expires_in int

The lifetime in seconds of the access token.

expires_at datetime

The datetime when the token will expire Derived from expires_in and the current system time when the token was received.

refresh_token Optional[str]

The refresh token, which can be used to obtain new access tokens.

id_token Optional[str]

ID Token value associated with the authenticated session.

Source code in tenduke_core/auth/token_response.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@dataclass
class TokenResponse(Model):
    """Token response data model.

    https://www.rfc-editor.org/rfc/rfc6749#section-4.2.2

    Attributes:
        access_token: The access token issued by the authorization server.
        token_type: The type of the token issued.
        expires_in: The lifetime in seconds of the access token.
        expires_at:
            The datetime when the token will expire Derived from expires_in and the current system
            time when the token was received.
        refresh_token: The refresh token, which can be used to obtain new access tokens.
        id_token: ID Token value associated with the authenticated session.
    """

    access_token: str
    token_type: str
    expires_in: int
    expires_at: datetime = field(init=False)
    refresh_token: Optional[str] = None
    # id_token is not mandatory in the RFC 6749 Access Token Response
    # but Open ID Connect id_token is required for ID Token auth
    id_token: Optional[str] = None

    def __post_init__(self):
        """Initialize expires_at based on expires_in and current time."""
        self.expires_at = datetime.now(timezone.utc) + timedelta(
            seconds=self.expires_in
        )

__post_init__()

Initialize expires_at based on expires_in and current time.

Source code in tenduke_core/auth/token_response.py
39
40
41
42
43
def __post_init__(self):
    """Initialize expires_at based on expires_in and current time."""
    self.expires_at = datetime.now(timezone.utc) + timedelta(
        seconds=self.expires_in
    )

UserInfo dataclass

Bases: Model

User info response data model.

Attributes:

Name Type Description
sub str

Subject identitfier. A locally unique and never reassigned identifier within the issuer for the End-User.

name Optional[str]

End-User's full name in displayable form including all name parts, possibly including titles and suffixes, ordered according to the End-User's locale and preferences.

given_name Optional[str]

Given name(s) or first name(s) of the End-User. Note that in some cultures, people can have multiple given names; all can be present, with the names being separated by space characters.

family_name Optional[str]

Surname(s) or last name(s) of the End-User. Note that in some cultures, people can have multiple family names or no family name; all can be present, with the names being separated by space characters.

email Optional[str]

End-User's preferred e-mail address.

formatted_name Optional[str]

Combination of names for display purposes. Shall contain a value if any identifier is populated.

Source code in tenduke_core/auth/user_info.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@dataclass
class UserInfo(Model):
    """User info response data model.

    Attributes:
        sub:
            Subject identitfier. A locally unique and never reassigned identifier within the issuer
            for the End-User.
        name:
            End-User's full name in displayable form including all name parts, possibly including
            titles and suffixes, ordered according to the End-User's locale and preferences.
        given_name:
            Given name(s) or first name(s) of the End-User. Note that in some cultures, people can
            have multiple given names; all can be present, with the names being separated by space
            characters.
        family_name:
            Surname(s) or last name(s) of the End-User. Note that in some cultures, people can have
            multiple family names or no family name; all can be present, with the names being
            separated by space characters.
        email: End-User's preferred e-mail address.
        formatted_name:
            Combination of names for display purposes. Shall contain a value if any identifier is
            populated.
    """

    sub: str
    name: Optional[str] = None
    given_name: Optional[str] = None
    family_name: Optional[str] = None
    email: Optional[str] = None
    formatted_name: Optional[str] = field(init=False)

    def __post_init__(self):
        """Initialize formatted name based on other values."""
        if self.given_name and self.family_name:
            self.formatted_name = f"{self.given_name} {self.family_name}"
        else:
            self.formatted_name = self.name or self.email or self.sub

__post_init__()

Initialize formatted name based on other values.

Source code in tenduke_core/auth/user_info.py
41
42
43
44
45
46
def __post_init__(self):
    """Initialize formatted name based on other values."""
    if self.given_name and self.family_name:
        self.formatted_name = f"{self.given_name} {self.family_name}"
    else:
        self.formatted_name = self.name or self.email or self.sub

Configuration data for 10Duke API and OAuth interactions.

TendukeConfig dataclass

Configuration properties for interacting with licensing and oauth / open ID connect.

Attributes:

Name Type Description
token_path str

Location on disk to save license tokens.

public_key_path str

Location on disk to save public keys.

licensing_api_url str

Protocol and host name for API tenant.

token_refresh_leeway_seconds float

The number of seconds before expiry time that an ID Token or JWT will be automatically refreshed.

http_timeout_seconds float

Timeout for HTTP requests.

licensing_api_authorization_model Optional[str]

Method of authorization used for API calls.

idp_oidc_discovery_url Optional[str]

Used to retrieve the details of the Open ID Connect endpoints for the identity provider.

idp_oauth_authorization_url Optional[str]

Endpoint for Authorization Request in Authorization Code or Implicit Grant flows.

idp_oauth_device_code_url Optional[str]

Endpoint for Device Authorization Request in Device Authorization Grant flow.

idp_oauth_token_url Optional[str]

Endpoint for Access Token Request or Device Access Token Request.

idp_oauth_client_id Optional[str]

Application credentials for OAuth/Open ID Connect.

idp_userinfo_url Optional[str]

Endpoint handling the UserInfo Request.

idp_oauth_client_secret Optional[str]

Application credentials for OAuth/Open ID Connect. Required for some OAuth flows or for some Identity Providers.

idp_oauth_scope Optional[str]

Scopes to include in the Access and ID tokens requested via Open ID Connect.

idp_jwks_uri Optional[str]

URL path to read public key used to verify JWTs received from Authorization Server authenticating Open ID Connect session.

auth_redirect_uri Optional[str]

Fully specified URL for OAuth redirect_uri to listen for redirect during PKCE Flow.

auth_redirect_path str

Redirect path fragment to listen for redirect during PKCE Flow. For desktop clients, this path will be appended to http://localhost (either on the specified port or a random ephemeral port). This fragment is ignored if auth_redirect_uri is specified.

auth_redirect_port int

Local redirect port to listen on for PKCE Flow Client.

auth_success_message Optional[str]

File containing response for successful login (see PKCE Flow Client).

https_proxy Optional[str]

Proxy to use for HTTPS requests.

Source code in tenduke_core/config/tenduke_config.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
@dataclass
class TendukeConfig:
    """Configuration properties for interacting with licensing and oauth / open ID connect.

    Attributes:
        token_path: Location on disk to save license tokens.
        public_key_path: Location on disk to save public keys.
        licensing_api_url: Protocol and host name for API tenant.
        token_refresh_leeway_seconds:
            The number of seconds before expiry time that an ID Token or JWT will be
            automatically refreshed.
        http_timeout_seconds: Timeout for HTTP requests.
        licensing_api_authorization_model: Method of authorization used for API calls.
        idp_oidc_discovery_url:
            Used to retrieve the details of the Open ID Connect endpoints for the identity
            provider.
        idp_oauth_authorization_url:
            Endpoint for Authorization Request in Authorization Code or Implicit Grant flows.
        idp_oauth_device_code_url:
            Endpoint for Device Authorization Request in Device Authorization Grant flow.
        idp_oauth_token_url: Endpoint for Access Token Request or Device Access Token Request.
        idp_oauth_client_id: Application credentials for OAuth/Open ID Connect.
        idp_userinfo_url: Endpoint handling the UserInfo Request.
        idp_oauth_client_secret:
            Application credentials for OAuth/Open ID Connect. Required for some OAuth flows or for
            some Identity Providers.
        idp_oauth_scope:
            Scopes to include in the Access and ID tokens requested via Open ID Connect.
        idp_jwks_uri:
            URL path to read public key used to verify JWTs received from Authorization Server
            authenticating Open ID Connect session.
        auth_redirect_uri:
            Fully specified URL for OAuth redirect_uri to listen for redirect during PKCE Flow.
        auth_redirect_path:
            Redirect path fragment to listen for redirect during PKCE Flow. For desktop clients,
            this path will be appended to http://localhost (either on the specified port or a random
            ephemeral port). This fragment is ignored if auth_redirect_uri is specified.
        auth_redirect_port: Local redirect port to listen on for PKCE Flow Client.
        auth_success_message: File containing response for successful login (see PKCE Flow Client).
        https_proxy: Proxy to use for HTTPS requests.
    """

    token_path: str
    public_key_path: str
    licensing_api_url: str
    token_refresh_leeway_seconds: float = 30.0
    http_timeout_seconds: float = 30.0
    auth_redirect_path: str = "/login/callback"
    auth_redirect_port: int = 0
    auth_redirect_uri: Optional[str] = None
    licensing_api_authorization_model: Optional[str] = None
    idp_oidc_discovery_url: Optional[str] = None
    idp_oauth_authorization_url: Optional[str] = None
    idp_oauth_device_code_url: Optional[str] = None
    idp_oauth_token_url: Optional[str] = None
    idp_oauth_client_id: Optional[str] = None
    idp_userinfo_url: Optional[str] = None
    idp_oauth_client_secret: Optional[str] = None
    idp_oauth_scope: Optional[str] = None
    idp_jwks_uri: Optional[str] = None
    auth_success_message: Optional[str] = None
    https_proxy: Optional[str] = None

    @classmethod
    def load(
        cls: Type['TendukeConfig'], prefix: Optional[str] = None, file_name: Optional[str] = None, **kwargs
    ) -> 'TendukeConfig':
        """Load the configuration.

        Priority order for configuration values:
        - environment variables
        - configuration file
        - kwargs

        Args:
            prefix: Optionally override default prefix for environment variables.
            file_name: Configuration file to load.
        """
        config_builder = dataconf.multi.dict(kwargs)

        if file_name:
            config_builder = config_builder.file(file_name)

        if prefix:
            config_builder = config_builder.env(prefix)

        config = config_builder.on(TendukeConfig)
        return config  # pyright: ignore[reportReturnType]

load(prefix=None, file_name=None, **kwargs) classmethod

Load the configuration.

Priority order for configuration values: - environment variables - configuration file - kwargs

Parameters:

Name Type Description Default
prefix Optional[str]

Optionally override default prefix for environment variables.

None
file_name Optional[str]

Configuration file to load.

None
Source code in tenduke_core/config/tenduke_config.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
@classmethod
def load(
    cls: Type['TendukeConfig'], prefix: Optional[str] = None, file_name: Optional[str] = None, **kwargs
) -> 'TendukeConfig':
    """Load the configuration.

    Priority order for configuration values:
    - environment variables
    - configuration file
    - kwargs

    Args:
        prefix: Optionally override default prefix for environment variables.
        file_name: Configuration file to load.
    """
    config_builder = dataconf.multi.dict(kwargs)

    if file_name:
        config_builder = config_builder.file(file_name)

    if prefix:
        config_builder = config_builder.env(prefix)

    config = config_builder.on(TendukeConfig)
    return config  # pyright: ignore[reportReturnType]