pyUSPTO 0.4.4__tar.gz → 0.4.5__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. {pyuspto-0.4.4 → pyuspto-0.4.5}/PKG-INFO +1 -1
  2. {pyuspto-0.4.4 → pyuspto-0.4.5}/requirements-dev.txt +1 -5
  3. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/clients/base.py +144 -64
  4. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/clients/bulk_data.py +4 -9
  5. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/clients/patent_data.py +29 -38
  6. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/clients/petition_decisions.py +13 -16
  7. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/clients/ptab_appeals.py +4 -6
  8. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/clients/ptab_interferences.py +4 -6
  9. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/clients/ptab_trials.py +10 -12
  10. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/models/patent_data.py +9 -3
  11. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/models/petition_decisions.py +6 -3
  12. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO.egg-info/PKG-INFO +1 -1
  13. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/clients/test_base.py +87 -50
  14. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/clients/test_bulk_data_clients.py +3 -3
  15. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/clients/test_patent_data_clients.py +327 -280
  16. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/clients/test_petition_decision_clients.py +130 -112
  17. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/integration/test_patent_data_integration.py +15 -15
  18. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/integration/test_petition_decisions_integration.py +23 -23
  19. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/integration/test_ptab_appeals_integration.py +18 -14
  20. {pyuspto-0.4.4 → pyuspto-0.4.5}/.github/ISSUE_TEMPLATE/bug-report.yml +0 -0
  21. {pyuspto-0.4.4 → pyuspto-0.4.5}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  22. {pyuspto-0.4.4 → pyuspto-0.4.5}/.github/ISSUE_TEMPLATE/documentation-updates.yml +0 -0
  23. {pyuspto-0.4.4 → pyuspto-0.4.5}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
  24. {pyuspto-0.4.4 → pyuspto-0.4.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  25. {pyuspto-0.4.4 → pyuspto-0.4.5}/.github/dependabot.yml +0 -0
  26. {pyuspto-0.4.4 → pyuspto-0.4.5}/.github/workflows/publish-docs.yaml +0 -0
  27. {pyuspto-0.4.4 → pyuspto-0.4.5}/.github/workflows/publish-to-test-pypi.yml +0 -0
  28. {pyuspto-0.4.4 → pyuspto-0.4.5}/.github/workflows/python-tests.yml +0 -0
  29. {pyuspto-0.4.4 → pyuspto-0.4.5}/.gitignore +0 -0
  30. {pyuspto-0.4.4 → pyuspto-0.4.5}/.readthedocs.yaml +0 -0
  31. {pyuspto-0.4.4 → pyuspto-0.4.5}/ADVANCED.md +0 -0
  32. {pyuspto-0.4.4 → pyuspto-0.4.5}/CHANGELOG.md +0 -0
  33. {pyuspto-0.4.4 → pyuspto-0.4.5}/CODE_OF_CONDUCT.md +0 -0
  34. {pyuspto-0.4.4 → pyuspto-0.4.5}/CONTRIBUTING.md +0 -0
  35. {pyuspto-0.4.4 → pyuspto-0.4.5}/LICENSE +0 -0
  36. {pyuspto-0.4.4 → pyuspto-0.4.5}/README.md +0 -0
  37. {pyuspto-0.4.4 → pyuspto-0.4.5}/_version.pyi +0 -0
  38. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/Makefile +0 -0
  39. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/_static/.gitkeep +0 -0
  40. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/advanced.rst +0 -0
  41. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/clients/bulk_data.rst +0 -0
  42. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/clients/index.rst +0 -0
  43. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/clients/patent_data.rst +0 -0
  44. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/clients/petition_decisions.rst +0 -0
  45. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/clients/ptab_appeals.rst +0 -0
  46. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/clients/ptab_interferences.rst +0 -0
  47. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/clients/ptab_trials.rst +0 -0
  48. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/config/config.rst +0 -0
  49. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/config/http_config.rst +0 -0
  50. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/config/index.rst +0 -0
  51. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/exceptions.rst +0 -0
  52. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/index.rst +0 -0
  53. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/models/base.rst +0 -0
  54. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/models/bulk_data.rst +0 -0
  55. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/models/index.rst +0 -0
  56. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/models/patent_data.rst +0 -0
  57. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/models/petition_decisions.rst +0 -0
  58. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/models/ptab.rst +0 -0
  59. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/models/utils.rst +0 -0
  60. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/api/warnings.rst +0 -0
  61. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/conf.py +0 -0
  62. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/configuration.rst +0 -0
  63. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/development.rst +0 -0
  64. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/examples/bulk_data.rst +0 -0
  65. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/examples/ifw_example.rst +0 -0
  66. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/examples/index.rst +0 -0
  67. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/examples/patent_data.rst +0 -0
  68. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/examples/petition_decisions.rst +0 -0
  69. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/examples/ptab_appeals.rst +0 -0
  70. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/examples/ptab_interferences.rst +0 -0
  71. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/examples/ptab_trials.rst +0 -0
  72. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/index.rst +0 -0
  73. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/installation.rst +0 -0
  74. {pyuspto-0.4.4 → pyuspto-0.4.5}/docs/source/quickstart.rst +0 -0
  75. {pyuspto-0.4.4 → pyuspto-0.4.5}/examples/bulk_data_example.py +0 -0
  76. {pyuspto-0.4.4 → pyuspto-0.4.5}/examples/error_handling_example.py +0 -0
  77. {pyuspto-0.4.4 → pyuspto-0.4.5}/examples/ifw_example.py +0 -0
  78. {pyuspto-0.4.4 → pyuspto-0.4.5}/examples/patent_data_example.py +0 -0
  79. {pyuspto-0.4.4 → pyuspto-0.4.5}/examples/petition_decisions_example.py +0 -0
  80. {pyuspto-0.4.4 → pyuspto-0.4.5}/examples/ptab_appeals_example.py +0 -0
  81. {pyuspto-0.4.4 → pyuspto-0.4.5}/examples/ptab_interferences_example.py +0 -0
  82. {pyuspto-0.4.4 → pyuspto-0.4.5}/examples/ptab_trials_example.py +0 -0
  83. {pyuspto-0.4.4 → pyuspto-0.4.5}/pyUSPTO.code-workspace +0 -0
  84. {pyuspto-0.4.4 → pyuspto-0.4.5}/pyproject.toml +0 -0
  85. {pyuspto-0.4.4 → pyuspto-0.4.5}/requirements.txt +0 -0
  86. {pyuspto-0.4.4 → pyuspto-0.4.5}/setup.cfg +0 -0
  87. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/__init__.py +0 -0
  88. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/clients/__init__.py +0 -0
  89. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/config.py +0 -0
  90. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/exceptions.py +0 -0
  91. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/http_config.py +0 -0
  92. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/models/__init__.py +0 -0
  93. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/models/base.py +0 -0
  94. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/models/bulk_data.py +0 -0
  95. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/models/ptab.py +0 -0
  96. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/models/utils.py +0 -0
  97. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/py.typed +0 -0
  98. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/utils/__init__.py +0 -0
  99. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO/warnings.py +0 -0
  100. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO.egg-info/SOURCES.txt +0 -0
  101. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO.egg-info/dependency_links.txt +0 -0
  102. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO.egg-info/requires.txt +0 -0
  103. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO.egg-info/top_level.txt +0 -0
  104. {pyuspto-0.4.4 → pyuspto-0.4.5}/src/pyUSPTO.egg-info/zip-safe +0 -0
  105. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/__init__.py +0 -0
  106. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/clients/__init__.py +0 -0
  107. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/clients/test_ptab_appeals_client.py +0 -0
  108. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/clients/test_ptab_interferences_client.py +0 -0
  109. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/clients/test_ptab_trials_client.py +0 -0
  110. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/conftest.py +0 -0
  111. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/integration/__init__.py +0 -0
  112. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/integration/conftest.py +0 -0
  113. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/integration/test_bulk_data_integration.py +0 -0
  114. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/integration/test_ptab_interferences_integration.py +0 -0
  115. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/integration/test_ptab_trials_integration.py +0 -0
  116. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/models/__init__.py +0 -0
  117. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/models/test_bulk_data_models.py +0 -0
  118. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/models/test_patent_data_models.py +0 -0
  119. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/models/test_petition_decision_models.py +0 -0
  120. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/models/test_ptab_models.py +0 -0
  121. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/models/test_utils.py +0 -0
  122. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/test_config.py +0 -0
  123. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/test_edge_cases.py +0 -0
  124. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/test_exceptions.py +0 -0
  125. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/test_http_config.py +0 -0
  126. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/test_init.py +0 -0
  127. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/test_version.py +0 -0
  128. {pyuspto-0.4.4 → pyuspto-0.4.5}/tests/utils/__init__.py +0 -0
  129. {pyuspto-0.4.4 → pyuspto-0.4.5}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyUSPTO
3
- Version: 0.4.4
3
+ Version: 0.4.5
4
4
  Summary: A Modern Python client for accessing the United States Patent and Trademark Office (USPTO) Open Data Portal (ODP) APIs.
5
5
  Author-email: Andrew Piechocki <apiechocki@dunlapcodding.com>
6
6
  License-Expression: MIT
@@ -88,10 +88,6 @@ pytest-cov==7.0.0
88
88
  # via pyuspto
89
89
  pytest-mock==3.15.1
90
90
  # via pyuspto
91
- pyuspto @ file:///C:/Users/andrewp/Documents/GitHub/pyUSPTO
92
- # via
93
- # pyUSPTO (pyproject.toml)
94
- # pyuspto
95
91
  pyyaml==6.0.3
96
92
  # via myst-parser
97
93
  requests==2.32.5
@@ -115,7 +111,7 @@ sphinx==9.1.0
115
111
  # sphinx-immaterial
116
112
  # sphinx-rtd-theme
117
113
  # sphinxcontrib-jquery
118
- sphinx-autodoc-typehints==3.9.7
114
+ sphinx-autodoc-typehints==3.9.8
119
115
  # via pyuspto
120
116
  sphinx-copybutton==0.5.2
121
117
  # via pyuspto
@@ -14,6 +14,10 @@ from typing import (
14
14
  runtime_checkable,
15
15
  )
16
16
 
17
+ try:
18
+ from typing import Self
19
+ except ImportError:
20
+ from typing_extensions import Self
17
21
  import requests
18
22
 
19
23
  from pyUSPTO.config import USPTOConfig
@@ -31,7 +35,7 @@ class FromDictProtocol(Protocol):
31
35
  """Protocol for classes that can be created from a dictionary."""
32
36
 
33
37
  @classmethod
34
- def from_dict(cls, data: dict[str, Any], include_raw_data: bool = False) -> Any:
38
+ def from_dict(cls, data: dict[str, Any], include_raw_data: bool = False) -> Self:
35
39
  """Create an object from a dictionary."""
36
40
  ...
37
41
 
@@ -39,6 +43,9 @@ class FromDictProtocol(Protocol):
39
43
  # Type variable for response classes
40
44
  T = TypeVar("T", bound=FromDictProtocol)
41
45
 
46
+ # Type variable for response classes
47
+ M = TypeVar("M", bound=FromDictProtocol)
48
+
42
49
 
43
50
  class BaseUSPTOClient(Generic[T]):
44
51
  """Base client class for USPTO API clients."""
@@ -166,49 +173,56 @@ class BaseUSPTOClient(Generic[T]):
166
173
  ),
