gmt-python-sdk 0.27.0__tar.gz → 0.28.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 (129) hide show
  1. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/.gitignore +1 -0
  2. gmt_python_sdk-0.28.0/.release-please-manifest.json +3 -0
  3. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/CHANGELOG.md +21 -0
  4. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/PKG-INFO +1 -1
  5. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/pyproject.toml +1 -1
  6. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_utils/__init__.py +1 -0
  7. gmt_python_sdk-0.28.0/src/gmt/_utils/_path.py +127 -0
  8. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_version.py +1 -1
  9. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/resources/accounts.py +3 -3
  10. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/resources/purchases/bulk.py +5 -5
  11. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/resources/purchases/purchases.py +7 -7
  12. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/account_retrieve_response.py +4 -1
  13. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/profile/referral_retrieve_response.py +4 -3
  14. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/purchase_create_response.py +3 -0
  15. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/purchase_list_response.py +3 -0
  16. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/purchase_refund_response.py +3 -0
  17. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/purchase_request_verification_code_response.py +3 -0
  18. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/purchase_retrieve_response.py +3 -0
  19. gmt_python_sdk-0.28.0/tests/test_utils/test_path.py +89 -0
  20. gmt_python_sdk-0.27.0/.release-please-manifest.json +0 -3
  21. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/CONTRIBUTING.md +0 -0
  22. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/LICENSE +0 -0
  23. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/README.md +0 -0
  24. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/SECURITY.md +0 -0
  25. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/api.md +0 -0
  26. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/bin/check-release-environment +0 -0
  27. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/bin/publish-pypi +0 -0
  28. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/examples/.keep +0 -0
  29. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/release-please-config.json +0 -0
  30. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/requirements-dev.lock +0 -0
  31. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/__init__.py +0 -0
  32. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_base_client.py +0 -0
  33. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_client.py +0 -0
  34. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_compat.py +0 -0
  35. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_constants.py +0 -0
  36. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_exceptions.py +0 -0
  37. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_files.py +0 -0
  38. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_models.py +0 -0
  39. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_qs.py +0 -0
  40. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_resource.py +0 -0
  41. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_response.py +0 -0
  42. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_streaming.py +0 -0
  43. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_types.py +0 -0
  44. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_utils/_compat.py +0 -0
  45. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_utils/_datetime_parse.py +0 -0
  46. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_utils/_json.py +0 -0
  47. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_utils/_logs.py +0 -0
  48. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_utils/_proxy.py +0 -0
  49. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_utils/_reflection.py +0 -0
  50. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_utils/_resources_proxy.py +0 -0
  51. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_utils/_streams.py +0 -0
  52. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_utils/_sync.py +0 -0
  53. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_utils/_transform.py +0 -0
  54. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_utils/_typing.py +0 -0
  55. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/_utils/_utils.py +0 -0
  56. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/lib/.keep +0 -0
  57. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/pagination.py +0 -0
  58. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/py.typed +0 -0
  59. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/resources/__init__.py +0 -0
  60. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/resources/profile/__init__.py +0 -0
  61. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/resources/profile/discount.py +0 -0
  62. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/resources/profile/profile.py +0 -0
  63. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/resources/profile/referral/__init__.py +0 -0
  64. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/resources/profile/referral/referral.py +0 -0
  65. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/resources/profile/referral/transaction.py +0 -0
  66. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/resources/purchases/__init__.py +0 -0
  67. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/resources/service.py +0 -0
  68. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/resources/webhooks.py +0 -0
  69. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/__init__.py +0 -0
  70. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/account_list_countries_params.py +0 -0
  71. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/account_list_countries_response.py +0 -0
  72. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/account_list_params.py +0 -0
  73. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/account_list_response.py +0 -0
  74. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/profile/__init__.py +0 -0
  75. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/profile/discount_retrieve_response.py +0 -0
  76. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/profile/referral/__init__.py +0 -0
  77. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/profile/referral/transaction_list_params.py +0 -0
  78. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/profile/referral/transaction_list_response.py +0 -0
  79. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/profile/referral_transfer_balance_params.py +0 -0
  80. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/profile/referral_transfer_balance_response.py +0 -0
  81. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/profile_change_login_params.py +0 -0
  82. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/profile_change_login_response.py +0 -0
  83. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/profile_change_password_params.py +0 -0
  84. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/profile_change_password_response.py +0 -0
  85. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/profile_retrieve_response.py +0 -0
  86. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/profile_unbind_telegram_response.py +0 -0
  87. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/purchase_create_params.py +0 -0
  88. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/purchase_list_params.py +0 -0
  89. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/purchase_request_verification_code_params.py +0 -0
  90. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/purchases/__init__.py +0 -0
  91. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/purchases/bulk_create_params.py +0 -0
  92. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/purchases/bulk_create_response.py +0 -0
  93. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/purchases/bulk_retrieve_response.py +0 -0
  94. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/service_get_server_time_response.py +0 -0
  95. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/service_health_check_response.py +0 -0
  96. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/webhook_test_params.py +0 -0
  97. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/src/gmt/types/webhook_test_response.py +0 -0
  98. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/__init__.py +0 -0
  99. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/api_resources/__init__.py +0 -0
  100. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/api_resources/profile/__init__.py +0 -0
  101. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/api_resources/profile/referral/__init__.py +0 -0
  102. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/api_resources/profile/referral/test_transaction.py +0 -0
  103. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/api_resources/profile/test_discount.py +0 -0
  104. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/api_resources/profile/test_referral.py +0 -0
  105. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/api_resources/purchases/__init__.py +0 -0
  106. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/api_resources/purchases/test_bulk.py +0 -0
  107. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/api_resources/test_accounts.py +0 -0
  108. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/api_resources/test_profile.py +0 -0
  109. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/api_resources/test_purchases.py +0 -0
  110. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/api_resources/test_service.py +0 -0
  111. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/api_resources/test_webhooks.py +0 -0
  112. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/conftest.py +0 -0
  113. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/sample_file.txt +0 -0
  114. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/test_client.py +0 -0
  115. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/test_deepcopy.py +0 -0
  116. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/test_extract_files.py +0 -0
  117. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/test_files.py +0 -0
  118. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/test_models.py +0 -0
  119. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/test_qs.py +0 -0
  120. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/test_required_args.py +0 -0
  121. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/test_response.py +0 -0
  122. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/test_streaming.py +0 -0
  123. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/test_transform.py +0 -0
  124. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/test_utils/test_datetime_parse.py +0 -0
  125. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/test_utils/test_json.py +0 -0
  126. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/test_utils/test_proxy.py +0 -0
  127. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/test_utils/test_typing.py +0 -0
  128. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/tests/utils.py +0 -0
  129. {gmt_python_sdk-0.27.0 → gmt_python_sdk-0.28.0}/uv.lock +0 -0
