growsurf-python 0.0.2__tar.gz → 0.1.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 (119) hide show
  1. growsurf_python-0.1.0/.release-please-manifest.json +3 -0
  2. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/CHANGELOG.md +19 -0
  3. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/PKG-INFO +1 -1
  4. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/api.md +2 -0
  5. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/bin/check-release-environment +4 -0
  6. growsurf_python-0.1.0/bin/publish-pypi +7 -0
  7. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/pyproject.toml +1 -1
  8. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_files.py +1 -1
  9. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_models.py +80 -0
  10. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_version.py +1 -1
  11. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/resources/campaign/participant.py +101 -0
  12. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/__init__.py +3 -0
  13. growsurf_python-0.1.0/src/growsurf/types/campaign/participant_create_mobile_token_response.py +15 -0
  14. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/api_resources/campaign/test_participant.py +109 -0
  15. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/test_models.py +57 -3
  16. growsurf_python-0.0.2/.release-please-manifest.json +0 -3
  17. growsurf_python-0.0.2/bin/publish-pypi +0 -11
  18. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/.gitignore +0 -0
  19. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/CONTRIBUTING.md +0 -0
  20. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/LICENSE +0 -0
  21. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/README.md +0 -0
  22. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/SECURITY.md +0 -0
  23. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/examples/.keep +0 -0
  24. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/release-please-config.json +0 -0
  25. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/requirements-dev.lock +0 -0
  26. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/__init__.py +0 -0
  27. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_base_client.py +0 -0
  28. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_client.py +0 -0
  29. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_compat.py +0 -0
  30. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_constants.py +0 -0
  31. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_exceptions.py +0 -0
  32. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_qs.py +0 -0
  33. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_resource.py +0 -0
  34. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_response.py +0 -0
  35. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_streaming.py +0 -0
  36. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_types.py +0 -0
  37. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_utils/__init__.py +0 -0
  38. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_utils/_compat.py +0 -0
  39. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_utils/_datetime_parse.py +0 -0
  40. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_utils/_json.py +0 -0
  41. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_utils/_logs.py +0 -0
  42. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_utils/_path.py +0 -0
  43. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_utils/_proxy.py +0 -0
  44. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_utils/_reflection.py +0 -0
  45. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_utils/_resources_proxy.py +0 -0
  46. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_utils/_streams.py +0 -0
  47. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_utils/_sync.py +0 -0
  48. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_utils/_transform.py +0 -0
  49. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_utils/_typing.py +0 -0
  50. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/_utils/_utils.py +0 -0
  51. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/lib/.keep +0 -0
  52. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/py.typed +0 -0
  53. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/resources/__init__.py +0 -0
  54. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/resources/campaign/__init__.py +0 -0
  55. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/resources/campaign/campaign.py +0 -0
  56. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/resources/campaign/commission.py +0 -0
  57. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/resources/campaign/reward.py +0 -0
  58. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/__init__.py +0 -0
  59. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/campaign.py +0 -0
  60. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/commission_approve_response.py +0 -0
  61. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/commission_delete_response.py +0 -0
  62. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/fraud_risk_level.py +0 -0
  63. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant.py +0 -0
  64. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant_add_params.py +0 -0
  65. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant_delete_response.py +0 -0
  66. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant_list_commissions_params.py +0 -0
  67. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant_list_payouts_params.py +0 -0
  68. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant_list_referrals_params.py +0 -0
  69. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant_list_rewards_params.py +0 -0
  70. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant_list_rewards_response.py +0 -0
  71. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant_record_transaction_params.py +0 -0
  72. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant_record_transaction_response.py +0 -0
  73. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant_reward.py +0 -0
  74. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant_send_invites_params.py +0 -0
  75. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant_send_invites_response.py +0 -0
  76. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant_trigger_referral_response.py +0 -0
  77. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/participant_update_params.py +0 -0
  78. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/referral_source.py +0 -0
  79. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/referral_status.py +0 -0
  80. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/reward_approve_params.py +0 -0
  81. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/reward_approve_response.py +0 -0
  82. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/reward_delete_response.py +0 -0
  83. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign/reward_fulfill_response.py +0 -0
  84. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign_list_commissions_params.py +0 -0
  85. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign_list_leaderboard_params.py +0 -0
  86. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign_list_participants_params.py +0 -0
  87. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign_list_payouts_params.py +0 -0
  88. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign_list_referrals_params.py +0 -0
  89. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign_list_response.py +0 -0
  90. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign_retrieve_analytics_params.py +0 -0
  91. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/campaign_retrieve_analytics_response.py +0 -0
  92. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/commission_structure.py +0 -0
  93. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/participant_commission_list.py +0 -0
  94. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/participant_list.py +0 -0
  95. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/participant_payout_list.py +0 -0
  96. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/src/growsurf/types/referral_list.py +0 -0
  97. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/__init__.py +0 -0
  98. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/api_resources/__init__.py +0 -0
  99. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/api_resources/campaign/__init__.py +0 -0
  100. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/api_resources/campaign/test_commission.py +0 -0
  101. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/api_resources/campaign/test_reward.py +0 -0
  102. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/api_resources/test_campaign.py +0 -0
  103. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/conftest.py +0 -0
  104. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/sample_file.txt +0 -0
  105. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/test_client.py +0 -0
  106. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/test_extract_files.py +0 -0
  107. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/test_files.py +0 -0
  108. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/test_qs.py +0 -0
  109. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/test_required_args.py +0 -0
  110. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/test_response.py +0 -0
  111. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/test_streaming.py +0 -0
  112. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/test_transform.py +0 -0
  113. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/test_utils/test_datetime_parse.py +0 -0
  114. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/test_utils/test_json.py +0 -0
  115. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/test_utils/test_path.py +0 -0
  116. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/test_utils/test_proxy.py +0 -0
  117. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/test_utils/test_typing.py +0 -0
  118. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/tests/utils.py +0 -0
  119. {growsurf_python-0.0.2 → growsurf_python-0.1.0}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.1.0"
