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.
Files changed (25) hide show
  1. {usethatapp-2.2.0 → usethatapp-2.3.0}/CHANGELOG.md +10 -0
  2. {usethatapp-2.2.0/usethatapp.egg-info → usethatapp-2.3.0}/PKG-INFO +4 -2
  3. {usethatapp-2.2.0 → usethatapp-2.3.0}/README.md +3 -1
  4. {usethatapp-2.2.0 → usethatapp-2.3.0}/pyproject.toml +1 -1
  5. {usethatapp-2.2.0 → usethatapp-2.3.0}/tests/test_client.py +14 -0
  6. {usethatapp-2.2.0 → usethatapp-2.3.0}/tests/test_license_keys.py +1 -0
  7. {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp/__init__.py +1 -1
  8. {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp/client.py +2 -0
  9. {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp/types.py +13 -0
  10. {usethatapp-2.2.0 → usethatapp-2.3.0/usethatapp.egg-info}/PKG-INFO +4 -2
  11. {usethatapp-2.2.0 → usethatapp-2.3.0}/LICENSE +0 -0
  12. {usethatapp-2.2.0 → usethatapp-2.3.0}/MANIFEST.in +0 -0
  13. {usethatapp-2.2.0 → usethatapp-2.3.0}/setup.cfg +0 -0
  14. {usethatapp-2.2.0 → usethatapp-2.3.0}/tests/__init__.py +0 -0
  15. {usethatapp-2.2.0 → usethatapp-2.3.0}/tests/conftest.py +0 -0
  16. {usethatapp-2.2.0 → usethatapp-2.3.0}/tests/test_config.py +0 -0
  17. {usethatapp-2.2.0 → usethatapp-2.3.0}/tests/test_purchase.py +0 -0
  18. {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp/config.py +0 -0
  19. {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp/discovery.py +0 -0
  20. {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp/errors.py +0 -0
  21. {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp/py.typed +0 -0
  22. {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp.egg-info/SOURCES.txt +0 -0
  23. {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp.egg-info/dependency_links.txt +0 -0
  24. {usethatapp-2.2.0 → usethatapp-2.3.0}/usethatapp.egg-info/requires.txt +0 -0
  25. {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.2.0
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.2.0"
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
@@ -67,7 +67,7 @@ from .types import (
67
67
  UtaSession,
68
68
  )
69
69
 
70
- __version__ = "2.2.0"
70
+ __version__ = "2.3.0"
71
71
 
72
72
  __all__ = [
73
73
  "__version__",
@@ -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.2.0
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