browserbase 1.8.0__tar.gz → 1.9.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 (144) hide show
  1. browserbase-1.9.0/.release-please-manifest.json +3 -0
  2. {browserbase-1.8.0 → browserbase-1.9.0}/CHANGELOG.md +32 -0
  3. {browserbase-1.8.0 → browserbase-1.9.0}/PKG-INFO +1 -1
  4. {browserbase-1.8.0 → browserbase-1.9.0}/api.md +13 -0
  5. {browserbase-1.8.0 → browserbase-1.9.0}/pyproject.toml +2 -2
  6. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_base_client.py +4 -0
  7. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_client.py +23 -1
  8. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_files.py +54 -4
  9. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_models.py +80 -0
  10. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_qs.py +2 -6
  11. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_types.py +3 -0
  12. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_utils/__init__.py +0 -1
  13. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_utils/_utils.py +37 -25
  14. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_version.py +1 -1
  15. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/resources/extensions.py +4 -3
  16. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/resources/sessions/__init__.py +14 -0
  17. browserbase-1.9.0/src/browserbase/resources/sessions/replays.py +266 -0
  18. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/resources/sessions/sessions.py +34 -2
  19. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/resources/sessions/uploads.py +4 -3
  20. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/session.py +1 -1
  21. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/session_create_params.py +6 -0
  22. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/session_list_params.py +1 -1
  23. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/sessions/__init__.py +1 -0
  24. browserbase-1.9.0/src/browserbase/types/sessions/replay_retrieve_response.py +25 -0
  25. browserbase-1.9.0/tests/api_resources/sessions/test_replays.py +242 -0
  26. {browserbase-1.8.0 → browserbase-1.9.0}/tests/api_resources/test_sessions.py +4 -2
  27. {browserbase-1.8.0 → browserbase-1.9.0}/tests/test_client.py +48 -0
  28. {browserbase-1.8.0 → browserbase-1.9.0}/tests/test_extract_files.py +28 -1
  29. browserbase-1.9.0/tests/test_files.py +148 -0
  30. {browserbase-1.8.0 → browserbase-1.9.0}/tests/test_models.py +57 -3
  31. browserbase-1.8.0/.release-please-manifest.json +0 -3
  32. browserbase-1.8.0/tests/test_deepcopy.py +0 -58
  33. browserbase-1.8.0/tests/test_files.py +0 -51
  34. {browserbase-1.8.0 → browserbase-1.9.0}/.gitignore +0 -0
  35. {browserbase-1.8.0 → browserbase-1.9.0}/CONTRIBUTING.md +0 -0
  36. {browserbase-1.8.0 → browserbase-1.9.0}/LICENSE +0 -0
  37. {browserbase-1.8.0 → browserbase-1.9.0}/MIGRATION.md +0 -0
  38. {browserbase-1.8.0 → browserbase-1.9.0}/README.md +0 -0
  39. {browserbase-1.8.0 → browserbase-1.9.0}/SECURITY.md +0 -0
  40. {browserbase-1.8.0 → browserbase-1.9.0}/bin/check-release-environment +0 -0
  41. {browserbase-1.8.0 → browserbase-1.9.0}/bin/publish-pypi +0 -0
  42. {browserbase-1.8.0 → browserbase-1.9.0}/examples/.keep +0 -0
  43. {browserbase-1.8.0 → browserbase-1.9.0}/examples/__init__.py +0 -0
  44. {browserbase-1.8.0 → browserbase-1.9.0}/examples/e2e/README.md +0 -0
  45. {browserbase-1.8.0 → browserbase-1.9.0}/examples/e2e/__init__.py +0 -0
  46. {browserbase-1.8.0 → browserbase-1.9.0}/examples/e2e/test_playwright.py +0 -0
  47. {browserbase-1.8.0 → browserbase-1.9.0}/examples/e2e/test_selenium.py +0 -0
  48. {browserbase-1.8.0 → browserbase-1.9.0}/examples/packages/extensions/.gitignore +0 -0
  49. {browserbase-1.8.0 → browserbase-1.9.0}/examples/packages/extensions/browserbase-test/hello.html +0 -0
  50. {browserbase-1.8.0 → browserbase-1.9.0}/examples/packages/extensions/browserbase-test/images/logo.png +0 -0
  51. {browserbase-1.8.0 → browserbase-1.9.0}/examples/packages/extensions/browserbase-test/manifest.json +0 -0
  52. {browserbase-1.8.0 → browserbase-1.9.0}/examples/packages/extensions/browserbase-test/scripts/content.js +0 -0
  53. {browserbase-1.8.0 → browserbase-1.9.0}/examples/packages/logo.png +0 -0
  54. {browserbase-1.8.0 → browserbase-1.9.0}/examples/playwright_basic.py +0 -0
  55. {browserbase-1.8.0 → browserbase-1.9.0}/examples/playwright_captcha.py +0 -0
  56. {browserbase-1.8.0 → browserbase-1.9.0}/examples/playwright_contexts.py +0 -0
  57. {browserbase-1.8.0 → browserbase-1.9.0}/examples/playwright_downloads.py +0 -0
  58. {browserbase-1.8.0 → browserbase-1.9.0}/examples/playwright_extensions.py +0 -0
  59. {browserbase-1.8.0 → browserbase-1.9.0}/examples/playwright_proxy.py +0 -0
  60. {browserbase-1.8.0 → browserbase-1.9.0}/examples/playwright_upload.py +0 -0
  61. {browserbase-1.8.0 → browserbase-1.9.0}/examples/selenium_basic.py +0 -0
  62. {browserbase-1.8.0 → browserbase-1.9.0}/noxfile.py +0 -0
  63. {browserbase-1.8.0 → browserbase-1.9.0}/release-please-config.json +0 -0
  64. {browserbase-1.8.0 → browserbase-1.9.0}/requirements-dev.lock +0 -0
  65. {browserbase-1.8.0 → browserbase-1.9.0}/requirements.lock +0 -0
  66. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/__init__.py +0 -0
  67. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_compat.py +0 -0
  68. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_constants.py +0 -0
  69. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_exceptions.py +0 -0
  70. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_resource.py +0 -0
  71. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_response.py +0 -0
  72. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_streaming.py +0 -0
  73. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_utils/_compat.py +0 -0
  74. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_utils/_datetime_parse.py +0 -0
  75. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_utils/_json.py +0 -0
  76. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_utils/_logs.py +0 -0
  77. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_utils/_path.py +0 -0
  78. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_utils/_proxy.py +0 -0
  79. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_utils/_reflection.py +0 -0
  80. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_utils/_resources_proxy.py +0 -0
  81. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_utils/_streams.py +0 -0
  82. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_utils/_sync.py +0 -0
  83. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_utils/_transform.py +0 -0
  84. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/_utils/_typing.py +0 -0
  85. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/lib/.keep +0 -0
  86. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/py.typed +0 -0
  87. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/resources/__init__.py +0 -0
  88. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/resources/contexts.py +0 -0
  89. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/resources/fetch_api.py +0 -0
  90. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/resources/projects.py +0 -0
  91. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/resources/search.py +0 -0
  92. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/resources/sessions/downloads.py +0 -0
  93. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/resources/sessions/logs.py +0 -0
  94. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/resources/sessions/recording.py +0 -0
  95. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/__init__.py +0 -0
  96. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/context.py +0 -0
  97. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/context_create_params.py +0 -0
  98. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/context_create_response.py +0 -0
  99. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/context_update_response.py +0 -0
  100. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/extension.py +0 -0
  101. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/extension_create_params.py +0 -0
  102. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/fetch_api_create_params.py +0 -0
  103. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/fetch_api_create_response.py +0 -0
  104. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/project.py +0 -0
  105. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/project_list_response.py +0 -0
  106. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/project_usage.py +0 -0
  107. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/search_web_params.py +0 -0
  108. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/search_web_response.py +0 -0
  109. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/session_create_response.py +0 -0
  110. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/session_list_response.py +0 -0
  111. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/session_live_urls.py +0 -0
  112. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/session_retrieve_response.py +0 -0
  113. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/session_update_params.py +0 -0
  114. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/sessions/log_list_response.py +0 -0
  115. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/sessions/recording_retrieve_response.py +0 -0
  116. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/sessions/session_log.py +0 -0
  117. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/sessions/session_recording.py +0 -0
  118. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/sessions/upload_create_params.py +0 -0
  119. {browserbase-1.8.0 → browserbase-1.9.0}/src/browserbase/types/sessions/upload_create_response.py +0 -0
  120. {browserbase-1.8.0 → browserbase-1.9.0}/tests/__init__.py +0 -0
  121. {browserbase-1.8.0 → browserbase-1.9.0}/tests/api_resources/__init__.py +0 -0
  122. {browserbase-1.8.0 → browserbase-1.9.0}/tests/api_resources/sessions/__init__.py +0 -0
  123. {browserbase-1.8.0 → browserbase-1.9.0}/tests/api_resources/sessions/test_downloads.py +0 -0
  124. {browserbase-1.8.0 → browserbase-1.9.0}/tests/api_resources/sessions/test_logs.py +0 -0
  125. {browserbase-1.8.0 → browserbase-1.9.0}/tests/api_resources/sessions/test_recording.py +0 -0
  126. {browserbase-1.8.0 → browserbase-1.9.0}/tests/api_resources/sessions/test_uploads.py +0 -0
  127. {browserbase-1.8.0 → browserbase-1.9.0}/tests/api_resources/test_contexts.py +0 -0
  128. {browserbase-1.8.0 → browserbase-1.9.0}/tests/api_resources/test_extensions.py +0 -0
  129. {browserbase-1.8.0 → browserbase-1.9.0}/tests/api_resources/test_fetch_api.py +0 -0
  130. {browserbase-1.8.0 → browserbase-1.9.0}/tests/api_resources/test_projects.py +0 -0
  131. {browserbase-1.8.0 → browserbase-1.9.0}/tests/api_resources/test_search.py +0 -0
  132. {browserbase-1.8.0 → browserbase-1.9.0}/tests/conftest.py +0 -0
  133. {browserbase-1.8.0 → browserbase-1.9.0}/tests/sample_file.txt +0 -0
  134. {browserbase-1.8.0 → browserbase-1.9.0}/tests/test_qs.py +0 -0
  135. {browserbase-1.8.0 → browserbase-1.9.0}/tests/test_required_args.py +0 -0
  136. {browserbase-1.8.0 → browserbase-1.9.0}/tests/test_response.py +0 -0
  137. {browserbase-1.8.0 → browserbase-1.9.0}/tests/test_streaming.py +0 -0
  138. {browserbase-1.8.0 → browserbase-1.9.0}/tests/test_transform.py +0 -0
  139. {browserbase-1.8.0 → browserbase-1.9.0}/tests/test_utils/test_datetime_parse.py +0 -0
  140. {browserbase-1.8.0 → browserbase-1.9.0}/tests/test_utils/test_json.py +0 -0
  141. {browserbase-1.8.0 → browserbase-1.9.0}/tests/test_utils/test_path.py +0 -0
  142. {browserbase-1.8.0 → browserbase-1.9.0}/tests/test_utils/test_proxy.py +0 -0
  143. {browserbase-1.8.0 → browserbase-1.9.0}/tests/test_utils/test_typing.py +0 -0
  144. {browserbase-1.8.0 → browserbase-1.9.0}/tests/utils.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "1.9.0"
