usethatapp 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {usethatapp-2.2.0 → usethatapp-2.3.0}/CHANGELOG.md +10 -0
- {usethatapp-2.2.0/usethatapp.egg-info → usethatapp-2.3.0}/PKG-INFO +4 -2
- {usethatapp-2.2.0 → usethatapp-2.3.0}/README.md +3 -1
- {usethatapp-2.2.0 → usethatapp-2.3.0}/pyproject.toml +1 -1
- {usethatapp-2.2.0 → usethatapp-2.3.0}/tests/test_client.py +14 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/tests/test_license_keys.py +1 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp/__init__.py +1 -1
- {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp/client.py +2 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp/types.py +13 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0/usethatapp.egg-info}/PKG-INFO +4 -2
- {usethatapp-2.2.0 → usethatapp-2.3.0}/LICENSE +0 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/MANIFEST.in +0 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/setup.cfg +0 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/tests/__init__.py +0 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/tests/conftest.py +0 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/tests/test_config.py +0 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/tests/test_purchase.py +0 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp/config.py +0 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp/discovery.py +0 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp/errors.py +0 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp/py.typed +0 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp.egg-info/SOURCES.txt +0 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp.egg-info/dependency_links.txt +0 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp.egg-info/requires.txt +0 -0
- {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp.egg-info/top_level.txt +0 -0
|
@@ -6,6 +6,16 @@ format inspired by "Keep a Changelog".
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.3.0] - 2026-10-29
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `Entitlement.is_trial` and `LicenseState.is_trial`: true while the user is in a free
|
|
14
|
+
trial of a paid plan (entitled, nothing charged yet; `period_end` is the conversion
|
|
15
|
+
date). Explicit so integrations never string-match `status == "trialing"`. Both are
|
|
16
|
+
trailing fields with a `False` default, so positional construction and servers that
|
|
17
|
+
predate the field keep working.
|
|
18
|
+
|
|
9
19
|
## [2.2.0] - 2026-08-25
|
|
10
20
|
|
|
11
21
|
### Fixed (pre-release code review)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: usethatapp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Python SDK for usethatapp.com — OpenID Connect login and live license entitlement queries.
|
|
5
5
|
Author-email: UseThatApp <support@usethatapp.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -106,7 +106,7 @@ from usethatapp import (
|
|
|
106
106
|
get_prices, # () -> AppPrices (anonymous public API)
|
|
107
107
|
get_prices_async,
|
|
108
108
|
UtaSession, # sub, access_token, refresh_token, id_token, expires_at, ...
|
|
109
|
-
Entitlement, # entitled, version, product_id, product_public_id, status, ...
|
|
109
|
+
Entitlement, # entitled, version, product_id, product_public_id, status, is_trial, ...
|
|
110
110
|
AppInfo, # client_id, name, tagline, listing_mode, url, marketplace_url
|
|
111
111
|
Price, # public_id, product_id, product_name, amount, currency, ...
|
|
112
112
|
AppPrices, # client_id, app_name, has_free_tier, prices
|
|
@@ -186,6 +186,8 @@ your_db.save(user=current_user, license_key=order.license_key,
|
|
|
186
186
|
# 2) Any time you need the truth (app start, feature gate, cron):
|
|
187
187
|
state = validate_license_key(stored_key) # LicenseState
|
|
188
188
|
if state.entitled: ... # cache against state.period_end
|
|
189
|
+
# state.is_trial: in a free trial — entitled, nothing charged yet, and
|
|
190
|
+
# period_end is the conversion date. Show a banner; gate nothing extra.
|
|
189
191
|
# canceled purchases return entitled=False, status="canceled" — an
|
|
190
192
|
# answer, not an exception. UtaNotFoundError means a key that was
|
|
191
193
|
# never issued for your app.
|
|
@@ -73,7 +73,7 @@ from usethatapp import (
|
|
|
73
73
|
get_prices, # () -> AppPrices (anonymous public API)
|
|
74
74
|
get_prices_async,
|
|
75
75
|
UtaSession, # sub, access_token, refresh_token, id_token, expires_at, ...
|
|
76
|
-
Entitlement, # entitled, version, product_id, product_public_id, status, ...
|
|
76
|
+
Entitlement, # entitled, version, product_id, product_public_id, status, is_trial, ...
|
|
77
77
|
AppInfo, # client_id, name, tagline, listing_mode, url, marketplace_url
|
|
78
78
|
Price, # public_id, product_id, product_name, amount, currency, ...
|
|
79
79
|
AppPrices, # client_id, app_name, has_free_tier, prices
|
|
@@ -153,6 +153,8 @@ your_db.save(user=current_user, license_key=order.license_key,
|
|
|
153
153
|
# 2) Any time you need the truth (app start, feature gate, cron):
|
|
154
154
|
state = validate_license_key(stored_key) # LicenseState
|
|
155
155
|
if state.entitled: ... # cache against state.period_end
|
|
156
|
+
# state.is_trial: in a free trial — entitled, nothing charged yet, and
|
|
157
|
+
# period_end is the conversion date. Show a banner; gate nothing extra.
|
|
156
158
|
# canceled purchases return entitled=False, status="canceled" — an
|
|
157
159
|
# answer, not an exception. UtaNotFoundError means a key that was
|
|
158
160
|
# never issued for your app.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "usethatapp"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
8
8
|
description = "Python SDK for usethatapp.com — OpenID Connect login and live license entitlement queries."
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
authors = [{ name = "UseThatApp", email = "support@usethatapp.com"}]
|
|
@@ -148,9 +148,23 @@ def test_get_entitlement_licensed(oidc_routes):
|
|
|
148
148
|
assert ent.entitled and ent.version == "Pro" and ent.product_id == "p-1"
|
|
149
149
|
assert ent.product_public_id == "prod_abc123"
|
|
150
150
|
assert ent.status == "active" and ent.is_free is False
|
|
151
|
+
assert ent.is_trial is False
|
|
151
152
|
assert ent.period_end == "2026-07-01"
|
|
152
153
|
|
|
153
154
|
|
|
155
|
+
def test_get_entitlement_trialing(oidc_routes):
|
|
156
|
+
oidc_routes.get(API_URL + "/licensing/entitlement/").mock(
|
|
157
|
+
return_value=httpx.Response(200, json={
|
|
158
|
+
"entitled": True, "version": "Pro", "product_id": "prod_abc",
|
|
159
|
+
"product_public_id": "prod_abc", "status": "trialing",
|
|
160
|
+
"is_free": False, "is_trial": True, "period_end": "2026-11-11",
|
|
161
|
+
})
|
|
162
|
+
)
|
|
163
|
+
ent = get_entitlement("at-123")
|
|
164
|
+
assert ent.entitled and ent.is_trial and ent.status == "trialing"
|
|
165
|
+
assert ent.period_end == "2026-11-11"
|
|
166
|
+
|
|
167
|
+
|
|
154
168
|
def test_get_entitlement_free(oidc_routes):
|
|
155
169
|
oidc_routes.get(API_URL + "/licensing/entitlement/").mock(
|
|
156
170
|
return_value=httpx.Response(200, json={
|
|
@@ -44,6 +44,7 @@ def test_validate_sends_client_credentials(respx_mock):
|
|
|
44
44
|
)
|
|
45
45
|
state = validate_license_key("key_abc")
|
|
46
46
|
assert state.entitled and state.status == "active"
|
|
47
|
+
assert state.is_trial is False
|
|
47
48
|
assert state.license_id == STATE["license_id"]
|
|
48
49
|
assert state.license_key is None
|
|
49
50
|
request = route.calls.last.request
|
|
@@ -694,6 +694,7 @@ def _parse_entitlement(data: Mapping[str, Any]) -> Entitlement:
|
|
|
694
694
|
period_end=data.get("period_end"),
|
|
695
695
|
product_public_id=data.get("product_public_id"),
|
|
696
696
|
raw=dict(data),
|
|
697
|
+
is_trial=bool(data.get("is_trial", False)),
|
|
697
698
|
)
|
|
698
699
|
|
|
699
700
|
|
|
@@ -870,6 +871,7 @@ def _parse_license_state(data: Mapping[str, Any]) -> LicenseState:
|
|
|
870
871
|
canceled_at=data.get("canceled_at"),
|
|
871
872
|
license_key=data.get("license_key"),
|
|
872
873
|
rotated_at=data.get("rotated_at"),
|
|
874
|
+
is_trial=bool(data.get("is_trial", False)),
|
|
873
875
|
# Alias pair: fall back to product_public_id against a server
|
|
874
876
|
# that predates the identifier cutover.
|
|
875
877
|
product_id=str(
|
|
@@ -87,6 +87,15 @@ class Entitlement:
|
|
|
87
87
|
:attr:`Price.product_id` from the pricing API. New integrations should
|
|
88
88
|
gate on this field."""
|
|
89
89
|
|
|
90
|
+
is_trial: bool = False
|
|
91
|
+
"""True while the user is in a free trial of a paid plan: entitled to
|
|
92
|
+
the plan's real functionality, nothing charged yet. While true,
|
|
93
|
+
:attr:`period_end` is the date the trial converts (the first charge).
|
|
94
|
+
A canceled trial keeps ``entitled`` until that date and then drops it
|
|
95
|
+
— never cache past ``period_end``. Explicit so you never string-match
|
|
96
|
+
:attr:`status`. Trailing with a default (see the note above) so
|
|
97
|
+
positional construction predating it still works."""
|
|
98
|
+
|
|
90
99
|
|
|
91
100
|
@dataclass(frozen=True)
|
|
92
101
|
class AppInfo:
|
|
@@ -211,3 +220,7 @@ class LicenseState:
|
|
|
211
220
|
gating code works identically in both verification modes. Trailing
|
|
212
221
|
with a default so positional construction predating it still
|
|
213
222
|
works."""
|
|
223
|
+
|
|
224
|
+
is_trial: bool = False
|
|
225
|
+
"""True while the license is in its free trial — see
|
|
226
|
+
:attr:`Entitlement.is_trial`."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: usethatapp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Python SDK for usethatapp.com — OpenID Connect login and live license entitlement queries.
|
|
5
5
|
Author-email: UseThatApp <support@usethatapp.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -106,7 +106,7 @@ from usethatapp import (
|
|
|
106
106
|
get_prices, # () -> AppPrices (anonymous public API)
|
|
107
107
|
get_prices_async,
|
|
108
108
|
UtaSession, # sub, access_token, refresh_token, id_token, expires_at, ...
|
|
109
|
-
Entitlement, # entitled, version, product_id, product_public_id, status, ...
|
|
109
|
+
Entitlement, # entitled, version, product_id, product_public_id, status, is_trial, ...
|
|
110
110
|
AppInfo, # client_id, name, tagline, listing_mode, url, marketplace_url
|
|
111
111
|
Price, # public_id, product_id, product_name, amount, currency, ...
|
|
112
112
|
AppPrices, # client_id, app_name, has_free_tier, prices
|
|
@@ -186,6 +186,8 @@ your_db.save(user=current_user, license_key=order.license_key,
|
|
|
186
186
|
# 2) Any time you need the truth (app start, feature gate, cron):
|
|
187
187
|
state = validate_license_key(stored_key) # LicenseState
|
|
188
188
|
if state.entitled: ... # cache against state.period_end
|
|
189
|
+
# state.is_trial: in a free trial — entitled, nothing charged yet, and
|
|
190
|
+
# period_end is the conversion date. Show a banner; gate nothing extra.
|
|
189
191
|
# canceled purchases return entitled=False, status="canceled" — an
|
|
190
192
|
# answer, not an exception. UtaNotFoundError means a key that was
|
|
191
193
|
# never issued for your app.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|