3
+ }
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0 (2026-05-12)
4
+
5
+ Full Changelog: [v0.0.2...v0.1.0](https://github.com/growsurf/growsurf-python/compare/v0.0.2...v0.1.0)
6
+
7
+ ### Features
8
+
9
+ * **api:** manual updates ([31c14cf](https://github.com/growsurf/growsurf-python/commit/31c14cfba6611cc5c974230e9e2584e6f6b3a7a3))
10
+ * **internal/types:** support eagerly validating pydantic iterators ([c6cd14c](https://github.com/growsurf/growsurf-python/commit/c6cd14c45e8a652f511ca9c31de194c4cd140d2d))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **client:** add missing f-string prefix in file type error message ([7a3c356](https://github.com/growsurf/growsurf-python/commit/7a3c356587c6b2ea311c1f28723fd86a946db5db))
16
+
17
+
18
+ ### Chores
19
+
20
+ * update SDK settings ([434e9ce](https://github.com/growsurf/growsurf-python/commit/434e9cec74d6101510ca7f45261e981b8362121f))
21
+
3
22
  ## 0.0.2 (2026-05-08)
4
23
 
5
24
  Full Changelog: [v0.0.1...v0.0.2](https://github.com/growsurf/growsurf-python/compare/v0.0.1...v0.0.2)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: growsurf-python
3
- Version: 0.0.2
3
+ Version: 0.1.0
4
4
  Summary: The official Python library for the growsurf API
5
5
  Project-URL: Homepage, https://github.com/growsurf/growsurf-python
6
6
  Project-URL: Repository, https://github.com/growsurf/growsurf-python
@@ -38,6 +38,7 @@ from growsurf.types.campaign import (
38
38
  ReferralSource,
39
39
  ReferralStatus,
40
40
  ParticipantDeleteResponse,
41
+ ParticipantCreateMobileTokenResponse,
41
42
  ParticipantListRewardsResponse,
42
43
  ParticipantRecordTransactionResponse,
43
44
  ParticipantSendInvitesResponse,
@@ -51,6 +52,7 @@ Methods:
51
52
  - <code title="post /campaign/{id}/participant/{participantIdOrEmail}">client.campaign.participant.<a href="./src/growsurf/resources/campaign/participant.py">update</a>(participant_id_or_email, \*, id, \*\*<a href="src/growsurf/types/campaign/participant_update_params.py">params</a>) -> <a href="./src/growsurf/types/campaign/participant.py">Participant</a></code>
52
53
  - <code title="delete /campaign/{id}/participant/{participantIdOrEmail}">client.campaign.participant.<a href="./src/growsurf/resources/campaign/participant.py">delete</a>(participant_id_or_email, \*, id) -> <a href="./src/growsurf/types/campaign/participant_delete_response.py">ParticipantDeleteResponse</a></code>
53
54
  - <code title="post /campaign/{id}/participant">client.campaign.participant.<a href="./src/growsurf/resources/campaign/participant.py">add</a>(id, \*\*<a href="src/growsurf/types/campaign/participant_add_params.py">params</a>) -> <a href="./src/growsurf/types/campaign/participant.py">Participant</a></code>
55
+ - <code title="post /campaign/{id}/participant/{participantIdOrEmail}/mobile-token">client.campaign.participant.<a href="./src/growsurf/resources/campaign/participant.py">create_mobile_token</a>(participant_id_or_email, \*, id) -> <a href="./src/growsurf/types/campaign/participant_create_mobile_token_response.py">ParticipantCreateMobileTokenResponse</a></code>
54
56
  - <code title="get /campaign/{id}/participant/{participantIdOrEmail}/commissions">client.campaign.participant.<a href="./src/growsurf/resources/campaign/participant.py">list_commissions</a>(participant_id_or_email, \*, id, \*\*<a href="src/growsurf/types/campaign/participant_list_commissions_params.py">params</a>) -> <a href="./src/growsurf/types/participant_commission_list.py">ParticipantCommissionList</a></code>
55
57
  - <code title="get /campaign/{id}/participant/{participantIdOrEmail}/payouts">client.campaign.participant.<a href="./src/growsurf/resources/campaign/participant.py">list_payouts</a>(participant_id_or_email, \*, id, \*\*<a href="src/growsurf/types/campaign/participant_list_payouts_params.py">params</a>) -> <a href="./src/growsurf/types/participant_payout_list.py">ParticipantPayoutList</a></code>
56
58
  - <code title="get /campaign/{id}/participant/{participantIdOrEmail}/referrals">client.campaign.participant.<a href="./src/growsurf/resources/campaign/participant.py">list_referrals</a>(participant_id_or_email, \*, id, \*\*<a href="src/growsurf/types/campaign/participant_list_referrals_params.py">params</a>) -> <a href="./src/growsurf/types/referral_list.py">ReferralList</a></code>
@@ -2,6 +2,10 @@
2
2
 
3
3
  errors=()
4
4
 
5
+ if [ -z "${PYPI_TOKEN}" ]; then
6
+ errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.")
7
+ fi
8
+
5
9
  lenErrors=${#errors[@]}
6
10
 
7
11
  if [[ lenErrors -gt 0 ]]; then
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -eux
4
+ rm -rf dist
5
+ mkdir -p dist
6
+ uv build
7
+ uv publish --token=$PYPI_TOKEN
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "growsurf-python"
3
- version = "0.0.2"
3
+ version = "0.1.0"
4
4
  description = "The official Python library for the growsurf API"
5
5
  dynamic = ["readme"]
6
6
  license = "Apache-2.0"
@@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles
99
99
  elif is_sequence_t(files):
100
100
  files = [(key, await _async_transform_file(file)) for key, file in files]
101
101
  else:
102
- raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence")
102
+ raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence")
103
103
 
104
104
  return files
105
105
 
@@ -25,7 +25,9 @@ from typing_extensions import (
25
25
  ClassVar,
26
26
  Protocol,
27
27
  Required,
28
+ Annotated,
28
29
  ParamSpec,
30
+ TypeAlias,
29
31
  TypedDict,
30
32
  TypeGuard,
31
33
  final,
@@ -79,7 +81,15 @@ from ._compat import (
79
81
  from ._constants import RAW_RESPONSE_HEADER
80
82
 
81
83
  if TYPE_CHECKING:
84
+ from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler
85
+ from pydantic_core import CoreSchema, core_schema
82
86
  from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema
87
+ else:
88
+ try:
89
+ from pydantic_core import CoreSchema, core_schema
90
+ except ImportError:
91
+ CoreSchema = None
92
+ core_schema = None
83
93
 
84
94
  __all__ = ["BaseModel", "GenericModel"]
85
95
 
@@ -396,6 +406,76 @@ class BaseModel(pydantic.BaseModel):
396
406
  )
397
407
 
398
408
 
409
+ class _EagerIterable(list[_T], Generic[_T]):
410
+ """
411
+ Accepts any Iterable[T] input (including generators), consumes it
412
+ eagerly, and validates all items upfront.
413
+
414
+ Validation preserves the original container type where possible
415
+ (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON)
416
+ always emits a list — round-tripping through model_dump() will not
417
+ restore the original container type.
418
+ """
419
+
420
+ @classmethod
421
+ def __get_pydantic_core_schema__(
422
+ cls,
423
+ source_type: Any,
424
+ handler: GetCoreSchemaHandler,
425
+ ) -> CoreSchema:
426
+ (item_type,) = get_args(source_type) or (Any,)
427
+ item_schema: CoreSchema = handler.generate_schema(item_type)
428
+ list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema)
429
+
430
+ return core_schema.no_info_wrap_validator_function(
431
+ cls._validate,
432
+ list_of_items_schema,
433
+ serialization=core_schema.plain_serializer_function_ser_schema(
434
+ cls._serialize,
435
+ info_arg=False,
436
+ ),
437
+ )
438
+
439
+ @staticmethod
440
+ def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any:
441
+ original_type: type[Any] = type(v)
442
+
443
+ # Normalize to list so list_schema can validate each item
444
+ if isinstance(v, list):
445
+ items: list[_T] = v
446
+ else:
447
+ try:
448
+ items = list(v)
449
+ except TypeError as e:
450
+ raise TypeError("Value is not iterable") from e
451
+
452
+ # Validate items against the inner schema
453
+ validated: list[_T] = handler(items)
454
+
455
+ # Reconstruct original container type
456
+ if original_type is list:
457
+ return validated
458
+ # str(list) produces the list's repr, not a string built from items,
459
+ # so skip reconstruction for str and its subclasses.
460
+ if issubclass(original_type, str):
461
+ return validated
462
+ try:
463
+ return original_type(validated)
464
+ except (TypeError, ValueError):
465
+ # If the type cannot be reconstructed, just return the validated list
466
+ return validated
467
+
468
+ @staticmethod
469
+ def _serialize(v: Iterable[_T]) -> list[_T]:
470
+ """Always serialize as a list so Pydantic's JSON encoder is happy."""
471
+ if isinstance(v, list):
472
+ return v
473
+ return list(v)
474
+
475
+
476
+ EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable]
477
+
478
+
399
479
  def _construct_field(value: object, field: FieldInfo, key: str) -> object:
400
480
  if value is None:
401
481
  return field_get_default(field)
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "growsurf"
4
- __version__ = "0.0.2" # x-release-please-version
4
+ __version__ = "0.1.0" # x-release-please-version
@@ -39,6 +39,7 @@ from ...types.campaign.participant_list_rewards_response import ParticipantListR
39
39
  from ...types.campaign.participant_send_invites_response import ParticipantSendInvitesResponse
40
40
  from ...types.campaign.participant_trigger_referral_response import ParticipantTriggerReferralResponse
41
41
  from ...types.campaign.participant_record_transaction_response import ParticipantRecordTransactionResponse
42
+ from ...types.campaign.participant_create_mobile_token_response import ParticipantCreateMobileTokenResponse
42
43
 
43
44
  __all__ = ["ParticipantResource", "AsyncParticipantResource"]
44
45
 
@@ -272,6 +273,50 @@ class ParticipantResource(SyncAPIResource):
272
273
  cast_to=Participant,
273
274
  )
274
275
 
276
+ def create_mobile_token(
277
+ self,
278
+ participant_id_or_email: str,
279
+ *,
280
+ id: str,
281
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
282
+ # The extra values given here take precedence over values defined on the client or passed to this method.
283
+ extra_headers: Headers | None = None,
284
+ extra_query: Query | None = None,
285
+ extra_body: Body | None = None,
286
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
287
+ ) -> ParticipantCreateMobileTokenResponse:
288
+ """Creates a participant-scoped token for GrowSurf mobile SDK participant
289
+ endpoints.
290
+
291
+ The program must have mobile SDK access enabled.
292
+
293
+ Args:
294
+ extra_headers: Send extra headers
295
+
296
+ extra_query: Add additional query parameters to the request
297
+
298
+ extra_body: Add additional JSON properties to the request
299
+
300
+ timeout: Override the client-level default timeout for this request, in seconds
301
+ """
302
+ if not id:
303
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
304
+ if not participant_id_or_email:
305
+ raise ValueError(
306
+ f"Expected a non-empty value for `participant_id_or_email` but received {participant_id_or_email!r}"
307
+ )
308
+ return self._post(
309
+ path_template(
310
+ "/campaign/{id}/participant/{participant_id_or_email}/mobile-token",
311
+ id=id,
312
+ participant_id_or_email=participant_id_or_email,
313
+ ),
314
+ options=make_request_options(
315
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
316
+ ),
317
+ cast_to=ParticipantCreateMobileTokenResponse,
318
+ )
319
+
275
320
  def list_commissions(
276
321
  self,
277
322
  participant_id_or_email: str,
@@ -966,6 +1011,50 @@ class AsyncParticipantResource(AsyncAPIResource):
966
1011
  cast_to=Participant,
967
1012
  )
968
1013
 
1014
+ async def create_mobile_token(
1015
+ self,
1016
+ participant_id_or_email: str,
1017
+ *,
1018
+ id: str,
1019
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
1020
+ # The extra values given here take precedence over values defined on the client or passed to this method.
1021
+ extra_headers: Headers | None = None,
1022
+ extra_query: Query | None = None,
1023
+ extra_body: Body | None = None,
1024
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
1025
+ ) -> ParticipantCreateMobileTokenResponse:
1026
+ """Creates a participant-scoped token for GrowSurf mobile SDK participant
1027
+ endpoints.
1028
+
1029
+ The program must have mobile SDK access enabled.
1030
+
1031
+ Args:
1032
+ extra_headers: Send extra headers
1033
+
1034
+ extra_query: Add additional query parameters to the request
1035
+
1036
+ extra_body: Add additional JSON properties to the request
1037
+
1038
+ timeout: Override the client-level default timeout for this request, in seconds
1039
+ """
1040
+ if not id:
1041
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
1042
+ if not participant_id_or_email:
1043
+ raise ValueError(
1044
+ f"Expected a non-empty value for `participant_id_or_email` but received {participant_id_or_email!r}"
1045
+ )
1046
+ return await self._post(
1047
+ path_template(
1048
+ "/campaign/{id}/participant/{participant_id_or_email}/mobile-token",
1049
+ id=id,
1050
+ participant_id_or_email=participant_id_or_email,
1051
+ ),
1052
+ options=make_request_options(
1053
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
1054
+ ),
1055
+ cast_to=ParticipantCreateMobileTokenResponse,
1056
+ )
1057
+
969
1058
  async def list_commissions(
970
1059
  self,
971
1060
  participant_id_or_email: str,
@@ -1447,6 +1536,9 @@ class ParticipantResourceWithRawResponse:
1447
1536
  self.add = to_raw_response_wrapper(
1448
1537
  participant.add,
1449
1538
  )
1539
+ self.create_mobile_token = to_raw_response_wrapper(
1540
+ participant.create_mobile_token,
1541
+ )
1450
1542
  self.list_commissions = to_raw_response_wrapper(
1451
1543
  participant.list_commissions,
1452
1544
  )
@@ -1486,6 +1578,9 @@ class AsyncParticipantResourceWithRawResponse:
1486
1578
  self.add = async_to_raw_response_wrapper(
1487
1579
  participant.add,
1488
1580
  )
1581
+ self.create_mobile_token = async_to_raw_response_wrapper(
1582
+ participant.create_mobile_token,
1583
+ )
1489
1584
  self.list_commissions = async_to_raw_response_wrapper(
1490
1585
  participant.list_commissions,
1491
1586
  )
@@ -1525,6 +1620,9 @@ class ParticipantResourceWithStreamingResponse:
1525
1620
  self.add = to_streamed_response_wrapper(
1526
1621
  participant.add,
1527
1622
  )
1623
+ self.create_mobile_token = to_streamed_response_wrapper(
1624
+ participant.create_mobile_token,
1625
+ )
1528
1626
  self.list_commissions = to_streamed_response_wrapper(
1529
1627
  participant.list_commissions,
1530
1628
  )
@@ -1564,6 +1662,9 @@ class AsyncParticipantResourceWithStreamingResponse:
1564
1662
  self.add = async_to_streamed_response_wrapper(
1565
1663
  participant.add,
1566
1664
  )
1665
+ self.create_mobile_token = async_to_streamed_response_wrapper(
1666
+ participant.create_mobile_token,
1667
+ )
1567
1668
  self.list_commissions = async_to_streamed_response_wrapper(
1568
1669
  participant.list_commissions,
1569
1670
  )
@@ -33,3 +33,6 @@ from .participant_trigger_referral_response import (
33
33
  from .participant_record_transaction_response import (
34
34
  ParticipantRecordTransactionResponse as ParticipantRecordTransactionResponse,
35
35
  )
36
+ from .participant_create_mobile_token_response import (
37
+ ParticipantCreateMobileTokenResponse as ParticipantCreateMobileTokenResponse,
38
+ )
@@ -0,0 +1,15 @@
1
+ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
+
3
+ from pydantic import Field as FieldInfo
4
+
5
+ from ..._models import BaseModel
6
+
7
+ __all__ = ["ParticipantCreateMobileTokenResponse"]
8
+
9
+
10
+ class ParticipantCreateMobileTokenResponse(BaseModel):
11
+ expires_in: int = FieldInfo(alias="expiresIn")
12
+ """Token lifetime in seconds."""
13
+
14
+ participant_token: str = FieldInfo(alias="participantToken")
15
+ """Participant-scoped bearer token for GrowSurf mobile SDK participant endpoints."""
@@ -16,6 +16,7 @@ from growsurf.types.campaign import (
16
16
  ParticipantListRewardsResponse,
17
17
  ParticipantSendInvitesResponse,
18
18
  ParticipantTriggerReferralResponse,
19
+ ParticipantCreateMobileTokenResponse,
19
20
  ParticipantRecordTransactionResponse,
20
21
  )
21
22
 
@@ -269,6 +270,60 @@ class TestParticipant:
269
270
  email="gavin@hooli.com",
270
271
  )
271
272
 
273
+ @pytest.mark.skip(reason="Mock server tests are disabled")
274
+ @parametrize
275
+ def test_method_create_mobile_token(self, client: Growsurf) -> None:
276
+ participant = client.campaign.participant.create_mobile_token(
277
+ participant_id_or_email="participantIdOrEmail",
278
+ id="id",
279
+ )
280
+ assert_matches_type(ParticipantCreateMobileTokenResponse, participant, path=["response"])
281
+
282
+ @pytest.mark.skip(reason="Mock server tests are disabled")
283
+ @parametrize
284
+ def test_raw_response_create_mobile_token(self, client: Growsurf) -> None:
285
+ response = client.campaign.participant.with_raw_response.create_mobile_token(
286
+ participant_id_or_email="participantIdOrEmail",
287
+ id="id",
288
+ )
289
+
290
+ assert response.is_closed is True
291
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
292
+ participant = response.parse()
293
+ assert_matches_type(ParticipantCreateMobileTokenResponse, participant, path=["response"])
294
+
295
+ @pytest.mark.skip(reason="Mock server tests are disabled")
296
+ @parametrize
297
+ def test_streaming_response_create_mobile_token(self, client: Growsurf) -> None:
298
+ with client.campaign.participant.with_streaming_response.create_mobile_token(
299
+ participant_id_or_email="participantIdOrEmail",
300
+ id="id",
301
+ ) as response:
302
+ assert not response.is_closed
303
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
304
+
305
+ participant = response.parse()
306
+ assert_matches_type(ParticipantCreateMobileTokenResponse, participant, path=["response"])
307
+
308
+ assert cast(Any, response.is_closed) is True
309
+
310
+ @pytest.mark.skip(reason="Mock server tests are disabled")
311
+ @parametrize
312
+ def test_path_params_create_mobile_token(self, client: Growsurf) -> None:
313
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
314
+ client.campaign.participant.with_raw_response.create_mobile_token(
315
+ participant_id_or_email="participantIdOrEmail",
316
+ id="",
317
+ )
318
+
319
+ with pytest.raises(
320
+ ValueError, match=r"Expected a non-empty value for `participant_id_or_email` but received ''"
321
+ ):
322
+ client.campaign.participant.with_raw_response.create_mobile_token(
323
+ participant_id_or_email="",
324
+ id="id",
325
+ )
326
+
272
327
  @pytest.mark.skip(reason="Mock server tests are disabled")
273
328
  @parametrize
274
329
  def test_method_list_commissions(self, client: Growsurf) -> None:
@@ -1007,6 +1062,60 @@ class TestAsyncParticipant:
1007
1062
  email="gavin@hooli.com",
1008
1063
  )
1009
1064
 
1065
+ @pytest.mark.skip(reason="Mock server tests are disabled")
1066
+ @parametrize
1067
+ async def test_method_create_mobile_token(self, async_client: AsyncGrowsurf) -> None:
1068
+ participant = await async_client.campaign.participant.create_mobile_token(
1069
+ participant_id_or_email="participantIdOrEmail",
1070
+ id="id",
1071
+ )
1072
+ assert_matches_type(ParticipantCreateMobileTokenResponse, participant, path=["response"])
1073
+
1074
+ @pytest.mark.skip(reason="Mock server tests are disabled")
1075
+ @parametrize
1076
+ async def test_raw_response_create_mobile_token(self, async_client: AsyncGrowsurf) -> None:
1077
+ response = await async_client.campaign.participant.with_raw_response.create_mobile_token(
1078
+ participant_id_or_email="participantIdOrEmail",
1079
+ id="id",
1080
+ )
1081
+
1082
+ assert response.is_closed is True
1083
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
1084
+ participant = await response.parse()
1085
+ assert_matches_type(ParticipantCreateMobileTokenResponse, participant, path=["response"])
1086
+
1087
+ @pytest.mark.skip(reason="Mock server tests are disabled")
1088
+ @parametrize
1089
+ async def test_streaming_response_create_mobile_token(self, async_client: AsyncGrowsurf) -> None:
1090
+ async with async_client.campaign.participant.with_streaming_response.create_mobile_token(
1091
+ participant_id_or_email="participantIdOrEmail",
1092
+ id="id",
1093
+ ) as response:
1094
+ assert not response.is_closed
1095
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
1096
+
1097
+ participant = await response.parse()
1098
+ assert_matches_type(ParticipantCreateMobileTokenResponse, participant, path=["response"])
1099
+
1100
+ assert cast(Any, response.is_closed) is True
1101
+
1102
+ @pytest.mark.skip(reason="Mock server tests are disabled")
1103
+ @parametrize
1104
+ async def test_path_params_create_mobile_token(self, async_client: AsyncGrowsurf) -> None:
1105
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
1106
+ await async_client.campaign.participant.with_raw_response.create_mobile_token(
1107
+ participant_id_or_email="participantIdOrEmail",
1108
+ id="",
1109
+ )
1110
+
1111
+ with pytest.raises(
1112
+ ValueError, match=r"Expected a non-empty value for `participant_id_or_email` but received ''"
1113
+ ):
1114
+ await async_client.campaign.participant.with_raw_response.create_mobile_token(
1115
+ participant_id_or_email="",
1116
+ id="id",
1117
+ )
1118
+
1010
1119
  @pytest.mark.skip(reason="Mock server tests are disabled")
1011
1120
  @parametrize
1012
1121
  async def test_method_list_commissions(self, async_client: AsyncGrowsurf) -> None:
@@ -1,7 +1,8 @@
1
1
  import json
2
- from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast
2
+ from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast
3
3
  from datetime import datetime, timezone
4
- from typing_extensions import Literal, Annotated, TypeAliasType
4
+ from collections import deque
5
+ from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType
5
6
 
6
7
  import pytest
7
8
  import pydantic
@@ -9,7 +10,7 @@ from pydantic import Field
9
10
 
10
11
  from growsurf._utils import PropertyInfo
11
12
  from growsurf._compat import PYDANTIC_V1, parse_obj, model_dump, model_json
12
- from growsurf._models import DISCRIMINATOR_CACHE, BaseModel, construct_type
13
+ from growsurf._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type
13
14
 
14
15
 
15
16
  class BasicModel(BaseModel):
@@ -961,3 +962,56 @@ def test_extra_properties() -> None:
961
962
  assert model.a.prop == 1
962
963
  assert isinstance(model.a, Item)
963
964
  assert model.other == "foo"
965
+
966
+
967
+ # NOTE: Workaround for Pydantic Iterable behavior.
968
+ # Iterable fields are replaced with a ValidatorIterator and may be consumed
969
+ # during serialization, which can cause subsequent dumps to return empty data.
970
+ # See: https://github.com/pydantic/pydantic/issues/9541
971
+ @pytest.mark.parametrize(
972
+ "data, expected_validated",
973
+ [
974
+ ([1, 2, 3], [1, 2, 3]),
975
+ ((1, 2, 3), (1, 2, 3)),
976
+ (set([1, 2, 3]), set([1, 2, 3])),
977
+ (iter([1, 2, 3]), [1, 2, 3]),
978
+ ([], []),
979
+ ((x for x in [1, 2, 3]), [1, 2, 3]),
980
+ (map(lambda x: x, [1, 2, 3]), [1, 2, 3]),
981
+ (frozenset([1, 2, 3]), frozenset([1, 2, 3])),
982
+ (deque([1, 2, 3]), deque([1, 2, 3])),
983
+ ],
984
+ ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"],
985
+ )
986
+ @pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2")
987
+ def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None:
988
+ class TypeWithIterable(TypedDict):
989
+ items: EagerIterable[int]
990
+
991
+ class Model(BaseModel):
992
+ data: TypeWithIterable
993
+
994
+ m = Model.model_validate({"data": {"items": data}})
995
+ assert m.data["items"] == expected_validated
996
+
997
+ # Verify repeated dumps don't lose data (the original bug)
998
+ assert m.model_dump()["data"]["items"] == list(expected_validated)
999
+ assert m.model_dump()["data"]["items"] == list(expected_validated)
1000
+
1001
+
1002
+ @pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2")
1003
+ def test_iterable_construction_str_falls_back_to_list() -> None:
1004
+ # str is iterable (over chars), but str(list_of_chars) produces the list's repr
1005
+ # rather than reconstructing a string from items. We special-case str to fall
1006
+ # back to list instead of attempting reconstruction.
1007
+ class TypeWithIterable(TypedDict):
1008
+ items: EagerIterable[str]
1009
+
1010
+ class Model(BaseModel):
1011
+ data: TypeWithIterable
1012
+
1013
+ m = Model.model_validate({"data": {"items": "hello"}})
1014
+
1015
+ # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"])
1016
+ assert m.data["items"] == ["h", "e", "l", "l", "o"]
1017
+ assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"]
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.0.2"
3
- }
@@ -1,11 +0,0 @@
1
- #!/usr/bin/env bash
2
-
3
- set -eux
4
- rm -rf dist
5
- mkdir -p dist
6
- uv build
7
- if [ -n "${PYPI_TOKEN:-}" ]; then
8
- uv publish --token=$PYPI_TOKEN
9
- else
10
- uv publish
11
- fi
File without changes
File without changes