@@ -1,4 +1,5 @@
1
1
  .prism.log
2
+ .stdy.log
2
3
  _dev
3
4
 
4
5
  __pycache__
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.28.0"
3
+ }
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.28.0 (2026-03-24)
4
+
5
+ Full Changelog: [v0.27.1...v0.28.0](https://github.com/cameo6/gmt-python-sdk/compare/v0.27.1...v0.28.0)
6
+
7
+ ### Features
8
+
9
+ * **api:** api update ([d8f345c](https://github.com/cameo6/gmt-python-sdk/commit/d8f345cd620595d0869cc34711f768201695d7d8))
10
+
11
+
12
+ ### Chores
13
+
14
+ * **internal:** update gitignore ([c2e249c](https://github.com/cameo6/gmt-python-sdk/commit/c2e249c38f976c3eca69c196d97191927da36540))
15
+
16
+ ## 0.27.1 (2026-03-20)
17
+
18
+ Full Changelog: [v0.27.0...v0.27.1](https://github.com/cameo6/gmt-python-sdk/compare/v0.27.0...v0.27.1)
19
+
20
+ ### Bug Fixes
21
+
22
+ * sanitize endpoint path params ([6d2a959](https://github.com/cameo6/gmt-python-sdk/commit/6d2a959bae599064b628dfeb5bb5069b337c0cdf))
23
+
3
24
  ## 0.27.0 (2026-03-19)
4
25
 
5
26
  Full Changelog: [v0.26.0...v0.27.0](https://github.com/cameo6/gmt-python-sdk/compare/v0.26.0...v0.27.0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: gmt-python-sdk
3
- Version: 0.27.0
3
+ Version: 0.28.0
4
4
  Summary: The official Python library for the gmt API
5
5
  Project-URL: Homepage, https://github.com/cameo6/gmt-python-sdk
6
6
  Project-URL: Repository, https://github.com/cameo6/gmt-python-sdk
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gmt-python-sdk"
3
- version = "0.27.0"
3
+ version = "0.28.0"
4
4
  description = "The official Python library for the gmt API"
5
5
  dynamic = ["readme"]
6
6
  license = "Apache-2.0"
@@ -1,3 +1,4 @@
1
+ from ._path import path_template as path_template
1
2
  from ._sync import asyncify as asyncify
2
3
  from ._proxy import LazyProxy as LazyProxy
3
4
  from ._utils import (
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import (
5
+ Any,
6
+ Mapping,
7
+ Callable,
8
+ )
9
+ from urllib.parse import quote
10
+
11
+ # Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E).
12
+ _DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$")
13
+
14
+ _PLACEHOLDER_RE = re.compile(r"\{(\w+)\}")
15
+
16
+
17
+ def _quote_path_segment_part(value: str) -> str:
18
+ """Percent-encode `value` for use in a URI path segment.
19
+
20
+ Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe.
21
+ https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
22
+ """
23
+ # quote() already treats unreserved characters (letters, digits, and -._~)
24
+ # as safe, so we only need to add sub-delims, ':', and '@'.
25
+ # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted.
26
+ return quote(value, safe="!$&'()*+,;=:@")
27
+
28
+
29
+ def _quote_query_part(value: str) -> str:
30
+ """Percent-encode `value` for use in a URI query string.
31
+
32
+ Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe.
33
+ https://datatracker.ietf.org/doc/html/rfc3986#section-3.4
34
+ """
35
+ return quote(value, safe="!$'()*+,;:@/?")
36
+
37
+
38
+ def _quote_fragment_part(value: str) -> str:
39
+ """Percent-encode `value` for use in a URI fragment.
40
+
41
+ Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe.
42
+ https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
43
+ """
44
+ return quote(value, safe="!$&'()*+,;=:@/?")
45
+
46
+
47
+ def _interpolate(
48
+ template: str,
49
+ values: Mapping[str, Any],
50
+ quoter: Callable[[str], str],
51
+ ) -> str:
52
+ """Replace {name} placeholders in `template`, quoting each value with `quoter`.
53
+
54
+ Placeholder names are looked up in `values`.
55
+
56
+ Raises:
57
+ KeyError: If a placeholder is not found in `values`.
58
+ """
59
+ # re.split with a capturing group returns alternating
60
+ # [text, name, text, name, ..., text] elements.
61
+ parts = _PLACEHOLDER_RE.split(template)
62
+
63
+ for i in range(1, len(parts), 2):
64
+ name = parts[i]
65
+ if name not in values:
66
+ raise KeyError(f"a value for placeholder {{{name}}} was not provided")
67
+ val = values[name]
68
+ if val is None:
69
+ parts[i] = "null"
70
+ elif isinstance(val, bool):
71
+ parts[i] = "true" if val else "false"
72
+ else:
73
+ parts[i] = quoter(str(values[name]))
74
+
75
+ return "".join(parts)
76
+
77
+
78
+ def path_template(template: str, /, **kwargs: Any) -> str:
79
+ """Interpolate {name} placeholders in `template` from keyword arguments.
80
+
81
+ Args:
82
+ template: The template string containing {name} placeholders.
83
+ **kwargs: Keyword arguments to interpolate into the template.
84
+
85
+ Returns:
86
+ The template with placeholders interpolated and percent-encoded.
87
+
88
+ Safe characters for percent-encoding are dependent on the URI component.
89
+ Placeholders in path and fragment portions are percent-encoded where the `segment`
90
+ and `fragment` sets from RFC 3986 respectively are considered safe.
91
+ Placeholders in the query portion are percent-encoded where the `query` set from
92
+ RFC 3986 §3.3 is considered safe except for = and & characters.
93
+
94
+ Raises:
95
+ KeyError: If a placeholder is not found in `kwargs`.
96
+ ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments).
97
+ """
98
+ # Split the template into path, query, and fragment portions.
99
+ fragment_template: str | None = None
100
+ query_template: str | None = None
101
+
102
+ rest = template
103
+ if "#" in rest:
104
+ rest, fragment_template = rest.split("#", 1)
105
+ if "?" in rest:
106
+ rest, query_template = rest.split("?", 1)
107
+ path_template = rest
108
+
109
+ # Interpolate each portion with the appropriate quoting rules.
110
+ path_result = _interpolate(path_template, kwargs, _quote_path_segment_part)
111
+
112
+ # Reject dot-segments (. and ..) in the final assembled path. The check
113
+ # runs after interpolation so that adjacent placeholders or a mix of static
114
+ # text and placeholders that together form a dot-segment are caught.
115
+ # Also reject percent-encoded dot-segments to protect against incorrectly
116
+ # implemented normalization in servers/proxies.
117
+ for segment in path_result.split("/"):
118
+ if _DOT_SEGMENT_RE.match(segment):
119
+ raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed")
120
+
121
+ result = path_result
122
+ if query_template is not None:
123
+ result += "?" + _interpolate(query_template, kwargs, _quote_query_part)
124
+ if fragment_template is not None:
125
+ result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part)
126
+
127
+ return result
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "gmt"
4
- __version__ = "0.27.0" # x-release-please-version
4
+ __version__ = "0.28.0" # x-release-please-version
@@ -8,7 +8,7 @@ import httpx
8
8
 
9
9
  from ..types import account_list_params, account_list_countries_params
10
10
  from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
11
- from .._utils import maybe_transform
11
+ from .._utils import path_template, maybe_transform
12
12
  from .._compat import cached_property
13
13
  from .._resource import SyncAPIResource, AsyncAPIResource
14
14
  from .._response import (
@@ -100,7 +100,7 @@ class AccountsResource(SyncAPIResource):
100
100
  if not country_code:
101
101
  raise ValueError(f"Expected a non-empty value for `country_code` but received {country_code!r}")
102
102
  return self._get(
103
- f"/v1/accounts/{country_code}",
103
+ path_template("/v1/accounts/{country_code}", country_code=country_code),
104
104
  options=make_request_options(
105
105
  extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
106
106
  ),
@@ -322,7 +322,7 @@ class AsyncAccountsResource(AsyncAPIResource):
322
322
  if not country_code:
323
323
  raise ValueError(f"Expected a non-empty value for `country_code` but received {country_code!r}")
324
324
  return await self._get(
325
- f"/v1/accounts/{country_code}",
325
+ path_template("/v1/accounts/{country_code}", country_code=country_code),
326
326
  options=make_request_options(
327
327
  extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
328
328
  ),
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import httpx
6
6
 
7
7
  from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
8
- from ..._utils import maybe_transform, async_maybe_transform
8
+ from ..._utils import path_template, maybe_transform, async_maybe_transform
9
9
  from ..._compat import cached_property
10
10
  from ..._resource import SyncAPIResource, AsyncAPIResource
11
11
  from ..._response import (
@@ -145,7 +145,7 @@ class BulkResource(SyncAPIResource):
145
145
  if not purchase_id:
146
146
  raise ValueError(f"Expected a non-empty value for `purchase_id` but received {purchase_id!r}")
147
147
  return self._get(
148
- f"/v1/purchases/bulk/{purchase_id}",
148
+ path_template("/v1/purchases/bulk/{purchase_id}", purchase_id=purchase_id),
149
149
  options=make_request_options(
150
150
  extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
151
151
  ),
@@ -180,7 +180,7 @@ class BulkResource(SyncAPIResource):
180
180
  raise ValueError(f"Expected a non-empty value for `purchase_id` but received {purchase_id!r}")
181
181
  extra_headers = {"Accept": "application/zip", **(extra_headers or {})}
182
182
  return self._get(
183
- f"/v1/purchases/bulk/{purchase_id}/download",
183
+ path_template("/v1/purchases/bulk/{purchase_id}/download", purchase_id=purchase_id),
184
184
  options=make_request_options(
185
185
  extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
186
186
  ),
@@ -303,7 +303,7 @@ class AsyncBulkResource(AsyncAPIResource):
303
303
  if not purchase_id:
304
304
  raise ValueError(f"Expected a non-empty value for `purchase_id` but received {purchase_id!r}")
305
305
  return await self._get(
306
- f"/v1/purchases/bulk/{purchase_id}",
306
+ path_template("/v1/purchases/bulk/{purchase_id}", purchase_id=purchase_id),
307
307
  options=make_request_options(
308
308
  extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
309
309
  ),
@@ -338,7 +338,7 @@ class AsyncBulkResource(AsyncAPIResource):
338
338
  raise ValueError(f"Expected a non-empty value for `purchase_id` but received {purchase_id!r}")
339
339
  extra_headers = {"Accept": "application/zip", **(extra_headers or {})}
340
340
  return await self._get(
341
- f"/v1/purchases/bulk/{purchase_id}/download",
341
+ path_template("/v1/purchases/bulk/{purchase_id}/download", purchase_id=purchase_id),
342
342
  options=make_request_options(
343
343
  extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
344
344
  ),
@@ -16,7 +16,7 @@ from .bulk import (
16
16
  )
17
17
  from ...types import purchase_list_params, purchase_create_params, purchase_request_verification_code_params
18
18
  from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
19
- from ..._utils import maybe_transform, async_maybe_transform
19
+ from ..._utils import path_template, maybe_transform, async_maybe_transform
20
20
  from ..._compat import cached_property
21
21
  from ..._resource import SyncAPIResource, AsyncAPIResource
22
22
  from ..._response import (
@@ -141,7 +141,7 @@ class PurchasesResource(SyncAPIResource):
141
141
  timeout: Override the client-level default timeout for this request, in seconds
142
142
  """
143
143
  return self._get(
144
- f"/v1/purchases/{purchase_id}",
144
+ path_template("/v1/purchases/{purchase_id}", purchase_id=purchase_id),
145
145
  options=make_request_options(
146
146
  extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
147
147
  ),
@@ -254,7 +254,7 @@ class PurchasesResource(SyncAPIResource):
254
254
  timeout: Override the client-level default timeout for this request, in seconds
255
255
  """
256
256
  return self._post(
257
- f"/v1/purchases/{purchase_id}/refund",
257
+ path_template("/v1/purchases/{purchase_id}/refund", purchase_id=purchase_id),
258
258
  options=make_request_options(
259
259
  extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
260
260
  ),
@@ -314,7 +314,7 @@ class PurchasesResource(SyncAPIResource):
314
314
  timeout: Override the client-level default timeout for this request, in seconds
315
315
  """
316
316
  return self._post(
317
- f"/v1/purchases/{purchase_id}/request-code",
317
+ path_template("/v1/purchases/{purchase_id}/request-code", purchase_id=purchase_id),
318
318
  body=maybe_transform(
319
319
  {"callback_url": callback_url},
320
320
  purchase_request_verification_code_params.PurchaseRequestVerificationCodeParams,
@@ -433,7 +433,7 @@ class AsyncPurchasesResource(AsyncAPIResource):
433
433
  timeout: Override the client-level default timeout for this request, in seconds
434
434
  """
435
435
  return await self._get(
436
- f"/v1/purchases/{purchase_id}",
436
+ path_template("/v1/purchases/{purchase_id}", purchase_id=purchase_id),
437
437
  options=make_request_options(
438
438
  extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
439
439
  ),
@@ -546,7 +546,7 @@ class AsyncPurchasesResource(AsyncAPIResource):
546
546
  timeout: Override the client-level default timeout for this request, in seconds
547
547
  """
548
548
  return await self._post(
549
- f"/v1/purchases/{purchase_id}/refund",
549
+ path_template("/v1/purchases/{purchase_id}/refund", purchase_id=purchase_id),
550
550
  options=make_request_options(
551
551
  extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
552
552
  ),
@@ -606,7 +606,7 @@ class AsyncPurchasesResource(AsyncAPIResource):
606
606
  timeout: Override the client-level default timeout for this request, in seconds
607
607
  """
608
608
  return await self._post(
609
- f"/v1/purchases/{purchase_id}/request-code",
609
+ path_template("/v1/purchases/{purchase_id}/request-code", purchase_id=purchase_id),
610
610
  body=await async_maybe_transform(
611
611
  {"callback_url": callback_url},
612
612
  purchase_request_verification_code_params.PurchaseRequestVerificationCodeParams,
@@ -1,6 +1,6 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
- from typing import List
3
+ from typing import List, Optional
4
4
  from typing_extensions import Literal
5
5
 
6
6
  from .._models import BaseModel
@@ -50,3 +50,6 @@ class AccountRetrieveResponse(BaseModel):
50
50
 
51
51
  tags: List[Literal["HIGH_QUALITY", "HIGH_DEMAND"]]
52
52
  """Account tags (e.g., HIGH_QUALITY for premium accounts)."""
53
+
54
+ available_count: Optional[float] = None
55
+ """Number of available accounts for this country."""
@@ -1,6 +1,7 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  from typing import List, Optional
4
+ from typing_extensions import Literal
4
5
 
5
6
  from ..._models import BaseModel
6
7
 
@@ -26,7 +27,7 @@ class CurrentLevelProgress(BaseModel):
26
27
 
27
28
 
28
29
  class CurrentLevel(BaseModel):
29
- name: str
30
+ name: Literal["bronze", "silver", "gold", "platinum"]
30
31
  """Name of the current referral level"""
31
32
 
32
33
  percent: float
@@ -36,7 +37,7 @@ class CurrentLevel(BaseModel):
36
37
 
37
38
 
38
39
  class Level(BaseModel):
39
- name: str
40
+ name: Literal["bronze", "silver", "gold", "platinum"]
40
41
  """Name of the referral level"""
41
42
 
42
43
  percent: float
@@ -66,7 +67,7 @@ class NextLevelRequirements(BaseModel):
66
67
  class NextLevel(BaseModel):
67
68
  """Next level information. Null if already at max level"""
68
69
 
69
- name: str
70
+ name: Literal["bronze", "silver", "gold", "platinum"]
70
71
  """Name of the next referral level"""
71
72
 
72
73
  percent: float
@@ -74,6 +74,9 @@ class PurchaseCreateResponse(BaseModel):
74
74
 
75
75
  display_name: DisplayName
76
76
 
77
+ emoji: str
78
+ """Country flag emoji."""
79
+
77
80
  phone_number: Optional[str] = None
78
81
  """
79
82
  **E.164 International Format.** Phone number with country code prefix (e.g.,
@@ -74,6 +74,9 @@ class PurchaseListResponse(BaseModel):
74
74
 
75
75
  display_name: DisplayName
76
76
 
77
+ emoji: str
78
+ """Country flag emoji."""
79
+
77
80
  phone_number: Optional[str] = None
78
81
  """
79
82
  **E.164 International Format.** Phone number with country code prefix (e.g.,
@@ -82,6 +82,9 @@ class Purchase(BaseModel):
82
82
 
83
83
  display_name: PurchaseDisplayName
84
84
 
85
+ emoji: str
86
+ """Country flag emoji."""
87
+
85
88
  phone_number: Optional[str] = None
86
89
  """
87
90
  **E.164 International Format.** Phone number with country code prefix (e.g.,
@@ -98,6 +98,9 @@ class Purchase(BaseModel):
98
98
 
99
99
  display_name: PurchaseDisplayName
100
100
 
101
+ emoji: str
102
+ """Country flag emoji."""
103
+
101
104
  phone_number: Optional[str] = None
102
105
  """
103
106
  **E.164 International Format.** Phone number with country code prefix (e.g.,
@@ -74,6 +74,9 @@ class PurchaseRetrieveResponse(BaseModel):
74
74
 
75
75
  display_name: DisplayName
76
76
 
77
+ emoji: str
78
+ """Country flag emoji."""
79
+
77
80
  phone_number: Optional[str] = None
78
81
  """
79
82
  **E.164 International Format.** Phone number with country code prefix (e.g.,
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import pytest
6
+
7
+ from gmt._utils._path import path_template
8
+
9
+
10
+ @pytest.mark.parametrize(
11
+ "template, kwargs, expected",
12
+ [
13
+ ("/v1/{id}", dict(id="abc"), "/v1/abc"),
14
+ ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"),
15
+ ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"),
16
+ ("/{w}/{w}", dict(w="echo"), "/echo/echo"),
17
+ ("/v1/static", {}, "/v1/static"),
18
+ ("", {}, ""),
19
+ ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"),
20
+ ("/v1/{v}", dict(v=None), "/v1/null"),
21
+ ("/v1/{v}", dict(v=True), "/v1/true"),
22
+ ("/v1/{v}", dict(v=False), "/v1/false"),
23
+ ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok
24
+ ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok
25
+ ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok
26
+ ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok
27
+ ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine
28
+ (
29
+ "/v1/{a}?query={b}",
30
+ dict(a="../../other/endpoint", b="a&bad=true"),
31
+ "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue",
32
+ ),
33
+ ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"),
34
+ ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"),
35
+ ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"),
36
+ ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input
37
+ # Query: slash and ? are safe, # is not
38
+ ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"),
39
+ ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"),
40
+ ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"),
41
+ ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"),
42
+ # Fragment: slash and ? are safe
43
+ ("/docs#{v}", dict(v="a/b"), "/docs#a/b"),
44
+ ("/docs#{v}", dict(v="a?b"), "/docs#a?b"),
45
+ # Path: slash, ? and # are all encoded
46
+ ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"),
47
+ ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"),
48
+ ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"),
49
+ # same var encoded differently by component
50
+ (
51
+ "/v1/{v}?q={v}#{v}",
52
+ dict(v="a/b?c#d"),
53
+ "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d",
54
+ ),
55
+ ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection
56
+ ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection
57
+ ],
58
+ )
59
+ def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None:
60
+ assert path_template(template, **kwargs) == expected
61
+
62
+
63
+ def test_missing_kwarg_raises_key_error() -> None:
64
+ with pytest.raises(KeyError, match="org_id"):
65
+ path_template("/v1/{org_id}")
66
+
67
+
68
+ @pytest.mark.parametrize(
69
+ "template, kwargs",
70
+ [
71
+ ("{a}/path", dict(a=".")),
72
+ ("{a}/path", dict(a="..")),
73
+ ("/v1/{a}", dict(a=".")),
74
+ ("/v1/{a}", dict(a="..")),
75
+ ("/v1/{a}/path", dict(a=".")),
76
+ ("/v1/{a}/path", dict(a="..")),
77
+ ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".."
78
+ ("/v1/{a}.", dict(a=".")), # var + static → ".."
79
+ ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "."
80
+ ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text
81
+ ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static
82
+ ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static
83
+ ("/v1/{v}?q=1", dict(v="..")),
84
+ ("/v1/{v}#frag", dict(v="..")),
85
+ ],
86
+ )
87
+ def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None:
88
+ with pytest.raises(ValueError, match="dot-segment"):
89
+ path_template(template, **kwargs)
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.27.0"
3
- }
File without changes
File without changes
File without changes