167
174
  ) from json_err
168
175
 
169
- def _make_request(
176
+ def _build_url(
170
177
  self,
171
- method: str,
172
178
  endpoint: str,
179
+ custom_url: str | None = None,
180
+ custom_base_url: str | None = None,
181
+ ) -> str:
182
+ """Build the request URL from endpoint or custom URL.
183
+
184
+ Args:
185
+ endpoint: API endpoint path (without base URL)
186
+ custom_url: Optional full custom URL (overrides endpoint and base URL)
187
+ custom_base_url: Optional custom base URL instead of self.base_url
188
+
189
+ Returns:
190
+ The resolved URL string.
191
+ """
192
+ if custom_url:
193
+ return custom_url
194
+ base = custom_base_url if custom_base_url else self.base_url
195
+ return f"{base}/{endpoint.lstrip('/')}"
196
+
197
+ def _execute_request(
198
+ self,
199
+ method: str,
200
+ url: str,
173
201
  params: dict[str, Any] | None = None,
174
202
  json_data: dict[str, Any] | None = None,
175
203
  stream: bool = False,
176
- response_class: type[T] | None = None,
177
- custom_url: str | None = None,
178
- custom_base_url: str | None = None,
179
- ) -> dict[str, Any] | T | requests.Response:
180
- """Make an HTTP request to the USPTO API.
204
+ ) -> requests.Response:
205
+ """Execute an HTTP request and return the raw Response.
181
206
 
182
- Note: Only GET and POST methods are supported. Other HTTP methods will
183
- raise a ValueError.
207
+ Handles URL dispatch, error translation, and timeout/connection errors.
208
+ Callers are responsible for interpreting the response body.
184
209
 
185
210
  Args:
186
211
  method: HTTP method (GET or POST only)
187
- endpoint: API endpoint path (without base URL)
212
+ url: Fully resolved request URL
188
213
  params: Optional query parameters
189
214
  json_data: Optional JSON body for POST requests
190
215
  stream: Whether to stream the response
191
- response_class: Class to use for parsing the response
192
- custom_url: Optional full custom URL to use (overrides endpoint and base URL)
193
- custom_base_url: Optional custom base URL to use instead of self.base_url
194
216
 
195
217
  Returns:
196
- Response data in the appropriate format:
197
- - If stream=True: requests.Response object
198
- - If response_class is provided: Instance of response_class
199
- - Otherwise: Dict[str, Any] containing the JSON response
218
+ The raw requests.Response after raise_for_status().
200
219
 
201
220
  Raises:
202
221
  ValueError: If an unsupported HTTP method is provided
222
+ USPTOApiError: On HTTP errors from the API
223
+ USPTOTimeout: On request timeout
224
+ USPTOConnectionError: On connection failure
203
225
  """
204
- url: str = ""
205
- if custom_url:
206
- url = custom_url
207
- else:
208
- base = custom_base_url if custom_base_url else self.base_url
209
- url = f"{base}/{endpoint.lstrip('/')}"
210
-
211
- # Get timeout from HTTP config
212
226
  timeout = self.http_config.get_timeout_tuple()
213
227
 
214
228
  try:
@@ -228,26 +242,10 @@ class BaseUSPTOClient(Generic[T]):
228
242
  raise ValueError(f"Unsupported HTTP method: {method}")
229
243
 
230
244
  response.raise_for_status()
231
-
232
- # Return the raw response for streaming requests
233
- if stream:
234
- return response
235
-
236
- # Parse JSON response with error handling
237
- json_data = self._parse_json_response(response, url)
238
-
239
- # Parse the response based on the specified class
240
- if response_class:
241
- parsed_response: T = response_class.from_dict(
242
- json_data, include_raw_data=self.config.include_raw_data
243
- )
244
- return parsed_response
245
-
246
- # Return the raw JSON for other requests
247
- return json_data
245
+ return response
248
246
 
249
247
  except requests.exceptions.HTTPError as http_err:
250
- client_operation_message = f"API request to '{url}' failed with HTTPError" # 'url' is from _make_request scope
248
+ client_operation_message = f"API request to '{url}' failed with HTTPError"
251
249
 
252
250
  # Include request body for POST debugging
253
251
  if method.upper() == "POST" and json_data:
@@ -257,7 +255,6 @@ class BaseUSPTOClient(Generic[T]):
257
255
  f"\nRequest body sent:\n{json.dumps(json_data, indent=2)}"
258
256
  )
259
257
 
260
- # Create APIErrorArgs directly from the HTTPError
261
258
  current_error_args = APIErrorArgs.from_http_error(
262
259
  http_error=http_err, client_operation_message=client_operation_message
263
260
  )
@@ -266,7 +263,6 @@ class BaseUSPTOClient(Generic[T]):
266
263
  raise api_exception_to_raise from http_err
267
264
 
268
265
  except requests.exceptions.Timeout as timeout_err:
269
- # Specific handling for timeout errors
270
266
  raise USPTOTimeout(
271
267
  message=f"Request to '{url}' timed out",
272
268
  api_short_error="Timeout",
@@ -274,31 +270,120 @@ class BaseUSPTOClient(Generic[T]):
274
270
  ) from timeout_err
275
271
 
276
272
  except requests.exceptions.ConnectionError as conn_err:
277
- # Specific handling for connection errors (DNS, refused connection, etc.)
278
273
  raise USPTOConnectionError(
279
274
  message=f"Failed to connect to '{url}'",
280
275
  api_short_error="Connection Error",
281
276
  error_details=str(conn_err),
282
277
  ) from conn_err
283
278
 
284
- except (
285
- requests.exceptions.RequestException
286
- ) as req_err: # Catches other non-HTTP errors from requests
287
- client_operation_message = (
288
- f"API request to '{url}' failed" # 'url' is from _make_request scope
289
- )
279
+ except requests.exceptions.RequestException as req_err:
280
+ client_operation_message = f"API request to '{url}' failed"
290
281
 
291
- # Create APIErrorArgs from the generic RequestException
292
282
  current_error_args = APIErrorArgs.from_request_exception(
293
283
  request_exception=req_err,
294
- client_operation_message=client_operation_message, # or pass None if you prefer default message
284
+ client_operation_message=client_operation_message,
295
285
  )
296
286
 
297
- api_exception_to_raise = get_api_exception(
298
- current_error_args
299
- ) # Will default to USPTOApiError
287
+ api_exception_to_raise = get_api_exception(current_error_args)
300
288
  raise api_exception_to_raise from req_err
301
289
 
290
+ def _stream_request(
291
+ self,
292
+ method: str,
293
+ endpoint: str,
294
+ params: dict[str, Any] | None = None,
295
+ json_data: dict[str, Any] | None = None,
296
+ custom_url: str | None = None,
297
+ custom_base_url: str | None = None,
298
+ ) -> requests.Response:
299
+ """Make a streaming HTTP request and return the raw Response.
300
+
301
+ Args:
302
+ method: HTTP method (GET or POST only)
303
+ endpoint: API endpoint path (without base URL)
304
+ params: Optional query parameters
305
+ json_data: Optional JSON body for POST requests
306
+ custom_url: Optional full custom URL (overrides endpoint and base URL)
307
+ custom_base_url: Optional custom base URL instead of self.base_url
308
+
309
+ Returns:
310
+ Streaming requests.Response object.
311
+ """
312
+ url = self._build_url(
313
+ endpoint, custom_url=custom_url, custom_base_url=custom_base_url
314
+ )
315
+ return self._execute_request(
316
+ method=method, url=url, params=params, json_data=json_data, stream=True
317
+ )
318
+
319
+ def _get_model(
320
+ self,
321
+ method: str,
322
+ endpoint: str,
323
+ response_class: type[M],
324
+ params: dict[str, Any] | None = None,
325
+ json_data: dict[str, Any] | None = None,
326
+ custom_url: str | None = None,
327
+ custom_base_url: str | None = None,
328
+ ) -> M:
329
+ """Make an HTTP request and parse the response into a model.
330
+
331
+ Args:
332
+ method: HTTP method (GET or POST only)
333
+ endpoint: API endpoint path (without base URL)
334
+ response_class: Class to use for parsing the response
335
+ params: Optional query parameters
336
+ json_data: Optional JSON body for POST requests
337
+ custom_url: Optional full custom URL (overrides endpoint and base URL)
338
+ custom_base_url: Optional custom base URL instead of self.base_url
339
+
340
+ Returns:
341
+ Instance of response_class parsed from the JSON response.
342
+ """
343
+ url = self._build_url(
344
+ endpoint, custom_url=custom_url, custom_base_url=custom_base_url
345
+ )
346
+ response = self._execute_request(
347
+ method=method, url=url, params=params, json_data=json_data
348
+ )
349
+ data = self._parse_json_response(response, url)
350
+
351
+ ret = response_class.from_dict(
352
+ data, include_raw_data=self.config.include_raw_data
353
+ )
354
+ assert isinstance(ret, response_class)
355
+ return ret
356
+
357
+ def _get_json(
358
+ self,
359
+ method: str,
360
+ endpoint: str,
361
+ params: dict[str, Any] | None = None,
362
+ json_data: dict[str, Any] | None = None,
363
+ custom_url: str | None = None,
364
+ custom_base_url: str | None = None,
365
+ ) -> dict[str, Any]:
366
+ """Make an HTTP request and return the parsed JSON response.
367
+
368
+ Args:
369
+ method: HTTP method (GET or POST only)
370
+ endpoint: API endpoint path (without base URL)
371
+ params: Optional query parameters
372
+ json_data: Optional JSON body for POST requests
373
+ custom_url: Optional full custom URL (overrides endpoint and base URL)
374
+ custom_base_url: Optional custom base URL instead of self.base_url
375
+
376
+ Returns:
377
+ Dict containing the JSON response.
378
+ """
379
+ url = self._build_url(
380
+ endpoint, custom_url=custom_url, custom_base_url=custom_base_url
381
+ )
382
+ response = self._execute_request(
383
+ method=method, url=url, params=params, json_data=json_data
384
+ )
385
+ return self._parse_json_response(response, url)
386
+
302
387
  def paginate_results(
303
388
  self,
304
389
  method_name: str,
@@ -785,19 +870,14 @@ class BaseUSPTOClient(Generic[T]):
785
870
  Path to downloaded file
786
871
 
787
872
  Raises:
788
- TypeError: If response is not a valid Response object
789
873
  FileExistsError: If file exists and overwrite is False
790
874
  """
791
- response = self._make_request(
875
+ response = self._stream_request(
792
876
  method="GET",
793
877
  endpoint="",
794
- stream=True,
795
878
  custom_url=url,
796
879
  )
797
880
 
798
- if not isinstance(response, requests.Response):
799
- raise TypeError(f"Expected Response, got {type(response)}")
800
-
801
881
  return self._save_response_to_file(
802
882
  response=response,
803
883
  destination=destination,
@@ -109,13 +109,12 @@ class BulkDataClient(BaseUSPTOClient[BulkDataResponse]):
109
109
  params["latest"] = str(latest).lower()
110
110
 
111
111
  # Use response_class for clean parsing
112
- response = self._make_request(
112
+ response = self._get_model(
113
113
  method="GET",
114
114
  endpoint=endpoint,
115
- params=params if params else None,
116
115
  response_class=BulkDataResponse,
116
+ params=params if params else None,
117
117
  )
118
- assert isinstance(response, BulkDataResponse)
119
118
 
120
119
  # Extract the product from response
121
120
  if response.bulk_data_product_bag:
@@ -251,13 +250,9 @@ class BulkDataClient(BaseUSPTOClient[BulkDataResponse]):
251
250
  if fields is not None:
252
251
  params["fields"] = ",".join(fields)
253
252
 
254
- result = self._make_request(
253
+ return self._get_model(
255
254
  method="GET",
256
255
  endpoint=self.ENDPOINTS["products_search"],
257
- params=params,
258
256
  response_class=BulkDataResponse,
257
+ params=params,
259
258
  )
260
-
261
- # Since we specified response_class=BulkDataResponse, the result should be a BulkDataResponse
262
- assert isinstance(result, BulkDataResponse)
263
- return result
@@ -263,12 +263,12 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
263
263
  endpoint = self.ENDPOINTS["search_applications"]
264
264
 
265
265
  if post_body is not None:
266
- result = self._make_request(
266
+ result = self._get_model(
267
267
  method="POST",
268
268
  endpoint=endpoint,
269
+ response_class=PatentDataResponse,
269
270
  json_data=post_body,
270
271
  params=additional_query_params,
271
- response_class=PatentDataResponse,
272
272
  )
273
273
  else:
274
274
  params: dict[str, Any] = {}
@@ -366,13 +366,12 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
366
366
 
367
367
  if additional_query_params:
368
368
  params.update(additional_query_params)
369
- result = self._make_request(
369
+ result = self._get_model(
370
370
  method="GET",
371
371
  endpoint=endpoint,
372
- params=params,
373
372
  response_class=PatentDataResponse,
373
+ params=params,
374
374
  )
375
- assert isinstance(result, PatentDataResponse)
376
375
  return result
377
376
 
378
377
  def get_search_results(
@@ -408,7 +407,7 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
408
407
  if "format" not in post_body:
409
408
  post_body["format"] = "json"
410
409
 
411
- result = self._make_request(
410
+ result = self._get_json(
412
411
  method="POST",
413
412
  endpoint=endpoint,
414
413
  json_data=post_body,
@@ -504,12 +503,11 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
504
503
  if additional_query_params:
505
504
  params.update(additional_query_params)
506
505
 
507
- result = self._make_request(
506
+ result = self._get_json(
508
507
  method="GET",
509
508
  endpoint=endpoint,
510
509
  params=params,
511
510
  )
512
- assert isinstance(result, dict)
513
511
  amd_list = [
514
512
  ApplicationMetaData.from_dict(item["applicationMetaData"])
515
513
  for item in result["patentdata"]
@@ -541,14 +539,14 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
541
539
  endpoint = self.ENDPOINTS["get_application_by_number"].format(
542
540
  application_number=self.sanitize_application_number(application_number)
543
541
  )
544
- response_data = self._make_request(
542
+ response_data = self._get_model(
545
543
  method="GET", endpoint=endpoint, response_class=PatentDataResponse
546
544
  )
547
- assert isinstance(response_data, PatentDataResponse)
548
- return self._get_wrapper_from_response(
545
+ ret = self._get_wrapper_from_response(
549
546
  response_data=response_data,
550
547
  application_number_for_validation=application_number,
551
548
  )
549
+ return ret
552
550
 
553
551
  def get_application_metadata(
554
552
  self, application_number: str
@@ -575,10 +573,9 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
575
573
  endpoint = self.ENDPOINTS["get_application_metadata"].format(
576
574
  application_number=self.sanitize_application_number(application_number)
577
575
  )
578
- response_data = self._make_request(
576
+ response_data = self._get_model(
579
577
  method="GET", endpoint=endpoint, response_class=PatentDataResponse
580
578
  )
581
- assert isinstance(response_data, PatentDataResponse)
582
579
  wrapper = self._get_wrapper_from_response(response_data, application_number)
583
580
  return wrapper.application_meta_data if wrapper else None
584
581
 
@@ -606,10 +603,9 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
606
603
  endpoint = self.ENDPOINTS["get_application_adjustment"].format(
607
604
  application_number=self.sanitize_application_number(application_number)
608
605
  )
609
- response_data = self._make_request(
606
+ response_data = self._get_model(
610
607
  method="GET", endpoint=endpoint, response_class=PatentDataResponse
611
608
  )
612
- assert isinstance(response_data, PatentDataResponse)
613
609
  wrapper = self._get_wrapper_from_response(response_data, application_number)
614
610
  return wrapper.patent_term_adjustment_data if wrapper else None
615
611
 
@@ -638,10 +634,9 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
638
634
  endpoint = self.ENDPOINTS["get_application_assignment"].format(
639
635
  application_number=self.sanitize_application_number(application_number)
640
636
  )
641
- response_data = self._make_request(
637
+ response_data = self._get_model(
642
638
  method="GET", endpoint=endpoint, response_class=PatentDataResponse
643
639
  )
644
- assert isinstance(response_data, PatentDataResponse)
645
640
  wrapper = self._get_wrapper_from_response(response_data, application_number)
646
641
  return wrapper.assignment_bag if wrapper else None
647
642
 
@@ -668,10 +663,9 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
668
663
  endpoint = self.ENDPOINTS["get_application_attorney"].format(
669
664
  application_number=self.sanitize_application_number(application_number)
670
665
  )
671
- response_data = self._make_request(
666
+ response_data = self._get_model(
672
667
  method="GET", endpoint=endpoint, response_class=PatentDataResponse
673
668
  )
674
- assert isinstance(response_data, PatentDataResponse)
675
669
  wrapper = self._get_wrapper_from_response(response_data, application_number)
676
670
  return wrapper.record_attorney if wrapper else None
677
671
 
@@ -701,10 +695,9 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
701
695
  endpoint = self.ENDPOINTS["get_application_continuity"].format(
702
696
  application_number=self.sanitize_application_number(application_number)
703
697
  )
704
- response_data = self._make_request(
698
+ response_data = self._get_model(
705
699
  method="GET", endpoint=endpoint, response_class=PatentDataResponse
706
700
  )
707
- assert isinstance(response_data, PatentDataResponse)
708
701
  wrapper = self._get_wrapper_from_response(response_data, application_number)
709
702
  return ApplicationContinuityData.from_wrapper(wrapper) if wrapper else None
710
703
 
@@ -733,10 +726,9 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
733
726
  endpoint = self.ENDPOINTS["get_application_foreign_priority"].format(
734
727
  application_number=self.sanitize_application_number(application_number)
735
728
  )
736
- response_data = self._make_request(
729
+ response_data = self._get_model(
737
730
  method="GET", endpoint=endpoint, response_class=PatentDataResponse
738
731
  )
739
- assert isinstance(response_data, PatentDataResponse)
740
732
  wrapper = self._get_wrapper_from_response(response_data, application_number)
741
733
  return wrapper.foreign_priority_bag if wrapper else None
742
734
 
@@ -765,10 +757,9 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
765
757
  endpoint = self.ENDPOINTS["get_application_transactions"].format(
766
758
  application_number=self.sanitize_application_number(application_number)
767
759
  )
768
- response_data = self._make_request(
760
+ response_data = self._get_model(
769
761
  method="GET", endpoint=endpoint, response_class=PatentDataResponse
770
762
  )
771
- assert isinstance(response_data, PatentDataResponse)
772
763
  wrapper = self._get_wrapper_from_response(response_data, application_number)
773
764
  return wrapper.event_data_bag if wrapper else None
774
765
 
@@ -819,11 +810,12 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
819
810
  if official_date_to:
820
811
  params["officialDateTo"] = official_date_to
821
812
 
822
- result_dict = self._make_request(
823
- method="GET", endpoint=endpoint, params=params if params else None
813
+ return self._get_model(
814
+ method="GET",
815
+ endpoint=endpoint,
816
+ response_class=DocumentBag,
817
+ params=params if params else None,
824
818
  )
825
- assert isinstance(result_dict, dict)
826
- return DocumentBag.from_dict(result_dict)
827
819
 
828
820
  def get_application_associated_documents(
829
821
  self, application_number: str
@@ -855,10 +847,9 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
855
847
  endpoint = self.ENDPOINTS["get_application_associated_documents"].format(
856
848
  application_number=self.sanitize_application_number(application_number)
857
849
  )
858
- response_data = self._make_request(
850
+ response_data = self._get_model(
859
851
  method="GET", endpoint=endpoint, response_class=PatentDataResponse
860
852
  )
861
- assert isinstance(response_data, PatentDataResponse)
862
853
  wrapper = self._get_wrapper_from_response(response_data, application_number)
863
854
  return PrintedPublication.from_wrapper(wrapper) if wrapper else None
864
855
 
@@ -935,11 +926,12 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
935
926
  status codes, a `StatusCodeCollection` of the `StatusCode`
936
927
  objects (code and description), and a request identifier.
937
928
  """
938
- result_dict = self._make_request(
939
- method="GET", endpoint=self.ENDPOINTS["status_codes"], params=params
929
+ return self._get_model(
930
+ method="GET",
931
+ endpoint=self.ENDPOINTS["status_codes"],
932
+ response_class=StatusCodeSearchResponse,
933
+ params=params,
940
934
  )
941
- assert isinstance(result_dict, dict)
942
- return StatusCodeSearchResponse.from_dict(result_dict)
943
935
 
944
936
  def search_status_codes(
945
937
  self, search_request: dict[str, Any]
@@ -962,13 +954,12 @@ class PatentDataClient(BaseUSPTOClient[PatentDataResponse]):
962
954
  status codes, a `StatusCodeCollection` of the `StatusCode`
963
955
  objects (code and description), and a request identifier.
964
956
  """
965
- result_dict = self._make_request(
957
+ return self._get_model(
966
958
  method="POST",
967
959
  endpoint=self.ENDPOINTS["status_codes"],
960
+ response_class=StatusCodeSearchResponse,
968
961
  json_data=search_request,
969
962
  )
970
- assert isinstance(result_dict, dict)
971
- return StatusCodeSearchResponse.from_dict(result_dict)
972
963
 
973
964
  def download_document(
974
965
  self,
@@ -184,12 +184,12 @@ class FinalPetitionDecisionsClient(BaseUSPTOClient[PetitionDecisionResponse]):
184
184
 
185
185
  if post_body is not None:
186
186
  # POST request path
187
- result = self._make_request(
187
+ result = self._get_model(
188
188
  method="POST",
189
189
  endpoint=endpoint,
190
+ response_class=PetitionDecisionResponse,
190
191
  json_data=post_body,
191
192
  params=additional_query_params,
192
- response_class=PetitionDecisionResponse,
193
193
  )
194
194
  else:
195
195
  # GET request path
@@ -280,14 +280,12 @@ class FinalPetitionDecisionsClient(BaseUSPTOClient[PetitionDecisionResponse]):
280
280
  if additional_query_params:
281
281
  params.update(additional_query_params)
282
282
 
283
- result = self._make_request(
283
+ result = self._get_model(
284
284
  method="GET",
285
285
  endpoint=endpoint,
286
- params=params,
287
286
  response_class=PetitionDecisionResponse,
287
+ params=params,
288
288
  )
289
-
290
- assert isinstance(result, PetitionDecisionResponse)
291
289
  return result
292
290
 
293
291
  def get_decision_by_id(
@@ -326,13 +324,12 @@ class FinalPetitionDecisionsClient(BaseUSPTOClient[PetitionDecisionResponse]):
326
324
  if include_documents is not None:
327
325
  params["includeDocuments"] = str(include_documents).lower()
328
326
 
329
- response_data = self._make_request(
327
+ response_data = self._get_model(
330
328
  method="GET",
331
329
  endpoint=endpoint,
332
- params=params if params else None,
333
330
  response_class=PetitionDecisionResponse,
331
+ params=params if params else None,
334
332
  )
335
- assert isinstance(response_data, PetitionDecisionResponse)
336
333
  return self._get_decision_from_response(
337
334
  response_data=response_data,
338
335
  petition_decision_record_identifier_for_validation=petition_decision_record_identifier,
@@ -480,17 +477,17 @@ class FinalPetitionDecisionsClient(BaseUSPTOClient[PetitionDecisionResponse]):
480
477
 
481
478
  if format.lower() == "json":
482
479
  # For JSON, parse the response
483
- result_dict = self._make_request(
484
- method="GET", endpoint=endpoint, params=params
480
+ return self._get_model(
481
+ method="GET",
482
+ endpoint=endpoint,
483
+ response_class=PetitionDecisionDownloadResponse,
484
+ params=params,
485
485
  )
486
- assert isinstance(result_dict, dict)
487
- return PetitionDecisionDownloadResponse.from_dict(result_dict)
488
486
  else:
489
487
  # For CSV or other formats, get streaming response
490
- result = self._make_request(
491
- method="GET", endpoint=endpoint, params=params, stream=True
488
+ result = self._stream_request(
489
+ method="GET", endpoint=endpoint, params=params
492
490
  )
493
- assert isinstance(result, requests.Response)
494
491
 
495
492
  if destination is not None:
496
493
  # Save to file using the base class helper