3
+ }
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.0 (2026-05-13)
4
+
5
+ Full Changelog: [v1.8.0...v1.9.0](https://github.com/browserbase/sdk-python/compare/v1.8.0...v1.9.0)
6
+
7
+ ### Features
8
+
9
+ * [CORE-1928][apps/api] Add `PENDING` as a valid session state ([4f1248d](https://github.com/browserbase/sdk-python/commit/4f1248dfb1bf79194f65854a6fcd6a0d53433ba1))
10
+ * [CORE-1979] [apps/api] Regenerate OpenAPI spec to match current routes ([0e366a6](https://github.com/browserbase/sdk-python/commit/0e366a6a0a01e2ce8e661d73f9fa312e56d1a582))
11
+ * **api:** add replays ([58e18df](https://github.com/browserbase/sdk-python/commit/58e18df7d7b0376234a322a197584a7163eba4b4))
12
+ * **internal/types:** support eagerly validating pydantic iterators ([9d56949](https://github.com/browserbase/sdk-python/commit/9d569494a2050437404866315561e341f8e38a92))
13
+ * support setting headers via env ([308d35e](https://github.com/browserbase/sdk-python/commit/308d35edb58454e542fcf58f379061fb742bd83b))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * **client:** add missing f-string prefix in file type error message ([e01f048](https://github.com/browserbase/sdk-python/commit/e01f0484313315bb9d99338b8486054d53f2b46b))
19
+ * **client:** preserve hardcoded query params when merging with user params ([953fd3e](https://github.com/browserbase/sdk-python/commit/953fd3ecd54a7ffc2ee390eac67f2063df89b8e9))
20
+ * ensure file data are only sent as 1 parameter ([a837357](https://github.com/browserbase/sdk-python/commit/a83735708037eec6cb4807e4220ca7452b5b6503))
21
+ * use correct field name format for multipart file arrays ([9488fb3](https://github.com/browserbase/sdk-python/commit/9488fb39a656b0aaaf740ccec0a9dce57b996b02))
22
+
23
+
24
+ ### Performance Improvements
25
+
26
+ * **client:** optimize file structure copying in multipart requests ([4146f22](https://github.com/browserbase/sdk-python/commit/4146f22bb6c054e7491e4c10021dff3e9e2c8824))
27
+
28
+
29
+ ### Chores
30
+
31
+ * **internal:** more robust bootstrap script ([83d1f68](https://github.com/browserbase/sdk-python/commit/83d1f686936e4593c3017c719056573089bbd1e0))
32
+ * **internal:** reformat pyproject.toml ([979436a](https://github.com/browserbase/sdk-python/commit/979436a2cb86944f8bf400a2e623e946188d87c1))
33
+ * **tests:** bump steady to v0.22.1 ([bafb680](https://github.com/browserbase/sdk-python/commit/bafb68055a4f36ef6b7d6a308606db9e8d33257b))
34
+
3
35
  ## 1.8.0 (2026-04-06)
4
36
 
5
37
  Full Changelog: [v1.7.0...v1.8.0](https://github.com/browserbase/sdk-python/compare/v1.7.0...v1.8.0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: browserbase
3
- Version: 1.8.0
3
+ Version: 1.9.0
4
4
  Summary: The official Python library for the Browserbase API
5
5
  Project-URL: Homepage, https://github.com/browserbase/sdk-python
6
6
  Project-URL: Repository, https://github.com/browserbase/sdk-python
@@ -128,3 +128,16 @@ from browserbase.types.sessions import UploadCreateResponse
128
128
  Methods:
129
129
 
130
130
  - <code title="post /v1/sessions/{id}/uploads">client.sessions.uploads.<a href="./src/browserbase/resources/sessions/uploads.py">create</a>(id, \*\*<a href="src/browserbase/types/sessions/upload_create_params.py">params</a>) -> <a href="./src/browserbase/types/sessions/upload_create_response.py">UploadCreateResponse</a></code>
131
+
132
+ ## Replays
133
+
134
+ Types:
135
+
136
+ ```python
137
+ from browserbase.types.sessions import ReplayRetrieveResponse
138
+ ```
139
+
140
+ Methods:
141
+
142
+ - <code title="get /v1/sessions/{id}/replays">client.sessions.replays.<a href="./src/browserbase/resources/sessions/replays.py">retrieve</a>(id) -> <a href="./src/browserbase/types/sessions/replay_retrieve_response.py">ReplayRetrieveResponse</a></code>
143
+ - <code title="get /v1/sessions/{id}/replays/{pageId}">client.sessions.replays.<a href="./src/browserbase/resources/sessions/replays.py">retrieve_page</a>(page_id, \*, id) -> BinaryAPIResponse</code>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "browserbase"
3
- version = "1.8.0"
3
+ version = "1.9.0"
4
4
  description = "The official Python library for the Browserbase API"
5
5
  dynamic = ["readme"]
6
6
  license = "Apache-2.0"
@@ -177,7 +177,7 @@ show_error_codes = true
177
177
  #
178
178
  # We also exclude our `tests` as mypy doesn't always infer
179
179
  # types correctly and Pyright will still catch any type errors.
180
- exclude = ['src/browserbase/_files.py', '_dev/.*.py', 'tests/.*']
180
+ exclude = ["src/browserbase/_files.py", "_dev/.*.py", "tests/.*"]
181
181
 
182
182
  strict_equality = true
183
183
  implicit_reexport = true
@@ -540,6 +540,10 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
540
540
  files = cast(HttpxRequestFiles, ForceMultipartDict())
541
541
 
542
542
  prepared_url = self._prepare_url(options.url)
543
+ # preserve hard-coded query params from the url
544
+ if params and prepared_url.query:
545
+ params = {**dict(prepared_url.params.items()), **params}
546
+ prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0])
543
547
  if "_" in prepared_url.host:
544
548
  # work around https://github.com/encode/httpx/discussions/2880
545
549
  kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")}
@@ -19,7 +19,11 @@ from ._types import (
19
19
  RequestOptions,
20
20
  not_given,
21
21
  )
22
- from ._utils import is_given, get_async_library
22
+ from ._utils import (
23
+ is_given,
24
+ is_mapping_t,
25
+ get_async_library,
26
+ )
23
27
  from ._compat import cached_property
24
28
  from ._version import __version__
25
29
  from ._streaming import Stream as Stream, AsyncStream as AsyncStream
@@ -95,6 +99,15 @@ class Browserbase(SyncAPIClient):
95
99
  if base_url is None:
96
100
  base_url = f"https://api.browserbase.com"
97
101
 
102
+ custom_headers_env = os.environ.get("BROWSERBASE_CUSTOM_HEADERS")
103
+ if custom_headers_env is not None:
104
+ parsed: dict[str, str] = {}
105
+ for line in custom_headers_env.split("\n"):
106
+ colon = line.find(":")
107
+ if colon >= 0:
108
+ parsed[line[:colon].strip()] = line[colon + 1 :].strip()
109
+ default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})}
110
+
98
111
  super().__init__(
99
112
  version=__version__,
100
113
  base_url=base_url,
@@ -299,6 +312,15 @@ class AsyncBrowserbase(AsyncAPIClient):
299
312
  if base_url is None:
300
313
  base_url = f"https://api.browserbase.com"
301
314
 
315
+ custom_headers_env = os.environ.get("BROWSERBASE_CUSTOM_HEADERS")
316
+ if custom_headers_env is not None:
317
+ parsed: dict[str, str] = {}
318
+ for line in custom_headers_env.split("\n"):
319
+ colon = line.find(":")
320
+ if colon >= 0:
321
+ parsed[line[:colon].strip()] = line[colon + 1 :].strip()
322
+ default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})}
323
+
302
324
  super().__init__(
303
325
  version=__version__,
304
326
  base_url=base_url,
@@ -3,8 +3,8 @@ from __future__ import annotations
3
3
  import io
4
4
  import os
5
5
  import pathlib
6
- from typing import overload
7
- from typing_extensions import TypeGuard
6
+ from typing import Sequence, cast, overload
7
+ from typing_extensions import TypeVar, TypeGuard
8
8
 
9
9
  import anyio
10
10
 
@@ -17,7 +17,9 @@ from ._types import (
17
17
  HttpxFileContent,
18
18
  HttpxRequestFiles,
19
19
  )
20
- from ._utils import is_tuple_t, is_mapping_t, is_sequence_t
20
+ from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t
21
+
22
+ _T = TypeVar("_T")
21
23
 
22
24
 
23
25
  def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]:
@@ -97,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles
97
99
  elif is_sequence_t(files):
98
100
  files = [(key, await _async_transform_file(file)) for key, file in files]
99
101
  else:
100
- 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")
101
103
 
102
104
  return files
103
105
 
@@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent:
121
123
  return await anyio.Path(file).read_bytes()
122
124
 
123
125
  return file
126
+
127
+
128
+ def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T:
129
+ """Copy only the containers along the given paths.
130
+
131
+ Used to guard against mutation by extract_files without copying the entire structure.
132
+ Only dicts and lists that lie on a path are copied; everything else
133
+ is returned by reference.
134
+
135
+ For example, given paths=[["foo", "files", "file"]] and the structure:
136
+ {
137
+ "foo": {
138
+ "bar": {"baz": {}},
139
+ "files": {"file": <content>}
140
+ }
141
+ }
142
+ The root dict, "foo", and "files" are copied (they lie on the path).
143
+ "bar" and "baz" are returned by reference (off the path).
144
+ """
145
+ return _deepcopy_with_paths(item, paths, 0)
146
+
147
+
148
+ def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T:
149
+ if not paths:
150
+ return item
151
+ if is_mapping(item):
152
+ key_to_paths: dict[str, list[Sequence[str]]] = {}
153
+ for path in paths:
154
+ if index < len(path):
155
+ key_to_paths.setdefault(path[index], []).append(path)
156
+
157
+ # if no path continues through this mapping, it won't be mutated and copying it is redundant
158
+ if not key_to_paths:
159
+ return item
160
+
161
+ result = dict(item)
162
+ for key, subpaths in key_to_paths.items():
163
+ if key in result:
164
+ result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1)
165
+ return cast(_T, result)
166
+ if is_list(item):
167
+ array_paths = [path for path in paths if index < len(path) and path[index] == "<array>"]
168
+
169
+ # if no path expects a list here, nothing will be mutated inside it - return by reference
170
+ if not array_paths:
171
+ return cast(_T, item)
172
+ return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item])
173
+ return item
@@ -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)
@@ -2,17 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any, List, Tuple, Union, Mapping, TypeVar
4
4
  from urllib.parse import parse_qs, urlencode
5
- from typing_extensions import Literal, get_args
5
+ from typing_extensions import get_args
6
6
 
7
- from ._types import NotGiven, not_given
7
+ from ._types import NotGiven, ArrayFormat, NestedFormat, not_given
8
8
  from ._utils import flatten
9
9
 
10
10
  _T = TypeVar("_T")
11
11
 
12
-
13
- ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
14
- NestedFormat = Literal["dots", "brackets"]
15
-
16
12
  PrimitiveData = Union[str, int, float, bool, None]
17
13
  # this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"]
18
14
  # https://github.com/microsoft/pyright/issues/3555
@@ -47,6 +47,9 @@ AnyMapping = Mapping[str, object]
47
47
  ModelT = TypeVar("ModelT", bound=pydantic.BaseModel)
48
48
  _T = TypeVar("_T")
49
49
 
50
+ ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
51
+ NestedFormat = Literal["dots", "brackets"]
52
+
50
53
 
51
54
  # Approximates httpx internal ProxiesTypes and RequestFiles types
52
55
  # while adding support for `PathLike` instances
@@ -24,7 +24,6 @@ from ._utils import (
24
24
  coerce_integer as coerce_integer,
25
25
  file_from_path as file_from_path,
26
26
  strip_not_given as strip_not_given,
27
- deepcopy_minimal as deepcopy_minimal,
28
27
  get_async_library as get_async_library,
29
28
  maybe_coerce_float as maybe_coerce_float,
30
29
  get_required_header as get_required_header,
@@ -17,11 +17,11 @@ from typing import (
17
17
  )
18
18
  from pathlib import Path
19
19
  from datetime import date, datetime
20
- from typing_extensions import TypeGuard
20
+ from typing_extensions import TypeGuard, get_args
21
21
 
22
22
  import sniffio
23
23
 
24
- from .._types import Omit, NotGiven, FileTypes, HeadersLike
24
+ from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike
25
25
 
26
26
  _T = TypeVar("_T")
27
27
  _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...])
@@ -40,25 +40,45 @@ def extract_files(
40
40
  query: Mapping[str, object],
41
41
  *,
42
42
  paths: Sequence[Sequence[str]],
43
+ array_format: ArrayFormat = "brackets",
43
44
  ) -> list[tuple[str, FileTypes]]:
44
45
  """Recursively extract files from the given dictionary based on specified paths.
45
46
 
46
47
  A path may look like this ['foo', 'files', '<array>', 'data'].
47
48
 
49
+ ``array_format`` controls how ``<array>`` segments contribute to the emitted
50
+ field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and
51
+ ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``).
52
+
48
53
  Note: this mutates the given dictionary.
49
54
  """
50
55
  files: list[tuple[str, FileTypes]] = []
51
56
  for path in paths:
52
- files.extend(_extract_items(query, path, index=0, flattened_key=None))
57
+ files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format))
53
58
  return files
54
59
 
55
60
 
61
+ def _array_suffix(array_format: ArrayFormat, array_index: int) -> str:
62
+ if array_format == "brackets":
63
+ return "[]"
64
+ if array_format == "indices":
65
+ return f"[{array_index}]"
66
+ if array_format == "repeat" or array_format == "comma":
67
+ # Both repeat the bare field name for each file part; there is no
68
+ # meaningful way to comma-join binary parts.
69
+ return ""
70
+ raise NotImplementedError(
71
+ f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}"
72
+ )
73
+
74
+
56
75
  def _extract_items(
57
76
  obj: object,
58
77
  path: Sequence[str],
59
78
  *,
60
79
  index: int,
61
80
  flattened_key: str | None,
81
+ array_format: ArrayFormat,
62
82
  ) -> list[tuple[str, FileTypes]]:
63
83
  try:
64
84
  key = path[index]
@@ -75,9 +95,11 @@ def _extract_items(
75
95
 
76
96
  if is_list(obj):
77
97
  files: list[tuple[str, FileTypes]] = []
78
- for entry in obj:
79
- assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "")
80
- files.append((flattened_key + "[]", cast(FileTypes, entry)))
98
+ for array_index, entry in enumerate(obj):
99
+ suffix = _array_suffix(array_format, array_index)
100
+ emitted_key = (flattened_key + suffix) if flattened_key else suffix
101
+ assert_is_file_content(entry, key=emitted_key)
102
+ files.append((emitted_key, cast(FileTypes, entry)))
81
103
  return files
82
104
 
83
105
  assert_is_file_content(obj, key=flattened_key)
@@ -86,8 +108,9 @@ def _extract_items(
86
108
  index += 1
87
109
  if is_dict(obj):
88
110
  try:
89
- # We are at the last entry in the path so we must remove the field
90
- if (len(path)) == index:
111
+ # Remove the field if there are no more dict keys in the path,
112
+ # only "<array>" traversal markers or end.
113
+ if all(p == "<array>" for p in path[index:]):
91
114
  item = obj.pop(key)
92
115
  else:
93
116
  item = obj[key]
@@ -105,6 +128,7 @@ def _extract_items(
105
128
  path,
106
129
  index=index,
107
130
  flattened_key=flattened_key,
131
+ array_format=array_format,
108
132
  )
109
133
  elif is_list(obj):
110
134
  if key != "<array>":
@@ -116,9 +140,12 @@ def _extract_items(
116
140
  item,
117
141
  path,
118
142
  index=index,
119
- flattened_key=flattened_key + "[]" if flattened_key is not None else "[]",
143
+ flattened_key=(
144
+ (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index)
145
+ ),
146
+ array_format=array_format,
120
147
  )
121
- for item in obj
148
+ for array_index, item in enumerate(obj)
122
149
  ]
123
150
  )
124
151
 
@@ -176,21 +203,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]:
176
203
  return isinstance(obj, Iterable)
177
204
 
178
205
 
179
- def deepcopy_minimal(item: _T) -> _T:
180
- """Minimal reimplementation of copy.deepcopy() that will only copy certain object types:
181
-
182
- - mappings, e.g. `dict`
183
- - list
184
-
185
- This is done for performance reasons.
186
- """
187
- if is_mapping(item):
188
- return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()})
189
- if is_list(item):
190
- return cast(_T, [deepcopy_minimal(entry) for entry in item])
191
- return item
192
-
193
-
194
206
  # copied from https://github.com/Rapptz/RoboDanny
195
207
  def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str:
196
208
  size = len(seq)
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "browserbase"
4
- __version__ = "1.8.0" # x-release-please-version
4
+ __version__ = "1.9.0" # x-release-please-version
@@ -7,8 +7,9 @@ from typing import Mapping, cast
7
7
  import httpx
8
8
 
9
9
  from ..types import extension_create_params
10
+ from .._files import deepcopy_with_paths
10
11
  from .._types import Body, Query, Headers, NoneType, NotGiven, FileTypes, not_given
11
- from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform
12
+ from .._utils import extract_files, path_template, maybe_transform, async_maybe_transform
12
13
  from .._compat import cached_property
13
14
  from .._resource import SyncAPIResource, AsyncAPIResource
14
15
  from .._response import (
@@ -66,7 +67,7 @@ class ExtensionsResource(SyncAPIResource):
66
67
 
67
68
  timeout: Override the client-level default timeout for this request, in seconds
68
69
  """
69
- body = deepcopy_minimal({"file": file})
70
+ body = deepcopy_with_paths({"file": file}, [["file"]])
70
71
  files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
71
72
  # It should be noted that the actual Content-Type header that will be
72
73
  # sent to the server will contain a `boundary` parameter, e.g.
@@ -193,7 +194,7 @@ class AsyncExtensionsResource(AsyncAPIResource):
193
194
 
194
195
  timeout: Override the client-level default timeout for this request, in seconds
195
196
  """
196
- body = deepcopy_minimal({"file": file})
197
+ body = deepcopy_with_paths({"file": file}, [["file"]])
197
198
  files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
198
199
  # It should be noted that the actual Content-Type header that will be
199
200
  # sent to the server will contain a `boundary` parameter, e.g.
@@ -8,6 +8,14 @@ from .logs import (
8
8
  LogsResourceWithStreamingResponse,
9
9
  AsyncLogsResourceWithStreamingResponse,
10
10
  )
11
+ from .replays import (
12
+ ReplaysResource,
13
+ AsyncReplaysResource,
14
+ ReplaysResourceWithRawResponse,
15
+ AsyncReplaysResourceWithRawResponse,
16
+ ReplaysResourceWithStreamingResponse,
17
+ AsyncReplaysResourceWithStreamingResponse,
18
+ )
11
19
  from .uploads import (
12
20
  UploadsResource,
13
21
  AsyncUploadsResource,
@@ -66,6 +74,12 @@ __all__ = [
66
74
  "AsyncUploadsResourceWithRawResponse",
67
75
  "UploadsResourceWithStreamingResponse",
68
76
  "AsyncUploadsResourceWithStreamingResponse",
77
+ "ReplaysResource",
78
+ "AsyncReplaysResource",
79
+ "ReplaysResourceWithRawResponse",
80
+ "AsyncReplaysResourceWithRawResponse",
81
+ "ReplaysResourceWithStreamingResponse",
82
+ "AsyncReplaysResourceWithStreamingResponse",
69
83
  "SessionsResource",
70
84
  "AsyncSessionsResource",
71
85
  "SessionsResourceWithRawResponse",