pyUSPTO 0.4.3__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.3 → pyuspto-0.4.5}/.github/workflows/publish-to-test-pypi.yml +1 -1
  2. {pyuspto-0.4.3 → pyuspto-0.4.5}/PKG-INFO +4 -3
  3. {pyuspto-0.4.3 → pyuspto-0.4.5}/README.md +2 -1
  4. {pyuspto-0.4.3 → pyuspto-0.4.5}/pyproject.toml +7 -2
  5. {pyuspto-0.4.3 → pyuspto-0.4.5}/requirements-dev.txt +3 -7
  6. {pyuspto-0.4.3 → pyuspto-0.4.5}/requirements.txt +1 -1
  7. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/clients/base.py +144 -64
  8. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/clients/bulk_data.py +4 -9
  9. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/clients/patent_data.py +104 -39
  10. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/clients/petition_decisions.py +13 -16
  11. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/clients/ptab_appeals.py +4 -6
  12. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/clients/ptab_interferences.py +4 -6
  13. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/clients/ptab_trials.py +10 -12
  14. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/models/patent_data.py +9 -3
  15. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/models/petition_decisions.py +6 -3
  16. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO.egg-info/PKG-INFO +4 -3
  17. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/clients/test_base.py +87 -50
  18. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/clients/test_bulk_data_clients.py +3 -3
  19. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/clients/test_patent_data_clients.py +468 -261
  20. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/clients/test_petition_decision_clients.py +130 -112
  21. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/integration/test_patent_data_integration.py +64 -15
  22. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/integration/test_petition_decisions_integration.py +23 -23
  23. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/integration/test_ptab_appeals_integration.py +18 -14
  24. {pyuspto-0.4.3 → pyuspto-0.4.5}/.github/ISSUE_TEMPLATE/bug-report.yml +0 -0
  25. {pyuspto-0.4.3 → pyuspto-0.4.5}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  26. {pyuspto-0.4.3 → pyuspto-0.4.5}/.github/ISSUE_TEMPLATE/documentation-updates.yml +0 -0
  27. {pyuspto-0.4.3 → pyuspto-0.4.5}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
  28. {pyuspto-0.4.3 → pyuspto-0.4.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  29. {pyuspto-0.4.3 → pyuspto-0.4.5}/.github/dependabot.yml +0 -0
  30. {pyuspto-0.4.3 → pyuspto-0.4.5}/.github/workflows/publish-docs.yaml +0 -0
  31. {pyuspto-0.4.3 → pyuspto-0.4.5}/.github/workflows/python-tests.yml +0 -0
  32. {pyuspto-0.4.3 → pyuspto-0.4.5}/.gitignore +0 -0
  33. {pyuspto-0.4.3 → pyuspto-0.4.5}/.readthedocs.yaml +0 -0
  34. {pyuspto-0.4.3 → pyuspto-0.4.5}/ADVANCED.md +0 -0
  35. {pyuspto-0.4.3 → pyuspto-0.4.5}/CHANGELOG.md +0 -0
  36. {pyuspto-0.4.3 → pyuspto-0.4.5}/CODE_OF_CONDUCT.md +0 -0
  37. {pyuspto-0.4.3 → pyuspto-0.4.5}/CONTRIBUTING.md +0 -0
  38. {pyuspto-0.4.3 → pyuspto-0.4.5}/LICENSE +0 -0
  39. {pyuspto-0.4.3 → pyuspto-0.4.5}/_version.pyi +0 -0
  40. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/Makefile +0 -0
  41. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/_static/.gitkeep +0 -0
  42. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/advanced.rst +0 -0
  43. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/clients/bulk_data.rst +0 -0
  44. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/clients/index.rst +0 -0
  45. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/clients/patent_data.rst +0 -0
  46. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/clients/petition_decisions.rst +0 -0
  47. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/clients/ptab_appeals.rst +0 -0
  48. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/clients/ptab_interferences.rst +0 -0
  49. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/clients/ptab_trials.rst +0 -0
  50. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/config/config.rst +0 -0
  51. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/config/http_config.rst +0 -0
  52. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/config/index.rst +0 -0
  53. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/exceptions.rst +0 -0
  54. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/index.rst +0 -0
  55. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/models/base.rst +0 -0
  56. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/models/bulk_data.rst +0 -0
  57. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/models/index.rst +0 -0
  58. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/models/patent_data.rst +0 -0
  59. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/models/petition_decisions.rst +0 -0
  60. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/models/ptab.rst +0 -0
  61. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/models/utils.rst +0 -0
  62. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/api/warnings.rst +0 -0
  63. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/conf.py +0 -0
  64. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/configuration.rst +0 -0
  65. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/development.rst +0 -0
  66. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/examples/bulk_data.rst +0 -0
  67. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/examples/ifw_example.rst +0 -0
  68. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/examples/index.rst +0 -0
  69. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/examples/patent_data.rst +0 -0
  70. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/examples/petition_decisions.rst +0 -0
  71. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/examples/ptab_appeals.rst +0 -0
  72. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/examples/ptab_interferences.rst +0 -0
  73. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/examples/ptab_trials.rst +0 -0
  74. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/index.rst +0 -0
  75. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/installation.rst +0 -0
  76. {pyuspto-0.4.3 → pyuspto-0.4.5}/docs/source/quickstart.rst +0 -0
  77. {pyuspto-0.4.3 → pyuspto-0.4.5}/examples/bulk_data_example.py +0 -0
  78. {pyuspto-0.4.3 → pyuspto-0.4.5}/examples/error_handling_example.py +0 -0
  79. {pyuspto-0.4.3 → pyuspto-0.4.5}/examples/ifw_example.py +0 -0
  80. {pyuspto-0.4.3 → pyuspto-0.4.5}/examples/patent_data_example.py +0 -0
  81. {pyuspto-0.4.3 → pyuspto-0.4.5}/examples/petition_decisions_example.py +0 -0
  82. {pyuspto-0.4.3 → pyuspto-0.4.5}/examples/ptab_appeals_example.py +0 -0
  83. {pyuspto-0.4.3 → pyuspto-0.4.5}/examples/ptab_interferences_example.py +0 -0
  84. {pyuspto-0.4.3 → pyuspto-0.4.5}/examples/ptab_trials_example.py +0 -0
  85. {pyuspto-0.4.3 → pyuspto-0.4.5}/pyUSPTO.code-workspace +0 -0
  86. {pyuspto-0.4.3 → pyuspto-0.4.5}/setup.cfg +0 -0
  87. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/__init__.py +0 -0
  88. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/clients/__init__.py +0 -0
  89. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/config.py +0 -0
  90. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/exceptions.py +0 -0
  91. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/http_config.py +0 -0
  92. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/models/__init__.py +0 -0
  93. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/models/base.py +0 -0
  94. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/models/bulk_data.py +0 -0
  95. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/models/ptab.py +0 -0
  96. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/models/utils.py +0 -0
  97. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/py.typed +0 -0
  98. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/utils/__init__.py +0 -0
  99. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO/warnings.py +0 -0
  100. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO.egg-info/SOURCES.txt +0 -0
  101. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO.egg-info/dependency_links.txt +0 -0
  102. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO.egg-info/requires.txt +0 -0
  103. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO.egg-info/top_level.txt +0 -0
  104. {pyuspto-0.4.3 → pyuspto-0.4.5}/src/pyUSPTO.egg-info/zip-safe +0 -0
  105. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/__init__.py +0 -0
  106. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/clients/__init__.py +0 -0
  107. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/clients/test_ptab_appeals_client.py +0 -0
  108. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/clients/test_ptab_interferences_client.py +0 -0
  109. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/clients/test_ptab_trials_client.py +0 -0
  110. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/conftest.py +0 -0
  111. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/integration/__init__.py +0 -0
  112. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/integration/conftest.py +0 -0
  113. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/integration/test_bulk_data_integration.py +0 -0
  114. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/integration/test_ptab_interferences_integration.py +0 -0
  115. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/integration/test_ptab_trials_integration.py +0 -0
  116. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/models/__init__.py +0 -0
  117. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/models/test_bulk_data_models.py +0 -0
  118. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/models/test_patent_data_models.py +0 -0
  119. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/models/test_petition_decision_models.py +0 -0
  120. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/models/test_ptab_models.py +0 -0
  121. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/models/test_utils.py +0 -0
  122. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/test_config.py +0 -0
  123. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/test_edge_cases.py +0 -0
  124. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/test_exceptions.py +0 -0
  125. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/test_http_config.py +0 -0
  126. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/test_init.py +0 -0
  127. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/test_version.py +0 -0
  128. {pyuspto-0.4.3 → pyuspto-0.4.5}/tests/utils/__init__.py +0 -0
  129. {pyuspto-0.4.3 → pyuspto-0.4.5}/tox.ini +0 -0
@@ -12,7 +12,7 @@ on:
12
12
 
13
13
  jobs:
14
14
  build:
15
- name: Build distribution 📦
15
+ name: Build distribution
16
16
  runs-on: ubuntu-latest
17
17
 
18
18
  steps:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyUSPTO
3
- Version: 0.4.3
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
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: 3.14
19
19
  Classifier: Operating System :: OS Independent
20
- Classifier: Development Status :: 4 - Beta
20
+ Classifier: Development Status :: 5 - Production/Stable
21
21
  Classifier: Environment :: Console
22
22
  Classifier: Intended Audience :: Developers
23
23
  Classifier: Intended Audience :: Education
@@ -65,12 +65,13 @@ Requires-Dist: pyUSPTO[docs]; extra == "dev"
65
65
  Requires-Dist: pyUSPTO[lint]; extra == "dev"
66
66
  Dynamic: license-file
67
67
 
68
- # pyUSPTO
68
+ # USPTO Open Data Portal Client Library
69
69
 
70
70
  [![PyPI version](https://badge.fury.io/py/pyUSPTO.svg)](https://badge.fury.io/py/pyUSPTO)
71
71
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
72
72
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
73
73
  [![Read the Docs](https://img.shields.io/readthedocs/pyuspto)](https://pyuspto.readthedocs.io/en/latest/)
74
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pyuspto?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLUE&right_color=GREY&left_text=downloads)](https://pepy.tech/projects/pyuspto)
74
75
 
75
76
  A Python client library for interacting with the United Stated Patent and Trademark Office (USPTO) [Open Data Portal](https://data.uspto.gov/home) APIs.
76
77
 
@@ -1,9 +1,10 @@
1
- # pyUSPTO
1
+ # USPTO Open Data Portal Client Library
2
2
 
3
3
  [![PyPI version](https://badge.fury.io/py/pyUSPTO.svg)](https://badge.fury.io/py/pyUSPTO)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
6
6
  [![Read the Docs](https://img.shields.io/readthedocs/pyuspto)](https://pyuspto.readthedocs.io/en/latest/)
7
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pyuspto?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLUE&right_color=GREY&left_text=downloads)](https://pepy.tech/projects/pyuspto)
7
8
 
8
9
  A Python client library for interacting with the United Stated Patent and Trademark Office (USPTO) [Open Data Portal](https://data.uspto.gov/home) APIs.
9
10
 
@@ -20,7 +20,7 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.13",
21
21
  "Programming Language :: Python :: 3.14",
22
22
  "Operating System :: OS Independent",
23
- "Development Status :: 4 - Beta",
23
+ "Development Status :: 5 - Production/Stable",
24
24
  "Environment :: Console",
25
25
  "Intended Audience :: Developers",
26
26
  "Intended Audience :: Education",
@@ -51,7 +51,12 @@ dependencies = [
51
51
  dynamic = ["version"]
52
52
 
53
53
  [project.optional-dependencies]
54
- test = ["pytest>=9.0.2", "pytest-cov>=7.0.0", "pytest-mock>=3.15.1", "typing_extensions>=4.15.0"]
54
+ test = [
55
+ "pytest>=9.0.2",
56
+ "pytest-cov>=7.0.0",
57
+ "pytest-mock>=3.15.1",
58
+ "typing_extensions>=4.15.0",
59
+ ]
55
60
  docs = [
56
61
  "sphinx>=9.1.0",
57
62
  "sphinx-rtd-theme>=3.1.0",
@@ -14,7 +14,7 @@ babel==2.18.0
14
14
  # via sphinx
15
15
  certifi==2026.2.25
16
16
  # via requests
17
- charset-normalizer==3.4.4
17
+ charset-normalizer==3.4.5
18
18
  # via requests
19
19
  colorama==0.4.6
20
20
  # via
@@ -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
@@ -102,7 +98,7 @@ requests==2.32.5
102
98
  # sphinx-immaterial
103
99
  roman-numerals==4.1.0
104
100
  # via sphinx
105
- ruff==0.15.4
101
+ ruff==0.15.5
106
102
  # via pyuspto
107
103
  snowballstemmer==3.0.1
108
104
  # via sphinx
@@ -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.6
114
+ sphinx-autodoc-typehints==3.9.8
119
115
  # via pyuspto
120
116
  sphinx-copybutton==0.5.2
121
117
  # via pyuspto
@@ -6,7 +6,7 @@
6
6
  #
7
7
  certifi==2026.2.25
8
8
  # via requests
9
- charset-normalizer==3.4.4
9
+ charset-normalizer==3.4.5
10
10
  # via requests
11
11
  idna==3.11
12
12
  # via requests
@@ -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