From c02265e89af10af20b5b684abca0459c3055d34d Mon Sep 17 00:00:00 2001 From: Andrew <3300522+dpieski@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:59:54 -0500 Subject: [PATCH 1/6] feat: Extract _stream_request and _execute_request from _make_request --- src/pyUSPTO/clients/base.py | 183 ++++++++++++------ src/pyUSPTO/clients/petition_decisions.py | 5 +- tests/clients/test_base.py | 7 +- tests/clients/test_patent_data_clients.py | 34 +--- .../clients/test_petition_decision_clients.py | 24 ++- 5 files changed, 145 insertions(+), 108 deletions(-) diff --git a/src/pyUSPTO/clients/base.py b/src/pyUSPTO/clients/base.py index ece2954..f095acd 100644 --- a/src/pyUSPTO/clients/base.py +++ b/src/pyUSPTO/clients/base.py @@ -166,49 +166,56 @@ def _parse_json_response( ), ) from json_err - def _make_request( + def _build_url( self, - method: str, endpoint: str, + custom_url: str | None = None, + custom_base_url: str | None = None, + ) -> str: + """Build the request URL from endpoint or custom URL. + + Args: + endpoint: API endpoint path (without base URL) + custom_url: Optional full custom URL (overrides endpoint and base URL) + custom_base_url: Optional custom base URL instead of self.base_url + + Returns: + The resolved URL string. + """ + if custom_url: + return custom_url + base = custom_base_url if custom_base_url else self.base_url + return f"{base}/{endpoint.lstrip('/')}" + + def _execute_request( + self, + method: str, + url: str, params: dict[str, Any] | None = None, json_data: dict[str, Any] | None = None, stream: bool = False, - response_class: type[T] | None = None, - custom_url: str | None = None, - custom_base_url: str | None = None, - ) -> dict[str, Any] | T | requests.Response: - """Make an HTTP request to the USPTO API. + ) -> requests.Response: + """Execute an HTTP request and return the raw Response. - Note: Only GET and POST methods are supported. Other HTTP methods will - raise a ValueError. + Handles URL dispatch, error translation, and timeout/connection errors. + Callers are responsible for interpreting the response body. Args: method: HTTP method (GET or POST only) - endpoint: API endpoint path (without base URL) + url: Fully resolved request URL params: Optional query parameters json_data: Optional JSON body for POST requests stream: Whether to stream the response - response_class: Class to use for parsing the response - custom_url: Optional full custom URL to use (overrides endpoint and base URL) - custom_base_url: Optional custom base URL to use instead of self.base_url Returns: - Response data in the appropriate format: - - If stream=True: requests.Response object - - If response_class is provided: Instance of response_class - - Otherwise: Dict[str, Any] containing the JSON response + The raw requests.Response after raise_for_status(). Raises: ValueError: If an unsupported HTTP method is provided + USPTOApiError: On HTTP errors from the API + USPTOTimeout: On request timeout + USPTOConnectionError: On connection failure """ - url: str = "" - if custom_url: - url = custom_url - else: - base = custom_base_url if custom_base_url else self.base_url - url = f"{base}/{endpoint.lstrip('/')}" - - # Get timeout from HTTP config timeout = self.http_config.get_timeout_tuple() try: @@ -228,26 +235,10 @@ def _make_request( raise ValueError(f"Unsupported HTTP method: {method}") response.raise_for_status() - - # Return the raw response for streaming requests - if stream: - return response - - # Parse JSON response with error handling - json_data = self._parse_json_response(response, url) - - # Parse the response based on the specified class - if response_class: - parsed_response: T = response_class.from_dict( - json_data, include_raw_data=self.config.include_raw_data - ) - return parsed_response - - # Return the raw JSON for other requests - return json_data + return response except requests.exceptions.HTTPError as http_err: - client_operation_message = f"API request to '{url}' failed with HTTPError" # 'url' is from _make_request scope + client_operation_message = f"API request to '{url}' failed with HTTPError" # Include request body for POST debugging if method.upper() == "POST" and json_data: @@ -257,7 +248,6 @@ def _make_request( f"\nRequest body sent:\n{json.dumps(json_data, indent=2)}" ) - # Create APIErrorArgs directly from the HTTPError current_error_args = APIErrorArgs.from_http_error( http_error=http_err, client_operation_message=client_operation_message ) @@ -266,7 +256,6 @@ def _make_request( raise api_exception_to_raise from http_err except requests.exceptions.Timeout as timeout_err: - # Specific handling for timeout errors raise USPTOTimeout( message=f"Request to '{url}' timed out", api_short_error="Timeout", @@ -274,31 +263,104 @@ def _make_request( ) from timeout_err except requests.exceptions.ConnectionError as conn_err: - # Specific handling for connection errors (DNS, refused connection, etc.) raise USPTOConnectionError( message=f"Failed to connect to '{url}'", api_short_error="Connection Error", error_details=str(conn_err), ) from conn_err - except ( - requests.exceptions.RequestException - ) as req_err: # Catches other non-HTTP errors from requests - client_operation_message = ( - f"API request to '{url}' failed" # 'url' is from _make_request scope - ) + except requests.exceptions.RequestException as req_err: + client_operation_message = f"API request to '{url}' failed" - # Create APIErrorArgs from the generic RequestException current_error_args = APIErrorArgs.from_request_exception( request_exception=req_err, - client_operation_message=client_operation_message, # or pass None if you prefer default message + client_operation_message=client_operation_message, ) - api_exception_to_raise = get_api_exception( - current_error_args - ) # Will default to USPTOApiError + api_exception_to_raise = get_api_exception(current_error_args) raise api_exception_to_raise from req_err + def _stream_request( + self, + method: str, + endpoint: str, + params: dict[str, Any] | None = None, + json_data: dict[str, Any] | None = None, + custom_url: str | None = None, + custom_base_url: str | None = None, + ) -> requests.Response: + """Make a streaming HTTP request and return the raw Response. + + Args: + method: HTTP method (GET or POST only) + endpoint: API endpoint path (without base URL) + params: Optional query parameters + json_data: Optional JSON body for POST requests + custom_url: Optional full custom URL (overrides endpoint and base URL) + custom_base_url: Optional custom base URL instead of self.base_url + + Returns: + Streaming requests.Response object. + """ + url = self._build_url( + endpoint, custom_url=custom_url, custom_base_url=custom_base_url + ) + return self._execute_request( + method=method, url=url, params=params, json_data=json_data, stream=True + ) + + def _make_request( + self, + method: str, + endpoint: str, + params: dict[str, Any] | None = None, + json_data: dict[str, Any] | None = None, + response_class: type[T] | None = None, + custom_url: str | None = None, + custom_base_url: str | None = None, + ) -> dict[str, Any] | T: + """Make an HTTP request to the USPTO API. + + Note: Only GET and POST methods are supported. Other HTTP methods will + raise a ValueError. + + Args: + method: HTTP method (GET or POST only) + endpoint: API endpoint path (without base URL) + params: Optional query parameters + json_data: Optional JSON body for POST requests + response_class: Class to use for parsing the response + custom_url: Optional full custom URL to use (overrides endpoint and base URL) + custom_base_url: Optional custom base URL to use instead of self.base_url + + Returns: + Response data in the appropriate format: + - If response_class is provided: Instance of response_class + - Otherwise: Dict[str, Any] containing the JSON response + + Raises: + ValueError: If an unsupported HTTP method is provided + """ + url = self._build_url( + endpoint, custom_url=custom_url, custom_base_url=custom_base_url + ) + response = self._execute_request( + method=method, url=url, params=params, json_data=json_data + ) + + # Parse JSON response with error handling + json_data = self._parse_json_response(response, url) + + # Parse the response based on the specified class + if response_class: + parsed_response: T = response_class.from_dict( + json_data, include_raw_data=self.config.include_raw_data + ) + return parsed_response + + # Return the raw JSON for other requests + return json_data + def paginate_results( self, method_name: str, @@ -785,19 +847,14 @@ def _download_file( Path to downloaded file Raises: - TypeError: If response is not a valid Response object FileExistsError: If file exists and overwrite is False """ - response = self._make_request( + response = self._stream_request( method="GET", endpoint="", - stream=True, custom_url=url, ) - if not isinstance(response, requests.Response): - raise TypeError(f"Expected Response, got {type(response)}") - return self._save_response_to_file( response=response, destination=destination, diff --git a/src/pyUSPTO/clients/petition_decisions.py b/src/pyUSPTO/clients/petition_decisions.py index 806fd7e..9993182 100644 --- a/src/pyUSPTO/clients/petition_decisions.py +++ b/src/pyUSPTO/clients/petition_decisions.py @@ -487,10 +487,9 @@ def download_decisions( return PetitionDecisionDownloadResponse.from_dict(result_dict) else: # For CSV or other formats, get streaming response - result = self._make_request( - method="GET", endpoint=endpoint, params=params, stream=True + result = self._stream_request( + method="GET", endpoint=endpoint, params=params ) - assert isinstance(result, requests.Response) if destination is not None: # Save to file using the base class helper diff --git a/tests/clients/test_base.py b/tests/clients/test_base.py index 67ff593..64782de 100644 --- a/tests/clients/test_base.py +++ b/tests/clients/test_base.py @@ -248,8 +248,8 @@ def test_make_request_with_custom_base_url(self, mock_session: MagicMock) -> Non ) assert result == {"key": "value"} - def test_make_request_with_stream(self, mock_session: MagicMock) -> None: - """Test _make_request method with stream=True.""" + def test_stream_request(self, mock_session: MagicMock) -> None: + """Test _stream_request returns raw Response.""" # Setup client: BaseUSPTOClient[Any] = BaseUSPTOClient(base_url="https://api.test.com") client.config._session = mock_session @@ -257,8 +257,7 @@ def test_make_request_with_stream(self, mock_session: MagicMock) -> None: mock_response = MagicMock() mock_session.get.return_value = mock_response - # Test with stream=True - result = client._make_request(method="GET", endpoint="test", stream=True) + result = client._stream_request(method="GET", endpoint="test") # Verify mock_session.get.assert_called_once_with( diff --git a/tests/clients/test_patent_data_clients.py b/tests/clients/test_patent_data_clients.py index ac194a5..c646c81 100644 --- a/tests/clients/test_patent_data_clients.py +++ b/tests/clients/test_patent_data_clients.py @@ -1142,10 +1142,10 @@ class TestDownloadFile: @patch("pathlib.Path.exists") @patch("builtins.open", new_callable=mock_open) - @patch.object(BaseUSPTOClient, "_make_request") + @patch.object(BaseUSPTOClient, "_stream_request") def test_download_file_success( self, - mock_make_request: MagicMock, + mock_stream_request: MagicMock, mock_file_open: MagicMock, mock_exists: MagicMock, patent_data_client: PatentDataClient, @@ -1157,7 +1157,7 @@ def test_download_file_success( mock_response.headers = {} mock_response.url = url mock_response.iter_content.return_value = [b"chunk1", b"chunk2", b""] - mock_make_request.return_value = mock_response + mock_stream_request.return_value = mock_response mock_exists.return_value = False destination = "/tmp" @@ -1167,9 +1167,9 @@ def test_download_file_success( url, destination=destination, file_name=file_name ) - # Verify _make_request called correctly - mock_make_request.assert_called_once_with( - method="GET", endpoint="", stream=True, custom_url=url + # Verify _stream_request called correctly + mock_stream_request.assert_called_once_with( + method="GET", endpoint="", custom_url=url ) # Verify file operations - use str(Path()) to normalize path for platform @@ -1183,27 +1183,11 @@ def test_download_file_success( assert result == str(expected_path) - @patch.object(BaseUSPTOClient, "_make_request") - def test_download_file_wrong_response_type( - self, - mock_make_request: MagicMock, - patent_data_client: PatentDataClient, - ) -> None: - """Test _download_file raises TypeError when _make_request returns wrong type.""" - # Return a dict instead of Response - mock_make_request.return_value = {"not": "a response"} - - url = "https://example.com/file.pdf" - file_path = "/tmp/test_file.pdf" - - with pytest.raises(TypeError, match="Expected Response, got "): - patent_data_client._download_file(url, file_path) - @patch("builtins.open", new_callable=mock_open) - @patch.object(BaseUSPTOClient, "_make_request") + @patch.object(BaseUSPTOClient, "_stream_request") def test_download_file_filters_empty_chunks( self, - mock_make_request: MagicMock, + mock_stream_request: MagicMock, mock_file_open: MagicMock, patent_data_client: PatentDataClient, ) -> None: @@ -1212,7 +1196,7 @@ def test_download_file_filters_empty_chunks( mock_response.headers = {} mock_response.url = "https://test.com/file" mock_response.iter_content.return_value = [b"data", b"", None, b"more"] - mock_make_request.return_value = mock_response + mock_stream_request.return_value = mock_response patent_data_client._download_file("https://test.com", "/tmp/file") diff --git a/tests/clients/test_petition_decision_clients.py b/tests/clients/test_petition_decision_clients.py index 519bd51..85e6dab 100644 --- a/tests/clients/test_petition_decision_clients.py +++ b/tests/clients/test_petition_decision_clients.py @@ -520,21 +520,19 @@ def test_download_json( def test_download_csv( self, - client_with_mocked_request: tuple[FinalPetitionDecisionsClient, MagicMock], + petition_client: FinalPetitionDecisionsClient, ) -> None: """Test downloading decisions in CSV format as streaming response.""" - client, mock_make_request = client_with_mocked_request - mock_response = MagicMock(spec=requests.Response) mock_response.iter_content.return_value = [b"csv,data"] - mock_make_request.return_value = mock_response - result = client.download_decisions(format="csv") + with patch.object( + petition_client, "_stream_request", return_value=mock_response + ) as mock_stream_request: + result = petition_client.download_decisions(format="csv") - assert isinstance(result, requests.Response) - mock_make_request.assert_called_once() - call_args = mock_make_request.call_args - assert call_args[1]["stream"] is True + assert isinstance(result, requests.Response) + mock_stream_request.assert_called_once() def test_download_csv_to_file( self, @@ -542,14 +540,14 @@ def test_download_csv_to_file( tmp_path, ) -> None: """Test downloading decisions in CSV format and saving to file.""" - # Mock both _make_request and _save_response_to_file + # Mock both _stream_request and _save_response_to_file mock_response = Mock(spec=requests.Response) expected_path = str(tmp_path / "petition_decisions.csv") with ( patch.object( - petition_client, "_make_request", return_value=mock_response - ) as mock_make_request, + petition_client, "_stream_request", return_value=mock_response + ) as mock_stream_request, patch.object( petition_client, "_save_response_to_file", return_value=expected_path ) as mock_save, @@ -562,7 +560,7 @@ def test_download_csv_to_file( ) assert result == expected_path - mock_make_request.assert_called_once() + mock_stream_request.assert_called_once() mock_save.assert_called_once() # Verify _save_response_to_file was called with correct arguments From 7f3254dce64cb41ae8b6a427b50364943cebb984 Mon Sep 17 00:00:00 2001 From: Andrew <3300522+dpieski@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:32:39 -0500 Subject: [PATCH 2/6] fix: requirements-dev --- requirements-dev.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1e1abd6..1fe24c4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -88,10 +88,6 @@ pytest-cov==7.0.0 # via pyuspto pytest-mock==3.15.1 # via pyuspto -pyuspto @ file:///C:/Users/andrewp/Documents/GitHub/pyUSPTO - # via - # pyUSPTO (pyproject.toml) - # pyuspto pyyaml==6.0.3 # via myst-parser requests==2.32.5 From bd44c370134372adad283f4b04477012b4e3e96a Mon Sep 17 00:00:00 2001 From: Andrew <3300522+dpieski@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:59:54 -0500 Subject: [PATCH 3/6] feat: Extract _stream_request and _execute_request from _make_request --- src/pyUSPTO/clients/base.py | 183 ++++++++++++------ src/pyUSPTO/clients/petition_decisions.py | 5 +- tests/clients/test_base.py | 7 +- tests/clients/test_patent_data_clients.py | 34 +--- .../clients/test_petition_decision_clients.py | 24 ++- 5 files changed, 145 insertions(+), 108 deletions(-) diff --git a/src/pyUSPTO/clients/base.py b/src/pyUSPTO/clients/base.py index ece2954..f095acd 100644 --- a/src/pyUSPTO/clients/base.py +++ b/src/pyUSPTO/clients/base.py @@ -166,49 +166,56 @@ def _parse_json_response( ), ) from json_err - def _make_request( + def _build_url( self, - method: str, endpoint: str, + custom_url: str | None = None, + custom_base_url: str | None = None, + ) -> str: + """Build the request URL from endpoint or custom URL. + + Args: + endpoint: API endpoint path (without base URL) + custom_url: Optional full custom URL (overrides endpoint and base URL) + custom_base_url: Optional custom base URL instead of self.base_url + + Returns: + The resolved URL string. + """ + if custom_url: + return custom_url + base = custom_base_url if custom_base_url else self.base_url + return f"{base}/{endpoint.lstrip('/')}" + + def _execute_request( + self, + method: str, + url: str, params: dict[str, Any] | None = None, json_data: dict[str, Any] | None = None, stream: bool = False, - response_class: type[T] | None = None, - custom_url: str | None = None, - custom_base_url: str | None = None, - ) -> dict[str, Any] | T | requests.Response: - """Make an HTTP request to the USPTO API. + ) -> requests.Response: + """Execute an HTTP request and return the raw Response. - Note: Only GET and POST methods are supported. Other HTTP methods will - raise a ValueError. + Handles URL dispatch, error translation, and timeout/connection errors. + Callers are responsible for interpreting the response body. Args: method: HTTP method (GET or POST only) - endpoint: API endpoint path (without base URL) + url: Fully resolved request URL params: Optional query parameters json_data: Optional JSON body for POST requests stream: Whether to stream the response - response_class: Class to use for parsing the response - custom_url: Optional full custom URL to use (overrides endpoint and base URL) - custom_base_url: Optional custom base URL to use instead of self.base_url Returns: - Response data in the appropriate format: - - If stream=True: requests.Response object - - If response_class is provided: Instance of response_class - - Otherwise: Dict[str, Any] containing the JSON response + The raw requests.Response after raise_for_status(). Raises: ValueError: If an unsupported HTTP method is provided + USPTOApiError: On HTTP errors from the API + USPTOTimeout: On request timeout + USPTOConnectionError: On connection failure """ - url: str = "" - if custom_url: - url = custom_url - else: - base = custom_base_url if custom_base_url else self.base_url - url = f"{base}/{endpoint.lstrip('/')}" - - # Get timeout from HTTP config timeout = self.http_config.get_timeout_tuple() try: @@ -228,26 +235,10 @@ def _make_request( raise ValueError(f"Unsupported HTTP method: {method}") response.raise_for_status() - - # Return the raw response for streaming requests - if stream: - return response - - # Parse JSON response with error handling - json_data = self._parse_json_response(response, url) - - # Parse the response based on the specified class - if response_class: - parsed_response: T = response_class.from_dict( - json_data, include_raw_data=self.config.include_raw_data - ) - return parsed_response - - # Return the raw JSON for other requests - return json_data + return response except requests.exceptions.HTTPError as http_err: - client_operation_message = f"API request to '{url}' failed with HTTPError" # 'url' is from _make_request scope + client_operation_message = f"API request to '{url}' failed with HTTPError" # Include request body for POST debugging if method.upper() == "POST" and json_data: @@ -257,7 +248,6 @@ def _make_request( f"\nRequest body sent:\n{json.dumps(json_data, indent=2)}" ) - # Create APIErrorArgs directly from the HTTPError current_error_args = APIErrorArgs.from_http_error( http_error=http_err, client_operation_message=client_operation_message ) @@ -266,7 +256,6 @@ def _make_request( raise api_exception_to_raise from http_err except requests.exceptions.Timeout as timeout_err: - # Specific handling for timeout errors raise USPTOTimeout( message=f"Request to '{url}' timed out", api_short_error="Timeout", @@ -274,31 +263,104 @@ def _make_request( ) from timeout_err except requests.exceptions.ConnectionError as conn_err: - # Specific handling for connection errors (DNS, refused connection, etc.) raise USPTOConnectionError( message=f"Failed to connect to '{url}'", api_short_error="Connection Error", error_details=str(conn_err), ) from conn_err - except ( - requests.exceptions.RequestException - ) as req_err: # Catches other non-HTTP errors from requests - client_operation_message = ( - f"API request to '{url}' failed" # 'url' is from _make_request scope - ) + except requests.exceptions.RequestException as req_err: + client_operation_message = f"API request to '{url}' failed" - # Create APIErrorArgs from the generic RequestException current_error_args = APIErrorArgs.from_request_exception( request_exception=req_err, - client_operation_message=client_operation_message, # or pass None if you prefer default message + client_operation_message=client_operation_message, ) - api_exception_to_raise = get_api_exception( - current_error_args - ) # Will default to USPTOApiError + api_exception_to_raise = get_api_exception(current_error_args) raise api_exception_to_raise from req_err + def _stream_request( + self, + method: str, + endpoint: str, + params: dict[str, Any] | None = None, + json_data: dict[str, Any] | None = None, + custom_url: str | None = None, + custom_base_url: str | None = None, + ) -> requests.Response: + """Make a streaming HTTP request and return the raw Response. + + Args: + method: HTTP method (GET or POST only) + endpoint: API endpoint path (without base URL) + params: Optional query parameters + json_data: Optional JSON body for POST requests + custom_url: Optional full custom URL (overrides endpoint and base URL) + custom_base_url: Optional custom base URL instead of self.base_url + + Returns: + Streaming requests.Response object. + """ + url = self._build_url( + endpoint, custom_url=custom_url, custom_base_url=custom_base_url + ) + return self._execute_request( + method=method, url=url, params=params, json_data=json_data, stream=True + ) + + def _make_request( + self, + method: str, + endpoint: str, + params: dict[str, Any] | None = None, + json_data: dict[str, Any] | None = None, + response_class: type[T] | None = None, + custom_url: str | None = None, + custom_base_url: str | None = None, + ) -> dict[str, Any] | T: + """Make an HTTP request to the USPTO API. + + Note: Only GET and POST methods are supported. Other HTTP methods will + raise a ValueError. + + Args: + method: HTTP method (GET or POST only) + endpoint: API endpoint path (without base URL) + params: Optional query parameters + json_data: Optional JSON body for POST requests + response_class: Class to use for parsing the response + custom_url: Optional full custom URL to use (overrides endpoint and base URL) + custom_base_url: Optional custom base URL to use instead of self.base_url + + Returns: + Response data in the appropriate format: + - If response_class is provided: Instance of response_class + - Otherwise: Dict[str, Any] containing the JSON response + + Raises: + ValueError: If an unsupported HTTP method is provided + """ + url = self._build_url( + endpoint, custom_url=custom_url, custom_base_url=custom_base_url + ) + response = self._execute_request( + method=method, url=url, params=params, json_data=json_data + ) + + # Parse JSON response with error handling + json_data = self._parse_json_response(response, url) + + # Parse the response based on the specified class + if response_class: + parsed_response: T = response_class.from_dict( + json_data, include_raw_data=self.config.include_raw_data + ) + return parsed_response + + # Return the raw JSON for other requests + return json_data + def paginate_results( self, method_name: str, @@ -785,19 +847,14 @@ def _download_file( Path to downloaded file Raises: - TypeError: If response is not a valid Response object FileExistsError: If file exists and overwrite is False """ - response = self._make_request( + response = self._stream_request( method="GET", endpoint="", - stream=True, custom_url=url, ) - if not isinstance(response, requests.Response): - raise TypeError(f"Expected Response, got {type(response)}") - return self._save_response_to_file( response=response, destination=destination, diff --git a/src/pyUSPTO/clients/petition_decisions.py b/src/pyUSPTO/clients/petition_decisions.py index 806fd7e..9993182 100644 --- a/src/pyUSPTO/clients/petition_decisions.py +++ b/src/pyUSPTO/clients/petition_decisions.py @@ -487,10 +487,9 @@ def download_decisions( return PetitionDecisionDownloadResponse.from_dict(result_dict) else: # For CSV or other formats, get streaming response - result = self._make_request( - method="GET", endpoint=endpoint, params=params, stream=True + result = self._stream_request( + method="GET", endpoint=endpoint, params=params ) - assert isinstance(result, requests.Response) if destination is not None: # Save to file using the base class helper diff --git a/tests/clients/test_base.py b/tests/clients/test_base.py index 67ff593..64782de 100644 --- a/tests/clients/test_base.py +++ b/tests/clients/test_base.py @@ -248,8 +248,8 @@ def test_make_request_with_custom_base_url(self, mock_session: MagicMock) -> Non ) assert result == {"key": "value"} - def test_make_request_with_stream(self, mock_session: MagicMock) -> None: - """Test _make_request method with stream=True.""" + def test_stream_request(self, mock_session: MagicMock) -> None: + """Test _stream_request returns raw Response.""" # Setup client: BaseUSPTOClient[Any] = BaseUSPTOClient(base_url="https://api.test.com") client.config._session = mock_session @@ -257,8 +257,7 @@ def test_make_request_with_stream(self, mock_session: MagicMock) -> None: mock_response = MagicMock() mock_session.get.return_value = mock_response - # Test with stream=True - result = client._make_request(method="GET", endpoint="test", stream=True) + result = client._stream_request(method="GET", endpoint="test") # Verify mock_session.get.assert_called_once_with( diff --git a/tests/clients/test_patent_data_clients.py b/tests/clients/test_patent_data_clients.py index ac194a5..c646c81 100644 --- a/tests/clients/test_patent_data_clients.py +++ b/tests/clients/test_patent_data_clients.py @@ -1142,10 +1142,10 @@ class TestDownloadFile: @patch("pathlib.Path.exists") @patch("builtins.open", new_callable=mock_open) - @patch.object(BaseUSPTOClient, "_make_request") + @patch.object(BaseUSPTOClient, "_stream_request") def test_download_file_success( self, - mock_make_request: MagicMock, + mock_stream_request: MagicMock, mock_file_open: MagicMock, mock_exists: MagicMock, patent_data_client: PatentDataClient, @@ -1157,7 +1157,7 @@ def test_download_file_success( mock_response.headers = {} mock_response.url = url mock_response.iter_content.return_value = [b"chunk1", b"chunk2", b""] - mock_make_request.return_value = mock_response + mock_stream_request.return_value = mock_response mock_exists.return_value = False destination = "/tmp" @@ -1167,9 +1167,9 @@ def test_download_file_success( url, destination=destination, file_name=file_name ) - # Verify _make_request called correctly - mock_make_request.assert_called_once_with( - method="GET", endpoint="", stream=True, custom_url=url + # Verify _stream_request called correctly + mock_stream_request.assert_called_once_with( + method="GET", endpoint="", custom_url=url ) # Verify file operations - use str(Path()) to normalize path for platform @@ -1183,27 +1183,11 @@ def test_download_file_success( assert result == str(expected_path) - @patch.object(BaseUSPTOClient, "_make_request") - def test_download_file_wrong_response_type( - self, - mock_make_request: MagicMock, - patent_data_client: PatentDataClient, - ) -> None: - """Test _download_file raises TypeError when _make_request returns wrong type.""" - # Return a dict instead of Response - mock_make_request.return_value = {"not": "a response"} - - url = "https://example.com/file.pdf" - file_path = "/tmp/test_file.pdf" - - with pytest.raises(TypeError, match="Expected Response, got "): - patent_data_client._download_file(url, file_path) - @patch("builtins.open", new_callable=mock_open) - @patch.object(BaseUSPTOClient, "_make_request") + @patch.object(BaseUSPTOClient, "_stream_request") def test_download_file_filters_empty_chunks( self, - mock_make_request: MagicMock, + mock_stream_request: MagicMock, mock_file_open: MagicMock, patent_data_client: PatentDataClient, ) -> None: @@ -1212,7 +1196,7 @@ def test_download_file_filters_empty_chunks( mock_response.headers = {} mock_response.url = "https://test.com/file" mock_response.iter_content.return_value = [b"data", b"", None, b"more"] - mock_make_request.return_value = mock_response + mock_stream_request.return_value = mock_response patent_data_client._download_file("https://test.com", "/tmp/file") diff --git a/tests/clients/test_petition_decision_clients.py b/tests/clients/test_petition_decision_clients.py index 519bd51..85e6dab 100644 --- a/tests/clients/test_petition_decision_clients.py +++ b/tests/clients/test_petition_decision_clients.py @@ -520,21 +520,19 @@ def test_download_json( def test_download_csv( self, - client_with_mocked_request: tuple[FinalPetitionDecisionsClient, MagicMock], + petition_client: FinalPetitionDecisionsClient, ) -> None: """Test downloading decisions in CSV format as streaming response.""" - client, mock_make_request = client_with_mocked_request - mock_response = MagicMock(spec=requests.Response) mock_response.iter_content.return_value = [b"csv,data"] - mock_make_request.return_value = mock_response - result = client.download_decisions(format="csv") + with patch.object( + petition_client, "_stream_request", return_value=mock_response + ) as mock_stream_request: + result = petition_client.download_decisions(format="csv") - assert isinstance(result, requests.Response) - mock_make_request.assert_called_once() - call_args = mock_make_request.call_args - assert call_args[1]["stream"] is True + assert isinstance(result, requests.Response) + mock_stream_request.assert_called_once() def test_download_csv_to_file( self, @@ -542,14 +540,14 @@ def test_download_csv_to_file( tmp_path, ) -> None: """Test downloading decisions in CSV format and saving to file.""" - # Mock both _make_request and _save_response_to_file + # Mock both _stream_request and _save_response_to_file mock_response = Mock(spec=requests.Response) expected_path = str(tmp_path / "petition_decisions.csv") with ( patch.object( - petition_client, "_make_request", return_value=mock_response - ) as mock_make_request, + petition_client, "_stream_request", return_value=mock_response + ) as mock_stream_request, patch.object( petition_client, "_save_response_to_file", return_value=expected_path ) as mock_save, @@ -562,7 +560,7 @@ def test_download_csv_to_file( ) assert result == expected_path - mock_make_request.assert_called_once() + mock_stream_request.assert_called_once() mock_save.assert_called_once() # Verify _save_response_to_file was called with correct arguments From 8c10871be75be21ebb8e3b336f5a9a795ecf133a Mon Sep 17 00:00:00 2001 From: Andrew <3300522+dpieski@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:32:39 -0500 Subject: [PATCH 4/6] fix: requirements-dev --- requirements-dev.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index d4126da..598e1a5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -88,10 +88,6 @@ pytest-cov==7.0.0 # via pyuspto pytest-mock==3.15.1 # via pyuspto -pyuspto @ file:///C:/Users/andrewp/Documents/GitHub/pyUSPTO - # via - # pyUSPTO (pyproject.toml) - # pyuspto pyyaml==6.0.3 # via myst-parser requests==2.32.5 From f114bd3b40f420af3e8f40016031e3bf4863ab70 Mon Sep 17 00:00:00 2001 From: Andrew <3300522+dpieski@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:16:31 -0500 Subject: [PATCH 5/6] fix: Split _make_request into _get_model and _get_json --- src/pyUSPTO/clients/base.py | 77 ++- src/pyUSPTO/clients/bulk_data.py | 13 +- src/pyUSPTO/clients/patent_data.py | 67 +- src/pyUSPTO/clients/petition_decisions.py | 24 +- src/pyUSPTO/clients/ptab_appeals.py | 10 +- src/pyUSPTO/clients/ptab_interferences.py | 10 +- src/pyUSPTO/clients/ptab_trials.py | 22 +- src/pyUSPTO/models/patent_data.py | 12 +- src/pyUSPTO/models/petition_decisions.py | 9 +- tests/clients/test_base.py | 130 ++-- tests/clients/test_bulk_data_clients.py | 6 +- tests/clients/test_patent_data_clients.py | 573 ++++++++++-------- .../clients/test_petition_decision_clients.py | 218 ++++--- 13 files changed, 651 insertions(+), 520 deletions(-) diff --git a/src/pyUSPTO/clients/base.py b/src/pyUSPTO/clients/base.py index f095acd..cf900e6 100644 --- a/src/pyUSPTO/clients/base.py +++ b/src/pyUSPTO/clients/base.py @@ -14,6 +14,10 @@ runtime_checkable, ) +try: + from typing import Self +except ImportError: + from typing_extensions import Self import requests from pyUSPTO.config import USPTOConfig @@ -31,7 +35,7 @@ class FromDictProtocol(Protocol): """Protocol for classes that can be created from a dictionary.""" @classmethod - def from_dict(cls, data: dict[str, Any], include_raw_data: bool = False) -> Any: + def from_dict(cls, data: dict[str, Any], include_raw_data: bool = False) -> Self: """Create an object from a dictionary.""" ... @@ -39,6 +43,9 @@ def from_dict(cls, data: dict[str, Any], include_raw_data: bool = False) -> Any: # Type variable for response classes T = TypeVar("T", bound=FromDictProtocol) +# Type variable for response classes +M = TypeVar("M", bound=FromDictProtocol) + class BaseUSPTOClient(Generic[T]): """Base client class for USPTO API clients.""" @@ -309,37 +316,29 @@ def _stream_request( method=method, url=url, params=params, json_data=json_data, stream=True ) - def _make_request( + def _get_model( self, method: str, endpoint: str, + response_class: type[M], params: dict[str, Any] | None = None, json_data: dict[str, Any] | None = None, - response_class: type[T] | None = None, custom_url: str | None = None, custom_base_url: str | None = None, - ) -> dict[str, Any] | T: - """Make an HTTP request to the USPTO API. - - Note: Only GET and POST methods are supported. Other HTTP methods will - raise a ValueError. + ) -> M: + """Make an HTTP request and parse the response into a model. Args: method: HTTP method (GET or POST only) endpoint: API endpoint path (without base URL) + response_class: Class to use for parsing the response params: Optional query parameters json_data: Optional JSON body for POST requests - response_class: Class to use for parsing the response - custom_url: Optional full custom URL to use (overrides endpoint and base URL) - custom_base_url: Optional custom base URL to use instead of self.base_url + custom_url: Optional full custom URL (overrides endpoint and base URL) + custom_base_url: Optional custom base URL instead of self.base_url Returns: - Response data in the appropriate format: - - If response_class is provided: Instance of response_class - - Otherwise: Dict[str, Any] containing the JSON response - - Raises: - ValueError: If an unsupported HTTP method is provided + Instance of response_class parsed from the JSON response. """ url = self._build_url( endpoint, custom_url=custom_url, custom_base_url=custom_base_url @@ -347,19 +346,43 @@ def _make_request( response = self._execute_request( method=method, url=url, params=params, json_data=json_data ) + data = self._parse_json_response(response, url) - # Parse JSON response with error handling - json_data = self._parse_json_response(response, url) + ret = response_class.from_dict( + data, include_raw_data=self.config.include_raw_data + ) + assert isinstance(ret, response_class) + return ret - # Parse the response based on the specified class - if response_class: - parsed_response: T = response_class.from_dict( - json_data, include_raw_data=self.config.include_raw_data - ) - return parsed_response + def _get_json( + self, + method: str, + endpoint: str, + params: dict[str, Any] | None = None, + json_data: dict[str, Any] | None = None, + custom_url: str | None = None, + custom_base_url: str | None = None, + ) -> dict[str, Any]: + """Make an HTTP request and return the parsed JSON response. + + Args: + method: HTTP method (GET or POST only) + endpoint: API endpoint path (without base URL) + params: Optional query parameters + json_data: Optional JSON body for POST requests + custom_url: Optional full custom URL (overrides endpoint and base URL) + custom_base_url: Optional custom base URL instead of self.base_url - # Return the raw JSON for other requests - return json_data + Returns: + Dict containing the JSON response. + """ + url = self._build_url( + endpoint, custom_url=custom_url, custom_base_url=custom_base_url + ) + response = self._execute_request( + method=method, url=url, params=params, json_data=json_data + ) + return self._parse_json_response(response, url) def paginate_results( self, diff --git a/src/pyUSPTO/clients/bulk_data.py b/src/pyUSPTO/clients/bulk_data.py index bb6bc52..2e69536 100644 --- a/src/pyUSPTO/clients/bulk_data.py +++ b/src/pyUSPTO/clients/bulk_data.py @@ -109,13 +109,12 @@ def get_product_by_id( params["latest"] = str(latest).lower() # Use response_class for clean parsing - response = self._make_request( + response = self._get_model( method="GET", endpoint=endpoint, - params=params if params else None, response_class=BulkDataResponse, + params=params if params else None, ) - assert isinstance(response, BulkDataResponse) # Extract the product from response if response.bulk_data_product_bag: @@ -251,13 +250,9 @@ def search_products( if fields is not None: params["fields"] = ",".join(fields) - result = self._make_request( + return self._get_model( method="GET", endpoint=self.ENDPOINTS["products_search"], - params=params, response_class=BulkDataResponse, + params=params, ) - - # Since we specified response_class=BulkDataResponse, the result should be a BulkDataResponse - assert isinstance(result, BulkDataResponse) - return result diff --git a/src/pyUSPTO/clients/patent_data.py b/src/pyUSPTO/clients/patent_data.py index 656c0cd..61f44d7 100644 --- a/src/pyUSPTO/clients/patent_data.py +++ b/src/pyUSPTO/clients/patent_data.py @@ -263,12 +263,12 @@ def search_applications( endpoint = self.ENDPOINTS["search_applications"] if post_body is not None: - result = self._make_request( + result = self._get_model( method="POST", endpoint=endpoint, + response_class=PatentDataResponse, json_data=post_body, params=additional_query_params, - response_class=PatentDataResponse, ) else: params: dict[str, Any] = {} @@ -366,13 +366,12 @@ def search_applications( if additional_query_params: params.update(additional_query_params) - result = self._make_request( + result = self._get_model( method="GET", endpoint=endpoint, - params=params, response_class=PatentDataResponse, + params=params, ) - assert isinstance(result, PatentDataResponse) return result def get_search_results( @@ -408,7 +407,7 @@ def get_search_results( if "format" not in post_body: post_body["format"] = "json" - result = self._make_request( + result = self._get_json( method="POST", endpoint=endpoint, json_data=post_body, @@ -504,12 +503,11 @@ def get_search_results( if additional_query_params: params.update(additional_query_params) - result = self._make_request( + result = self._get_json( method="GET", endpoint=endpoint, params=params, ) - assert isinstance(result, dict) amd_list = [ ApplicationMetaData.from_dict(item["applicationMetaData"]) for item in result["patentdata"] @@ -541,14 +539,14 @@ def get_application_by_number( endpoint = self.ENDPOINTS["get_application_by_number"].format( application_number=self.sanitize_application_number(application_number) ) - response_data = self._make_request( + response_data = self._get_model( method="GET", endpoint=endpoint, response_class=PatentDataResponse ) - assert isinstance(response_data, PatentDataResponse) - return self._get_wrapper_from_response( + ret = self._get_wrapper_from_response( response_data=response_data, application_number_for_validation=application_number, ) + return ret def get_application_metadata( self, application_number: str @@ -575,10 +573,9 @@ def get_application_metadata( endpoint = self.ENDPOINTS["get_application_metadata"].format( application_number=self.sanitize_application_number(application_number) ) - response_data = self._make_request( + response_data = self._get_model( method="GET", endpoint=endpoint, response_class=PatentDataResponse ) - assert isinstance(response_data, PatentDataResponse) wrapper = self._get_wrapper_from_response(response_data, application_number) return wrapper.application_meta_data if wrapper else None @@ -606,10 +603,9 @@ def get_application_adjustment( endpoint = self.ENDPOINTS["get_application_adjustment"].format( application_number=self.sanitize_application_number(application_number) ) - response_data = self._make_request( + response_data = self._get_model( method="GET", endpoint=endpoint, response_class=PatentDataResponse ) - assert isinstance(response_data, PatentDataResponse) wrapper = self._get_wrapper_from_response(response_data, application_number) return wrapper.patent_term_adjustment_data if wrapper else None @@ -638,10 +634,9 @@ def get_application_assignment( endpoint = self.ENDPOINTS["get_application_assignment"].format( application_number=self.sanitize_application_number(application_number) ) - response_data = self._make_request( + response_data = self._get_model( method="GET", endpoint=endpoint, response_class=PatentDataResponse ) - assert isinstance(response_data, PatentDataResponse) wrapper = self._get_wrapper_from_response(response_data, application_number) return wrapper.assignment_bag if wrapper else None @@ -668,10 +663,9 @@ def get_application_attorney( endpoint = self.ENDPOINTS["get_application_attorney"].format( application_number=self.sanitize_application_number(application_number) ) - response_data = self._make_request( + response_data = self._get_model( method="GET", endpoint=endpoint, response_class=PatentDataResponse ) - assert isinstance(response_data, PatentDataResponse) wrapper = self._get_wrapper_from_response(response_data, application_number) return wrapper.record_attorney if wrapper else None @@ -701,10 +695,9 @@ def get_application_continuity( endpoint = self.ENDPOINTS["get_application_continuity"].format( application_number=self.sanitize_application_number(application_number) ) - response_data = self._make_request( + response_data = self._get_model( method="GET", endpoint=endpoint, response_class=PatentDataResponse ) - assert isinstance(response_data, PatentDataResponse) wrapper = self._get_wrapper_from_response(response_data, application_number) return ApplicationContinuityData.from_wrapper(wrapper) if wrapper else None @@ -733,10 +726,9 @@ def get_application_foreign_priority( endpoint = self.ENDPOINTS["get_application_foreign_priority"].format( application_number=self.sanitize_application_number(application_number) ) - response_data = self._make_request( + response_data = self._get_model( method="GET", endpoint=endpoint, response_class=PatentDataResponse ) - assert isinstance(response_data, PatentDataResponse) wrapper = self._get_wrapper_from_response(response_data, application_number) return wrapper.foreign_priority_bag if wrapper else None @@ -765,10 +757,9 @@ def get_application_transactions( endpoint = self.ENDPOINTS["get_application_transactions"].format( application_number=self.sanitize_application_number(application_number) ) - response_data = self._make_request( + response_data = self._get_model( method="GET", endpoint=endpoint, response_class=PatentDataResponse ) - assert isinstance(response_data, PatentDataResponse) wrapper = self._get_wrapper_from_response(response_data, application_number) return wrapper.event_data_bag if wrapper else None @@ -819,11 +810,12 @@ def get_application_documents( if official_date_to: params["officialDateTo"] = official_date_to - result_dict = self._make_request( - method="GET", endpoint=endpoint, params=params if params else None + return self._get_model( + method="GET", + endpoint=endpoint, + response_class=DocumentBag, + params=params if params else None, ) - assert isinstance(result_dict, dict) - return DocumentBag.from_dict(result_dict) def get_application_associated_documents( self, application_number: str @@ -855,10 +847,9 @@ def get_application_associated_documents( endpoint = self.ENDPOINTS["get_application_associated_documents"].format( application_number=self.sanitize_application_number(application_number) ) - response_data = self._make_request( + response_data = self._get_model( method="GET", endpoint=endpoint, response_class=PatentDataResponse ) - assert isinstance(response_data, PatentDataResponse) wrapper = self._get_wrapper_from_response(response_data, application_number) return PrintedPublication.from_wrapper(wrapper) if wrapper else None @@ -935,11 +926,12 @@ def get_status_codes( status codes, a `StatusCodeCollection` of the `StatusCode` objects (code and description), and a request identifier. """ - result_dict = self._make_request( - method="GET", endpoint=self.ENDPOINTS["status_codes"], params=params + return self._get_model( + method="GET", + endpoint=self.ENDPOINTS["status_codes"], + response_class=StatusCodeSearchResponse, + params=params, ) - assert isinstance(result_dict, dict) - return StatusCodeSearchResponse.from_dict(result_dict) def search_status_codes( self, search_request: dict[str, Any] @@ -962,13 +954,12 @@ def search_status_codes( status codes, a `StatusCodeCollection` of the `StatusCode` objects (code and description), and a request identifier. """ - result_dict = self._make_request( + return self._get_model( method="POST", endpoint=self.ENDPOINTS["status_codes"], + response_class=StatusCodeSearchResponse, json_data=search_request, ) - assert isinstance(result_dict, dict) - return StatusCodeSearchResponse.from_dict(result_dict) def download_document( self, diff --git a/src/pyUSPTO/clients/petition_decisions.py b/src/pyUSPTO/clients/petition_decisions.py index 9993182..ec20143 100644 --- a/src/pyUSPTO/clients/petition_decisions.py +++ b/src/pyUSPTO/clients/petition_decisions.py @@ -184,12 +184,12 @@ def search_decisions( if post_body is not None: # POST request path - result = self._make_request( + result = self._get_model( method="POST", endpoint=endpoint, + response_class=PetitionDecisionResponse, json_data=post_body, params=additional_query_params, - response_class=PetitionDecisionResponse, ) else: # GET request path @@ -280,14 +280,12 @@ def search_decisions( if additional_query_params: params.update(additional_query_params) - result = self._make_request( + result = self._get_model( method="GET", endpoint=endpoint, - params=params, response_class=PetitionDecisionResponse, + params=params, ) - - assert isinstance(result, PetitionDecisionResponse) return result def get_decision_by_id( @@ -326,13 +324,12 @@ def get_decision_by_id( if include_documents is not None: params["includeDocuments"] = str(include_documents).lower() - response_data = self._make_request( + response_data = self._get_model( method="GET", endpoint=endpoint, - params=params if params else None, response_class=PetitionDecisionResponse, + params=params if params else None, ) - assert isinstance(response_data, PetitionDecisionResponse) return self._get_decision_from_response( response_data=response_data, petition_decision_record_identifier_for_validation=petition_decision_record_identifier, @@ -480,11 +477,12 @@ def download_decisions( if format.lower() == "json": # For JSON, parse the response - result_dict = self._make_request( - method="GET", endpoint=endpoint, params=params + return self._get_model( + method="GET", + endpoint=endpoint, + response_class=PetitionDecisionDownloadResponse, + params=params, ) - assert isinstance(result_dict, dict) - return PetitionDecisionDownloadResponse.from_dict(result_dict) else: # For CSV or other formats, get streaming response result = self._stream_request( diff --git a/src/pyUSPTO/clients/ptab_appeals.py b/src/pyUSPTO/clients/ptab_appeals.py index 0ca6117..6c90bfa 100644 --- a/src/pyUSPTO/clients/ptab_appeals.py +++ b/src/pyUSPTO/clients/ptab_appeals.py @@ -133,12 +133,12 @@ def search_decisions( if post_body is not None: # POST request path - result = self._make_request( + result = self._get_model( method="POST", endpoint=endpoint, + response_class=PTABAppealResponse, json_data=post_body, params=additional_query_params, - response_class=PTABAppealResponse, ) else: # GET request path @@ -207,14 +207,12 @@ def search_decisions( if additional_query_params: params.update(additional_query_params) - result = self._make_request( + result = self._get_model( method="GET", endpoint=endpoint, - params=params, response_class=PTABAppealResponse, + params=params, ) - - assert isinstance(result, PTABAppealResponse) return result def paginate_decisions( diff --git a/src/pyUSPTO/clients/ptab_interferences.py b/src/pyUSPTO/clients/ptab_interferences.py index 795ac9b..193878d 100644 --- a/src/pyUSPTO/clients/ptab_interferences.py +++ b/src/pyUSPTO/clients/ptab_interferences.py @@ -137,12 +137,12 @@ def search_decisions( if post_body is not None: # POST request path - result = self._make_request( + result = self._get_model( method="POST", endpoint=endpoint, + response_class=PTABInterferenceResponse, json_data=post_body, params=additional_query_params, - response_class=PTABInterferenceResponse, ) else: # GET request path @@ -222,14 +222,12 @@ def search_decisions( if additional_query_params: params.update(additional_query_params) - result = self._make_request( + result = self._get_model( method="GET", endpoint=endpoint, - params=params, response_class=PTABInterferenceResponse, + params=params, ) - - assert isinstance(result, PTABInterferenceResponse) return result def paginate_decisions( diff --git a/src/pyUSPTO/clients/ptab_trials.py b/src/pyUSPTO/clients/ptab_trials.py index c8ed371..550433d 100644 --- a/src/pyUSPTO/clients/ptab_trials.py +++ b/src/pyUSPTO/clients/ptab_trials.py @@ -8,7 +8,7 @@ from collections.abc import Iterator from typing import Any -from pyUSPTO.clients.base import BaseUSPTOClient +from pyUSPTO.clients.base import BaseUSPTOClient, M from pyUSPTO.config import USPTOConfig from pyUSPTO.models.ptab import ( PTABTrialDocumentResponse, @@ -70,7 +70,7 @@ def __init__( def _perform_search( self, endpoint_key: str, - response_class: Any, + response_class: type[M], query: str | None, query_parts: list[str], post_body: dict[str, Any] | None, @@ -82,7 +82,7 @@ def _perform_search( filters: str | None, range_filters: str | None, additional_params: dict[str, Any] | None, - ) -> PTABTrialProceedingResponse | PTABTrialDocumentResponse: + ) -> M: """Execute a PTAB trial search request using GET or POST. If a POST body is provided, perform a POST request; otherwise, build @@ -92,14 +92,13 @@ def _perform_search( # Handle POST request if post_body is not None: - result = self._make_request( + return self._get_model( method="POST", endpoint=endpoint, + response_class=response_class, json_data=post_body, params=additional_params, - response_class=response_class, ) - return result # type: ignore # Handle GET request params: dict[str, Any] = {} @@ -129,13 +128,12 @@ def _perform_search( if additional_params: params.update(additional_params) - result = self._make_request( + return self._get_model( method="GET", endpoint=endpoint, - params=params, response_class=response_class, + params=params, ) - return result # type: ignore def search_proceedings( self, @@ -245,7 +243,7 @@ def search_proceedings( filters=filters, range_filters=range_filters, additional_params=additional_query_params, - ) # type: ignore + ) def search_documents( self, @@ -361,7 +359,7 @@ def search_documents( filters=filters, range_filters=range_filters, additional_params=additional_query_params, - ) # type: ignore + ) def search_decisions( self, @@ -489,7 +487,7 @@ def search_decisions( filters=filters, range_filters=range_filters, additional_params=additional_query_params, - ) # type: ignore + ) def paginate_proceedings( self, post_body: dict[str, Any] | None = None, **kwargs: Any diff --git a/src/pyUSPTO/models/patent_data.py b/src/pyUSPTO/models/patent_data.py index 36fde89..2d242a0 100644 --- a/src/pyUSPTO/models/patent_data.py +++ b/src/pyUSPTO/models/patent_data.py @@ -68,7 +68,7 @@ def _missing_(cls, value: Any) -> "ActiveIndicator": return cls.FALSE if val_upper == "ACTIVE": return cls.ACTIVE - return super()._missing_(value=value) # type: ignore[no-any-return] + raise ValueError(f"{value!r} is not a valid {cls.__name__}") class DocumentMimeType(str, Enum): @@ -423,7 +423,9 @@ def filter_by_format(self, format: str | DocumentMimeType) -> "DocumentBag": return DocumentBag(list(filtered)) @classmethod - def from_dict(cls, data: dict[str, Any]) -> "DocumentBag": + def from_dict( + cls, data: dict[str, Any], include_raw_data: bool = False + ) -> "DocumentBag": """Create a `DocumentBag` instance from a dictionary representation. Expects a dictionary with a "documentBag" key containing a list of @@ -432,6 +434,7 @@ def from_dict(cls, data: dict[str, Any]) -> "DocumentBag": Args: data (Dict[str, Any]): A dictionary, typically from an API response, containing the document bag. + include_raw_data: Unused. Present for FromDictProtocol conformance. Returns: DocumentBag: An instance of `DocumentBag`. @@ -2526,11 +2529,14 @@ class StatusCodeSearchResponse: request_identifier: str | None = None @classmethod - def from_dict(cls, data: dict[str, Any]) -> "StatusCodeSearchResponse": + def from_dict( + cls, data: dict[str, Any], include_raw_data: bool = False + ) -> "StatusCodeSearchResponse": """Create a `StatusCodeSearchResponse` instance from a dictionary. Args: data (Dict[str, Any]): Dictionary with API response data for status codes. + include_raw_data: Unused. Present for FromDictProtocol conformance. Returns: StatusCodeSearchResponse: An instance of `StatusCodeSearchResponse`. diff --git a/src/pyUSPTO/models/petition_decisions.py b/src/pyUSPTO/models/petition_decisions.py index c15365c..f60c78a 100644 --- a/src/pyUSPTO/models/petition_decisions.py +++ b/src/pyUSPTO/models/petition_decisions.py @@ -44,7 +44,7 @@ def _missing_(cls, value: Any) -> "DecisionTypeCode": return cls.DENIED # if val_upper == "DISMISSED": # return cls.DISMISSED - return super()._missing_(value=value) # type: ignore[no-any-return] + raise ValueError(f"{value!r} is not a valid {cls.__name__}") class DocumentDirectionCategory(Enum): @@ -62,7 +62,7 @@ def _missing_(cls, value: Any) -> "DocumentDirectionCategory": return cls.INCOMING if val_upper == "OUTGOING": return cls.OUTGOING - return super()._missing_(value=value) # type: ignore[no-any-return] + raise ValueError(f"{value!r} is not a valid {cls.__name__}") # --- Data Models --- @@ -468,11 +468,14 @@ class PetitionDecisionDownloadResponse: petition_decision_data: list[PetitionDecision] = field(default_factory=list) @classmethod - def from_dict(cls, data: dict[str, Any]) -> "PetitionDecisionDownloadResponse": + def from_dict( + cls, data: dict[str, Any], include_raw_data: bool = False + ) -> "PetitionDecisionDownloadResponse": """Create a PetitionDecisionDownloadResponse instance from a dictionary. Args: data: Dictionary containing download API response data. + include_raw_data: Unused. Present for FromDictProtocol conformance. Returns: PetitionDecisionDownloadResponse: An instance of PetitionDecisionDownloadResponse. diff --git a/tests/clients/test_base.py b/tests/clients/test_base.py index 64782de..cfa8338 100644 --- a/tests/clients/test_base.py +++ b/tests/clients/test_base.py @@ -101,6 +101,39 @@ def from_dict( return instance +class TestImportFallback: + """Tests for typing import fallback logic.""" + + def test_import_fallback_logic(self) -> None: + """Tests that the module falls back to typing_extensions.Self if typing.Self fails.""" + import builtins + import importlib + import sys + + import typing_extensions + + module_name = "pyUSPTO.clients.base" + + if module_name in sys.modules: + del sys.modules[module_name] + + original_import = builtins.__import__ + + def mock_import(name, globals=None, locals=None, fromlist=(), level=0): + if name == "typing" and "Self" in fromlist: + raise ImportError("Simulated missing Self in typing") + return original_import(name, globals, locals, fromlist, level) + + with patch("builtins.__import__", side_effect=mock_import): + base_module = importlib.import_module(module_name) + + assert base_module.Self is typing_extensions.Self + + if module_name in sys.modules: + del sys.modules[module_name] + importlib.import_module(module_name) + + class TestBaseUSPTOClient: """Tests for the BaseUSPTOClient class.""" @@ -149,8 +182,8 @@ def test_retry_configuration(self) -> None: assert cast(HTTPAdapter, http_adapter).max_retries.total == 3 assert cast(HTTPAdapter, http_adapter).max_retries.backoff_factor == 2 - def test_make_request_get(self, mock_session: MagicMock) -> None: - """Test _make_request method with GET.""" + def test_get_json_get(self, mock_session: MagicMock) -> None: + """Test _get_json method with GET.""" # Setup client: BaseUSPTOClient[Any] = BaseUSPTOClient(base_url="https://api.test.com") client.config._session = mock_session @@ -160,7 +193,7 @@ def test_make_request_get(self, mock_session: MagicMock) -> None: mock_session.get.return_value = mock_response # Test GET request - result = client._make_request( + result = client._get_json( method="GET", endpoint="test", params={"param": "value"} ) @@ -173,8 +206,8 @@ def test_make_request_get(self, mock_session: MagicMock) -> None: ) assert result == {"key": "value"} - def test_make_request_post(self, mock_session: MagicMock) -> None: - """Test _make_request method with POST.""" + def test_get_json_post(self, mock_session: MagicMock) -> None: + """Test _get_json method with POST.""" # Setup client: BaseUSPTOClient[Any] = BaseUSPTOClient(base_url="https://api.test.com") client.config._session = mock_session @@ -184,7 +217,7 @@ def test_make_request_post(self, mock_session: MagicMock) -> None: mock_session.post.return_value = mock_response # Test POST request - result = client._make_request( + result = client._get_json( method="POST", endpoint="test", params={"param": "value"}, @@ -201,8 +234,8 @@ def test_make_request_post(self, mock_session: MagicMock) -> None: ) assert result == {"key": "value"} - def test_make_request_with_response_class(self, mock_session: MagicMock) -> None: - """Test _make_request method with response_class.""" + def test_get_model(self, mock_session: MagicMock) -> None: + """Test _get_model method parses response into model class.""" # Setup client: BaseUSPTOClient[Any] = BaseUSPTOClient(base_url="https://api.test.com") client.config._session = mock_session @@ -212,7 +245,7 @@ def test_make_request_with_response_class(self, mock_session: MagicMock) -> None mock_session.get.return_value = mock_response # Test with response_class - result = client._make_request( + result = client._get_model( method="GET", endpoint="test", response_class=TestResponseClass, @@ -222,8 +255,8 @@ def test_make_request_with_response_class(self, mock_session: MagicMock) -> None assert isinstance(result, TestResponseClass) assert result.data == {"key": "value"} - def test_make_request_with_custom_base_url(self, mock_session: MagicMock) -> None: - """Test _make_request method with custom_base_url.""" + def test_get_json_with_custom_url(self, mock_session: MagicMock) -> None: + """Test _get_json method with custom_url.""" # Setup client: BaseUSPTOClient[Any] = BaseUSPTOClient(base_url="https://api.test.com") client.config._session = mock_session @@ -232,8 +265,8 @@ def test_make_request_with_custom_base_url(self, mock_session: MagicMock) -> Non mock_response.json.return_value = {"key": "value"} mock_session.get.return_value = mock_response - # Test with custom_base_url - result = client._make_request( + # Test with custom_url + result = client._get_json( method="GET", endpoint="test", custom_url="https://custom.api.test.com", @@ -269,15 +302,16 @@ def test_stream_request(self, mock_session: MagicMock) -> None: assert result == mock_response mock_response.json.assert_not_called() - def test_make_request_invalid_method(self, mock_session: MagicMock) -> None: - """Test _make_request method with invalid HTTP method.""" + def test_execute_request_invalid_method(self, mock_session: MagicMock) -> None: + """Test _execute_request with invalid HTTP method.""" client: BaseUSPTOClient[Any] = BaseUSPTOClient(base_url="https://api.test.com") # Test with invalid method with pytest.raises(ValueError, match="Unsupported HTTP method: DELETE"): - client._make_request(method="DELETE", endpoint="test") + client._execute_request(method="DELETE", url="https://api.test.com/test") # Test catch-all error case with unknown status code + client.config._session = mock_session mock_response = MagicMock() mock_response.status_code = ( 418 # I'm a teapot (unused status in the specific handlers) @@ -291,13 +325,13 @@ def test_make_request_invalid_method(self, mock_session: MagicMock) -> None: ) with pytest.raises(USPTOApiError) as excinfo: - client._make_request(method="GET", endpoint="test") + client._execute_request(method="GET", url="https://api.test.com/test") assert "I'm a teapot" in str(excinfo.value) assert excinfo.value.error_details == "I'm a teapot" assert excinfo.value.status_code == 418 - def test_make_request_http_errors(self, mock_session: MagicMock) -> None: - """Test _make_request method with HTTP errors.""" + def test_execute_request_http_errors(self, mock_session: MagicMock) -> None: + """Test _execute_request with HTTP errors.""" # Setup client: BaseUSPTOClient[Any] = BaseUSPTOClient(base_url="https://api.test.com") client.config._session = mock_session @@ -314,7 +348,7 @@ def test_make_request_http_errors(self, mock_session: MagicMock) -> None: ) with pytest.raises(USPTOApiBadRequestError) as excinfo: - client._make_request(method="GET", endpoint="test") + client._execute_request(method="GET", url="https://api.test.com/test") assert "Invalid request parameters" in str(excinfo.value) assert excinfo.value.error_details == "Invalid request parameters" assert excinfo.value.request_identifier == "req-400" @@ -326,7 +360,7 @@ def test_make_request_http_errors(self, mock_session: MagicMock) -> None: "requestIdentifier": "req-401", } with pytest.raises(expected_exception=USPTOApiAuthError) as excinfo: - client._make_request(method="GET", endpoint="test") + client._execute_request(method="GET", url="https://api.test.com/test") assert "Authentication failed" in str(excinfo.value) assert excinfo.value.error_details == "Authentication failed" assert excinfo.value.request_identifier == "req-401" @@ -338,7 +372,7 @@ def test_make_request_http_errors(self, mock_session: MagicMock) -> None: "requestIdentifier": "req-403", } with pytest.raises(USPTOApiAuthError) as excinfo: - client._make_request(method="GET", endpoint="test") + client._execute_request(method="GET", url="https://api.test.com/test") assert "Access forbidden" in str(excinfo.value) assert excinfo.value.error_details == "Access forbidden" assert excinfo.value.request_identifier == "req-403" @@ -350,7 +384,7 @@ def test_make_request_http_errors(self, mock_session: MagicMock) -> None: "requestIdentifier": "req-404", } with pytest.raises(USPTOApiNotFoundError) as excinfo: - client._make_request(method="GET", endpoint="test") + client._execute_request(method="GET", url="https://api.test.com/test") assert "Resource not found" in str(excinfo.value) assert excinfo.value.error_details == "Resource not found" assert excinfo.value.request_identifier == "req-404" @@ -363,7 +397,7 @@ def test_make_request_http_errors(self, mock_session: MagicMock) -> None: "requestIdentifier": "req-413", } with pytest.raises(expected_exception=USPTOApiPayloadTooLargeError) as excinfo: - client._make_request(method="GET", endpoint="test") + client._execute_request(method="GET", url="https://api.test.com/test") assert "Payload Too Large" in str(excinfo.value) assert excinfo.value.error_details == "Request entity too large." assert excinfo.value.request_identifier == "req-413" @@ -375,7 +409,7 @@ def test_make_request_http_errors(self, mock_session: MagicMock) -> None: "requestIdentifier": "req-429", } with pytest.raises(USPTOApiRateLimitError) as excinfo: - client._make_request(method="GET", endpoint="test") + client._execute_request(method="GET", url="https://api.test.com/test") assert "Rate limit exceeded" in str(excinfo.value) assert excinfo.value.error_details == "Rate limit exceeded" assert excinfo.value.request_identifier == "req-429" @@ -387,7 +421,7 @@ def test_make_request_http_errors(self, mock_session: MagicMock) -> None: "requestIdentifier": "req-500", } with pytest.raises(USPTOApiServerError) as excinfo: - client._make_request(method="GET", endpoint="test") + client._execute_request(method="GET", url="https://api.test.com/test") assert "Internal server error" in str(excinfo.value) assert excinfo.value.error_details == "Internal server error" assert excinfo.value.request_identifier == "req-500" @@ -399,7 +433,7 @@ def test_make_request_http_errors(self, mock_session: MagicMock) -> None: "requestIdentifier": "req-500-alt", } with pytest.raises(USPTOApiServerError) as excinfo: - client._make_request(method="GET", endpoint="test") + client._execute_request(method="GET", url="https://api.test.com/test") assert "Alternative error format" in str(object=excinfo.value) assert excinfo.value.error_details == "Alternative error format" assert excinfo.value.request_identifier == "req-500-alt" @@ -408,11 +442,11 @@ def test_make_request_http_errors(self, mock_session: MagicMock) -> None: mock_response.json.side_effect = ValueError("Invalid JSON") mock_response.text = "This is an error less than 500 chars." with pytest.raises(USPTOApiServerError) as excinfo: - client._make_request(method="GET", endpoint="test") + client._execute_request(method="GET", url="https://api.test.com/test") assert "This is an error less than 500 chars." in str(excinfo.value) assert excinfo.value.request_identifier is None - def test_make_request_post_error_includes_body( + def test_execute_request_post_error_includes_body( self, mock_session: MagicMock ) -> None: """Test that POST request errors include the request body in the error message.""" @@ -438,7 +472,11 @@ def test_make_request_post_error_includes_body( post_body = {"q": "test query", "pagination": {"limit": 100}} with pytest.raises(USPTOApiBadRequestError) as excinfo: - client._make_request(method="POST", endpoint="test", json_data=post_body) + client._execute_request( + method="POST", + url="https://api.test.com/test", + json_data=post_body, + ) # Verify the error message includes the POST body error_message = str(excinfo.value) @@ -446,8 +484,8 @@ def test_make_request_post_error_includes_body( assert '"q": "test query"' in error_message assert '"pagination"' in error_message - def test_make_request_connection_error(self, mock_session: MagicMock) -> None: - """Test _make_request method with connection error.""" + def test_execute_request_connection_error(self, mock_session: MagicMock) -> None: + """Test _execute_request with connection error.""" # Setup client: BaseUSPTOClient[Any] = BaseUSPTOClient(base_url="https://api.test.com") client.config._session = mock_session @@ -458,15 +496,15 @@ def test_make_request_connection_error(self, mock_session: MagicMock) -> None: ) with pytest.raises(USPTOConnectionError) as excinfo: - client._make_request(method="GET", endpoint="test") + client._execute_request(method="GET", url="https://api.test.com/test") # Verify error details assert "Failed to connect to" in str(excinfo.value) assert "https://api.test.com/test" in str(excinfo.value) assert excinfo.value.api_short_error == "Connection Error" - def test_make_request_timeout_error(self, mock_session: MagicMock) -> None: - """Test _make_request method with timeout error.""" + def test_execute_request_timeout_error(self, mock_session: MagicMock) -> None: + """Test _execute_request with timeout error.""" # Setup client: BaseUSPTOClient[Any] = BaseUSPTOClient(base_url="https://api.test.com") client.config._session = mock_session @@ -475,17 +513,17 @@ def test_make_request_timeout_error(self, mock_session: MagicMock) -> None: mock_session.get.side_effect = requests.exceptions.Timeout("Request timed out") with pytest.raises(USPTOTimeout) as excinfo: - client._make_request(method="GET", endpoint="test") + client._execute_request(method="GET", url="https://api.test.com/test") # Verify error details assert "timed out" in str(excinfo.value) assert "https://api.test.com/test" in str(excinfo.value) assert excinfo.value.api_short_error == "Timeout" - def test_make_request_generic_request_exception( + def test_execute_request_generic_request_exception( self, mock_session: MagicMock ) -> None: - """Test _make_request method with generic request exception.""" + """Test _execute_request with generic request exception.""" # Setup client: BaseUSPTOClient[Any] = BaseUSPTOClient(base_url="https://api.test.com") client.config._session = mock_session @@ -498,10 +536,10 @@ def test_make_request_generic_request_exception( # Should fall back to generic USPTOApiError expected_message_pattern = "API request to 'https://api.test.com/test' failed due to a network or request issue" with pytest.raises(USPTOApiError, match=expected_message_pattern): - client._make_request(method="GET", endpoint="test") + client._execute_request(method="GET", url="https://api.test.com/test") - def test_make_request_json_parse_error(self, mock_session: MagicMock) -> None: - """Test _make_request method with JSON parsing error.""" + def test_get_json_parse_error(self, mock_session: MagicMock) -> None: + """Test _get_json with JSON parsing error.""" # Setup client: BaseUSPTOClient[Any] = BaseUSPTOClient(base_url="https://api.test.com") client.config._session = mock_session @@ -518,7 +556,7 @@ def test_make_request_json_parse_error(self, mock_session: MagicMock) -> None: # Should raise USPTOApiResponseParseError with pytest.raises(USPTOApiResponseParseError) as excinfo: - client._make_request(method="GET", endpoint="test") + client._get_json(method="GET", endpoint="test") # Verify error details assert "Failed to parse JSON response" in str(excinfo.value) @@ -527,10 +565,10 @@ def test_make_request_json_parse_error(self, mock_session: MagicMock) -> None: assert "text/html" in str(excinfo.value.error_details) assert "Error page" in str(excinfo.value.error_details) - def test_make_request_json_parse_error_with_response_class( + def test_get_model_json_parse_error( self, mock_session: MagicMock ) -> None: - """Test _make_request with JSON parsing error when using response_class.""" + """Test _get_model with JSON parsing error.""" # Setup client: BaseUSPTOClient[Any] = BaseUSPTOClient(base_url="https://api.test.com") client.config._session = mock_session @@ -549,7 +587,7 @@ def test_make_request_json_parse_error_with_response_class( # Should raise USPTOApiResponseParseError even with response_class with pytest.raises(USPTOApiResponseParseError) as excinfo: - client._make_request( + client._get_model( method="GET", endpoint="test", response_class=TestResponseClass ) @@ -1004,7 +1042,7 @@ def test_base_client_timeout_applied(self, mock_session: MagicMock) -> None: mock_session.get.return_value.json.return_value = {"test": "data"} # Make request - client._make_request(method="GET", endpoint="test") + client._get_json(method="GET", endpoint="test") # Verify timeout was passed mock_session.get.assert_called_once() diff --git a/tests/clients/test_bulk_data_clients.py b/tests/clients/test_bulk_data_clients.py index ff62cee..90b1c5b 100644 --- a/tests/clients/test_bulk_data_clients.py +++ b/tests/clients/test_bulk_data_clients.py @@ -477,9 +477,9 @@ def test_get_product_by_id_not_found(self) -> None: # Setup client = BulkDataClient(config=USPTOConfig(api_key="test_key")) - # Mock _make_request to return an empty BulkDataResponse + # Mock _get_model to return an empty BulkDataResponse empty_response = BulkDataResponse(count=0, bulk_data_product_bag=[]) - with patch.object(client, "_make_request", return_value=empty_response): + with patch.object(client, "_get_model", return_value=empty_response): # Test with product not found with pytest.raises(ValueError, match="Product 'TEST' not found"): client.get_product_by_id(product_id="TEST") @@ -498,7 +498,7 @@ def test_get_product_by_id_wrong_product_returned(self) -> None: ) response = BulkDataResponse(count=1, bulk_data_product_bag=[wrong_product]) - with patch.object(client, "_make_request", return_value=response): + with patch.object(client, "_get_model", return_value=response): # Should still return the product but issue a warning with pytest.warns( match="API returned product 'WRONG_ID' but requested 'TEST'" diff --git a/tests/clients/test_patent_data_clients.py b/tests/clients/test_patent_data_clients.py index c646c81..dcf80cd 100644 --- a/tests/clients/test_patent_data_clients.py +++ b/tests/clients/test_patent_data_clients.py @@ -231,14 +231,26 @@ def mock_get_search_results_empty() -> dict: def client_with_mocked_request( patent_data_client: PatentDataClient, ) -> Iterator[tuple[PatentDataClient, MagicMock]]: - """Provides a PatentDataClient instance with its _make_request method mocked. + """Provides a PatentDataClient instance with its _get_model method mocked. - Returns a tuple (client, mock_make_request). + Returns a tuple (client, mock_get_model). """ with patch.object( - patent_data_client, "_make_request", autospec=True - ) as mock_make_request: - yield patent_data_client, mock_make_request + patent_data_client, "_get_model", autospec=True + ) as mock_get_model: + yield patent_data_client, mock_get_model + + +@pytest.fixture +def client_with_mocked_get_json( + patent_data_client: PatentDataClient, +) -> Iterator[tuple[PatentDataClient, MagicMock]]: + """Provides a PatentDataClient instance with its _get_json method mocked. + + Returns a tuple (client, mock_get_json). + """ + with patch.object(patent_data_client, "_get_json", autospec=True) as mock_get_json: + yield patent_data_client, mock_get_json @pytest.fixture @@ -297,15 +309,15 @@ def test_search_applications_get_direct_query( mock_patent_data_response_with_data: PatentDataResponse, ) -> None: """Test search_applications method (GET search path) with direct query.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_with_data params_to_send: dict[str, Any] = {"query": "Test", "limit": 10, "offset": 0} expected_api_params: dict[str, Any] = {"q": "Test", "limit": 10, "offset": 0} result = client.search_applications(**params_to_send) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint="api/v1/patent/applications/search", params=expected_api_params, @@ -319,8 +331,8 @@ def test_search_applications_get_with_combined_q_convenience_params( mock_patent_data_response_empty: PatentDataResponse, ) -> None: """Test search_applications GET path with a combination of _q convenience params.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_empty + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_empty client.search_applications( inventor_name_q="Doe", filing_date_from_q="2021-01-01", limit=5 @@ -331,7 +343,7 @@ def test_search_applications_get_with_combined_q_convenience_params( "limit": 5, "offset": 0, } - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint="api/v1/patent/applications/search", params=expected_api_params, @@ -344,8 +356,8 @@ def test_search_applications_post( mock_patent_data_response_with_data: PatentDataResponse, ) -> None: """Test search_applications method (POST search path).""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_with_data search_body = { "q": "Test", "filters": [ @@ -356,7 +368,7 @@ def test_search_applications_post( result = client.search_applications(post_body=search_body) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="POST", endpoint="api/v1/patent/applications/search", json_data=search_body, @@ -432,8 +444,8 @@ def test_search_applications_get_various_q_convenience_filters( mock_patent_data_response_empty: PatentDataResponse, ) -> None: """Test search_applications GET path with various individual _q convenience filters.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_empty + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_empty limit = 25 offset = 0 @@ -451,7 +463,7 @@ def test_search_applications_get_various_q_convenience_filters( "offset": effective_offset, } - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint="api/v1/patent/applications/search", params=expected_call_params, @@ -464,8 +476,8 @@ def test_search_applications_get_multiple_q_convenience_filters( mock_patent_data_response_empty: PatentDataResponse, ) -> None: """Test search_applications GET path with multiple _q convenience filters combined.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_empty + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_empty client.search_applications( inventor_name_q="John Smith", @@ -481,7 +493,7 @@ def test_search_applications_get_multiple_q_convenience_filters( "applicationMetaData.cpcClassificationBag:G06F AND " "applicationMetaData.filingDate:[2020-01-01 TO 2022-01-01]" ) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint="api/v1/patent/applications/search", params={"q": expected_q, "limit": 20, "offset": 10}, @@ -494,12 +506,12 @@ def test_search_applications_get_empty_query_params_uses_defaults( mock_patent_data_response_empty: PatentDataResponse, ) -> None: """Test search_applications GET with no specific query parameters, only default limit/offset.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_empty + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_empty client.search_applications() - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint="api/v1/patent/applications/search", params={"offset": 0, "limit": 25}, @@ -512,12 +524,12 @@ def test_search_applications_get_explicitly_null_limit_offset_direct_q( mock_patent_data_response_empty: PatentDataResponse, ) -> None: """Test search_applications GET with limit and offset explicitly None, using direct 'query'.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_empty + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_empty client.search_applications(query="test query", limit=None, offset=None) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint="api/v1/patent/applications/search", params={"q": "test query"}, @@ -547,8 +559,8 @@ def test_search_applications_get_with_openapi_params( # New test mock_patent_data_response_empty: PatentDataResponse, ) -> None: """Test search_applications GET path with various direct OpenAPI parameters.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_empty + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_empty method_kwargs: dict[str, Any] = { api_param_name: api_param_value, @@ -562,13 +574,13 @@ def test_search_applications_get_with_openapi_params( # New test "limit": 5, "offset": 0, } - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint="api/v1/patent/applications/search", params=expected_api_params, response_class=PatentDataResponse, ) - # mock_make_request.reset_mock() # Removed as it caused issues with parametrize in some pytest versions + # mock_get_model.reset_mock() # Removed as it caused issues with parametrize in some pytest versions def test_search_applications_get_with_additional_query_params( # New test self, @@ -576,8 +588,8 @@ def test_search_applications_get_with_additional_query_params( # New test mock_patent_data_response_empty: PatentDataResponse, ) -> None: """Test search_applications GET path with additional_query_params.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_empty + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_empty client.search_applications( query="main_query", @@ -594,7 +606,7 @@ def test_search_applications_get_with_additional_query_params( # New test "limit": 10, "offset": 0, } - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint="api/v1/patent/applications/search", params=expected_api_params, @@ -612,14 +624,14 @@ def test_get_application_by_number_success( mock_patent_file_wrapper: PatentFileWrapper, ) -> None: """Test successful retrieval of patent application details.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_with_data app_num = mock_patent_file_wrapper.application_number_text assert app_num is not None result = client.get_application_by_number(application_number=app_num) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint=f"api/v1/patent/applications/{app_num}", response_class=PatentDataResponse, @@ -636,8 +648,8 @@ def test_get_application_by_number_empty_bag_returns_none( mock_patent_data_response_empty: PatentDataResponse, ) -> None: """Test get_application_by_number returns None if patentFileWrapperDataBag is empty.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_empty + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_empty app_num_to_request = "00000000" result = client.get_application_by_number(application_number=app_num_to_request) @@ -689,26 +701,29 @@ def test_get_application_documents( self, client_with_mocked_request: tuple[PatentDataClient, MagicMock] ) -> None: """Test retrieval of application documents.""" - client, mock_make_request = client_with_mocked_request + client, mock_get_model = client_with_mocked_request app_num = "12345678" - mock_response_dict = { - "documentBag": [ - { - "documentIdentifier": "DOC1", - "documentCode": "IDS", - "officialDate": "2023-01-01T00:00:00Z", - "downloadOptionBag": [ - {"mimeTypeIdentifier": "PDF", "downloadURI": "/doc1.pdf"} + mock_doc_bag = DocumentBag( + documents=[ + Document( + document_identifier="DOC1", + document_code="IDS", + official_date=datetime(2023, 1, 1, tzinfo=timezone.utc), + document_formats=[ + DocumentFormat( + mime_type_identifier="PDF", download_url="/doc1.pdf" + ) ], - } + ) ] - } - mock_make_request.return_value = mock_response_dict + ) + mock_get_model.return_value = mock_doc_bag result = client.get_application_documents(application_number=app_num) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint=f"api/v1/patent/applications/{app_num}/documents", + response_class=DocumentBag, params=None, ) assert isinstance(result, DocumentBag) @@ -719,28 +734,31 @@ def test_get_application_documents_with_document_code_filter( self, client_with_mocked_request: tuple[PatentDataClient, MagicMock] ) -> None: """Test retrieval of application documents filtered by document codes.""" - client, mock_make_request = client_with_mocked_request + client, mock_get_model = client_with_mocked_request app_num = "12345678" - mock_response_dict = { - "documentBag": [ - { - "documentIdentifier": "DOC2", - "documentCode": "ABST", - "officialDate": "2023-02-15T00:00:00Z", - "downloadOptionBag": [ - {"mimeTypeIdentifier": "PDF", "downloadURI": "/doc2.pdf"} + mock_doc_bag = DocumentBag( + documents=[ + Document( + document_identifier="DOC2", + document_code="ABST", + official_date=datetime(2023, 2, 15, tzinfo=timezone.utc), + document_formats=[ + DocumentFormat( + mime_type_identifier="PDF", download_url="/doc2.pdf" + ) ], - } + ) ] - } - mock_make_request.return_value = mock_response_dict + ) + mock_get_model.return_value = mock_doc_bag result = client.get_application_documents( application_number=app_num, document_codes=["ABST", "CLM"] ) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint=f"api/v1/patent/applications/{app_num}/documents", + response_class=DocumentBag, params={"documentCodes": "ABST,CLM"}, ) assert isinstance(result, DocumentBag) @@ -751,30 +769,33 @@ def test_get_application_documents_with_date_filter( self, client_with_mocked_request: tuple[PatentDataClient, MagicMock] ) -> None: """Test retrieval of application documents filtered by official date range.""" - client, mock_make_request = client_with_mocked_request + client, mock_get_model = client_with_mocked_request app_num = "12345678" - mock_response_dict = { - "documentBag": [ - { - "documentIdentifier": "DOC3", - "documentCode": "SPEC", - "officialDate": "2023-03-20T00:00:00Z", - "downloadOptionBag": [ - {"mimeTypeIdentifier": "PDF", "downloadURI": "/doc3.pdf"} + mock_doc_bag = DocumentBag( + documents=[ + Document( + document_identifier="DOC3", + document_code="SPEC", + official_date=datetime(2023, 3, 20, tzinfo=timezone.utc), + document_formats=[ + DocumentFormat( + mime_type_identifier="PDF", download_url="/doc3.pdf" + ) ], - } + ) ] - } - mock_make_request.return_value = mock_response_dict + ) + mock_get_model.return_value = mock_doc_bag result = client.get_application_documents( application_number=app_num, official_date_from="2023-01-01", official_date_to="2023-12-31", ) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint=f"api/v1/patent/applications/{app_num}/documents", + response_class=DocumentBag, params={"officialDateFrom": "2023-01-01", "officialDateTo": "2023-12-31"}, ) assert isinstance(result, DocumentBag) @@ -784,10 +805,10 @@ def test_get_application_documents_with_combined_filters( self, client_with_mocked_request: tuple[PatentDataClient, MagicMock] ) -> None: """Test retrieval of application documents with multiple filters combined.""" - client, mock_make_request = client_with_mocked_request + client, mock_get_model = client_with_mocked_request app_num = "12345678" - mock_response_dict = {"documentBag": []} - mock_make_request.return_value = mock_response_dict + mock_doc_bag = DocumentBag(documents=[]) + mock_get_model.return_value = mock_doc_bag result = client.get_application_documents( application_number=app_num, document_codes=["DRWD", "SPEC"], @@ -795,9 +816,10 @@ def test_get_application_documents_with_combined_filters( official_date_to="2023-06-30", ) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint=f"api/v1/patent/applications/{app_num}/documents", + response_class=DocumentBag, params={ "documentCodes": "DRWD,SPEC", "officialDateFrom": "2022-06-01", @@ -811,19 +833,20 @@ def test_get_application_documents_with_partial_date_filter( self, client_with_mocked_request: tuple[PatentDataClient, MagicMock] ) -> None: """Test retrieval with only one date boundary specified.""" - client, mock_make_request = client_with_mocked_request + client, mock_get_model = client_with_mocked_request app_num = "12345678" - mock_response_dict = {"documentBag": []} - mock_make_request.return_value = mock_response_dict + mock_doc_bag = DocumentBag(documents=[]) + mock_get_model.return_value = mock_doc_bag # Test with only from date result = client.get_application_documents( application_number=app_num, official_date_from="2023-01-01" ) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint=f"api/v1/patent/applications/{app_num}/documents", + response_class=DocumentBag, params={"officialDateFrom": "2023-01-01"}, ) assert isinstance(result, DocumentBag) @@ -839,14 +862,14 @@ def test_get_application_associated_documents( mock_patent_file_wrapper: PatentFileWrapper, ) -> None: """Test retrieval of associated documents metadata.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_with_data app_num = mock_patent_file_wrapper.application_number_text assert app_num is not None result = client.get_application_associated_documents(application_number=app_num) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint=f"api/v1/patent/applications/{app_num}/associated-documents", response_class=PatentDataResponse, @@ -1216,22 +1239,28 @@ def test_get_ifw_by_application_number( mock_patent_file_wrapper: PatentFileWrapper, ) -> None: """Test get_IFW with application_number calls get_application_by_number.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.side_effect = [ - PatentDataResponse(count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper]), - {"documentBag": []}, + client, mock_get_model = client_with_mocked_request + mock_get_model.side_effect = [ + PatentDataResponse( + count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] + ), + DocumentBag(documents=[]), ] app_num = "12345678" result = client.get_IFW_metadata(application_number=app_num) # Should call get_application_by_number first - assert mock_make_request.call_args_list[0] == call( + assert mock_get_model.call_args_list[0] == call( method="GET", endpoint=f"api/v1/patent/applications/{app_num}", response_class=PatentDataResponse, ) - assert result.application_number_text == mock_patent_file_wrapper.application_number_text + assert result is not None + assert ( + result.application_number_text + == mock_patent_file_wrapper.application_number_text + ) assert isinstance(result.document_bag, DocumentBag) def test_get_ifw_by_patent_number( @@ -1240,17 +1269,19 @@ def test_get_ifw_by_patent_number( mock_patent_file_wrapper: PatentFileWrapper, ) -> None: """Test get_IFW with patent_number calls search_applications.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.side_effect = [ - PatentDataResponse(count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper]), - {"documentBag": []}, + client, mock_get_model = client_with_mocked_request + mock_get_model.side_effect = [ + PatentDataResponse( + count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] + ), + DocumentBag(documents=[]), ] patent_num = "10000000" result = client.get_IFW_metadata(patent_number=patent_num) # Should call search_applications with patent_number_q first - assert mock_make_request.call_args_list[0] == call( + assert mock_get_model.call_args_list[0] == call( method="GET", endpoint="api/v1/patent/applications/search", params={ @@ -1260,7 +1291,11 @@ def test_get_ifw_by_patent_number( }, response_class=PatentDataResponse, ) - assert result.application_number_text == mock_patent_file_wrapper.application_number_text + assert result is not None + assert ( + result.application_number_text + == mock_patent_file_wrapper.application_number_text + ) assert isinstance(result.document_bag, DocumentBag) def test_get_ifw_by_publication_number( @@ -1269,17 +1304,19 @@ def test_get_ifw_by_publication_number( mock_patent_file_wrapper: PatentFileWrapper, ) -> None: """Test get_IFW with publication_number calls search_applications.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.side_effect = [ - PatentDataResponse(count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper]), - {"documentBag": []}, + client, mock_get_model = client_with_mocked_request + mock_get_model.side_effect = [ + PatentDataResponse( + count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] + ), + DocumentBag(documents=[]), ] pub_num = "US20240123456A1" result = client.get_IFW_metadata(publication_number=pub_num) # Should call search_applications with earliestPublicationNumber_q first - assert mock_make_request.call_args_list[0] == call( + assert mock_get_model.call_args_list[0] == call( method="GET", endpoint="api/v1/patent/applications/search", params={ @@ -1289,7 +1326,11 @@ def test_get_ifw_by_publication_number( }, response_class=PatentDataResponse, ) - assert result.application_number_text == mock_patent_file_wrapper.application_number_text + assert result is not None + assert ( + result.application_number_text + == mock_patent_file_wrapper.application_number_text + ) assert isinstance(result.document_bag, DocumentBag) def test_get_ifw_by_pct_app_number( @@ -1303,10 +1344,12 @@ def test_get_ifw_by_pct_app_number( has application_number_text='12345678' but we're requesting a PCT number. This is expected test behavior for validating the warning system. """ - client, mock_make_request = client_with_mocked_request - mock_make_request.side_effect = [ - PatentDataResponse(count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper]), - {"documentBag": []}, + client, mock_get_model = client_with_mocked_request + mock_get_model.side_effect = [ + PatentDataResponse( + count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] + ), + DocumentBag(documents=[]), ] pct_app = "PCT/US2024/012345" @@ -1316,12 +1359,16 @@ def test_get_ifw_by_pct_app_number( result = client.get_IFW_metadata(PCT_app_number=pct_app) # Should call get_application_by_number first - assert mock_make_request.call_args_list[0] == call( + assert mock_get_model.call_args_list[0] == call( method="GET", endpoint="api/v1/patent/applications/PCTUS2412345", response_class=PatentDataResponse, ) - assert result.application_number_text == mock_patent_file_wrapper.application_number_text + assert result is not None + assert ( + result.application_number_text + == mock_patent_file_wrapper.application_number_text + ) assert isinstance(result.document_bag, DocumentBag) def test_get_ifw_by_short_pct_app_number( @@ -1338,10 +1385,12 @@ def test_get_ifw_by_short_pct_app_number( has application_number_text='12345678' but we're requesting a PCT number. This is expected test behavior for validating the warning system. """ - client, mock_make_request = client_with_mocked_request - mock_make_request.side_effect = [ - PatentDataResponse(count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper]), - {"documentBag": []}, + client, mock_get_model = client_with_mocked_request + mock_get_model.side_effect = [ + PatentDataResponse( + count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] + ), + DocumentBag(documents=[]), ] pct_app = "PCT/US24/012345" @@ -1351,12 +1400,16 @@ def test_get_ifw_by_short_pct_app_number( result = client.get_IFW_metadata(PCT_app_number=pct_app) # Should call get_application_by_number first - assert mock_make_request.call_args_list[0] == call( + assert mock_get_model.call_args_list[0] == call( method="GET", endpoint="api/v1/patent/applications/PCTUS2412345", response_class=PatentDataResponse, ) - assert result.application_number_text == mock_patent_file_wrapper.application_number_text + assert result is not None + assert ( + result.application_number_text + == mock_patent_file_wrapper.application_number_text + ) assert isinstance(result.document_bag, DocumentBag) def test_get_ifw_by_pct_app_number_malformed( @@ -1369,8 +1422,8 @@ def test_get_ifw_by_pct_app_number_malformed( Verifies that PCT numbers missing the first slash (PCTUS2024/012345 instead of PCT/US2024/012345) raise ValueError with descriptive error message. """ - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = PatentDataResponse( + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PatentDataResponse( count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] ) @@ -1393,8 +1446,8 @@ def test_get_ifw_by_pct_app_year_corrupted( Verifies that PCT numbers with incorrect year length (PCT/US224/012345 with 3-digit year instead of 2 or 4 digits) raise ValueError with descriptive error message. """ - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = PatentDataResponse( + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PatentDataResponse( count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] ) @@ -1418,8 +1471,8 @@ def test_get_ifw_by_pct_app_year_malformed( (PCT/USA2024/012345 instead of PCT/US2024/012345) raise ValueError with descriptive error message. """ - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = PatentDataResponse( + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PatentDataResponse( count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] ) @@ -1442,8 +1495,8 @@ def test_get_ifw_by_pct_app_serial_malformed( Verifies that PCT numbers with non-numeric serial number (PCT/US2024/A12345 instead of PCT/US2024/012345) raise ValueError with descriptive error message. """ - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = PatentDataResponse( + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PatentDataResponse( count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] ) @@ -1462,17 +1515,19 @@ def test_get_ifw_by_pct_pub_number( mock_patent_file_wrapper: PatentFileWrapper, ) -> None: """Test get_IFW with PCT_pub_number calls search_applications.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.side_effect = [ - PatentDataResponse(count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper]), - {"documentBag": []}, + client, mock_get_model = client_with_mocked_request + mock_get_model.side_effect = [ + PatentDataResponse( + count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] + ), + DocumentBag(documents=[]), ] pct_pub = "WO2024012345A1" result = client.get_IFW_metadata(PCT_pub_number=pct_pub) # Should call search_applications with pctPublicationNumber_q first - assert mock_make_request.call_args_list[0] == call( + assert mock_get_model.call_args_list[0] == call( method="GET", endpoint="api/v1/patent/applications/search", params={ @@ -1482,7 +1537,11 @@ def test_get_ifw_by_pct_pub_number( }, response_class=PatentDataResponse, ) - assert result.application_number_text == mock_patent_file_wrapper.application_number_text + assert result is not None + assert ( + result.application_number_text + == mock_patent_file_wrapper.application_number_text + ) assert isinstance(result.document_bag, DocumentBag) def test_get_ifw_no_parameters_returns_none( @@ -1496,8 +1555,8 @@ def test_get_ifw_empty_search_results_returns_none( self, client_with_mocked_request: tuple[PatentDataClient, MagicMock] ) -> None: """Test get_IFW returns None when search returns empty results.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = PatentDataResponse( + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PatentDataResponse( count=0, patent_file_wrapper_data_bag=[] ) @@ -1510,10 +1569,12 @@ def test_get_ifw_prioritizes_first_parameter( mock_patent_file_wrapper: PatentFileWrapper, ) -> None: """Test get_IFW uses application_number when multiple parameters provided.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.side_effect = [ - PatentDataResponse(count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper]), - {"documentBag": []}, + client, mock_get_model = client_with_mocked_request + mock_get_model.side_effect = [ + PatentDataResponse( + count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] + ), + DocumentBag(documents=[]), ] app_num = "12345678" @@ -1525,12 +1586,16 @@ def test_get_ifw_prioritizes_first_parameter( ) # Should call get_application_by_number first, not search - assert mock_make_request.call_args_list[0] == call( + assert mock_get_model.call_args_list[0] == call( method="GET", endpoint=f"api/v1/patent/applications/{app_num}", response_class=PatentDataResponse, ) - assert result.application_number_text == mock_patent_file_wrapper.application_number_text + assert result is not None + assert ( + result.application_number_text + == mock_patent_file_wrapper.application_number_text + ) assert isinstance(result.document_bag, DocumentBag) @@ -1543,15 +1608,15 @@ def test_get_patent_found( mock_patent_file_wrapper: PatentFileWrapper, ) -> None: """Test get_patent returns wrapper when patent is found.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = PatentDataResponse( + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PatentDataResponse( count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] ) patent_num = "11000000" result = client.get_patent(patent_num) - assert mock_make_request.call_args == call( + assert mock_get_model.call_args == call( method="GET", endpoint="api/v1/patent/applications/search", params={ @@ -1562,15 +1627,18 @@ def test_get_patent_found( response_class=PatentDataResponse, ) assert result is not None - assert result.application_number_text == mock_patent_file_wrapper.application_number_text + assert ( + result.application_number_text + == mock_patent_file_wrapper.application_number_text + ) def test_get_patent_not_found( self, client_with_mocked_request: tuple[PatentDataClient, MagicMock], ) -> None: """Test get_patent returns None when patent is not found.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = PatentDataResponse( + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PatentDataResponse( count=0, patent_file_wrapper_data_bag=[] ) @@ -1587,15 +1655,15 @@ def test_get_publication_found( mock_patent_file_wrapper: PatentFileWrapper, ) -> None: """Test get_publication returns wrapper when publication is found.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = PatentDataResponse( + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PatentDataResponse( count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] ) pub_num = "20230123456" result = client.get_publication(pub_num) - assert mock_make_request.call_args == call( + assert mock_get_model.call_args == call( method="GET", endpoint="api/v1/patent/applications/search", params={ @@ -1606,15 +1674,18 @@ def test_get_publication_found( response_class=PatentDataResponse, ) assert result is not None - assert result.application_number_text == mock_patent_file_wrapper.application_number_text + assert ( + result.application_number_text + == mock_patent_file_wrapper.application_number_text + ) def test_get_publication_not_found( self, client_with_mocked_request: tuple[PatentDataClient, MagicMock], ) -> None: """Test get_publication returns None when publication is not found.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = PatentDataResponse( + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PatentDataResponse( count=0, patent_file_wrapper_data_bag=[] ) @@ -1636,8 +1707,8 @@ def test_get_pct_with_app_number( mock_pct_file_wrapper: PatentFileWrapper, ) -> None: """Test get_pct with PCT application number uses direct lookup.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = PatentDataResponse( + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PatentDataResponse( count=1, patent_file_wrapper_data_bag=[mock_pct_file_wrapper] ) @@ -1645,13 +1716,16 @@ def test_get_pct_with_app_number( result = client.get_pct(pct_app) # Should call get_application_by_number (direct lookup) - assert mock_make_request.call_args == call( + assert mock_get_model.call_args == call( method="GET", endpoint="api/v1/patent/applications/PCTUS2412345", response_class=PatentDataResponse, ) assert result is not None - assert result.application_number_text == mock_pct_file_wrapper.application_number_text + assert ( + result.application_number_text + == mock_pct_file_wrapper.application_number_text + ) def test_get_pct_with_pub_number( self, @@ -1659,15 +1733,15 @@ def test_get_pct_with_pub_number( mock_patent_file_wrapper: PatentFileWrapper, ) -> None: """Test get_pct with PCT publication number uses search.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = PatentDataResponse( + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PatentDataResponse( count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] ) pct_pub = "WO2024012345A1" result = client.get_pct(pct_pub) - assert mock_make_request.call_args == call( + assert mock_get_model.call_args == call( method="GET", endpoint="api/v1/patent/applications/search", params={ @@ -1678,15 +1752,18 @@ def test_get_pct_with_pub_number( response_class=PatentDataResponse, ) assert result is not None - assert result.application_number_text == mock_patent_file_wrapper.application_number_text + assert ( + result.application_number_text + == mock_patent_file_wrapper.application_number_text + ) def test_get_pct_not_found( self, client_with_mocked_request: tuple[PatentDataClient, MagicMock], ) -> None: """Test get_pct returns None when PCT number is not found.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = PatentDataResponse( + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PatentDataResponse( count=0, patent_file_wrapper_data_bag=[] ) @@ -2065,12 +2142,12 @@ class TestPatentApplicationDataRetrieval: def test_get_search_results_get_direct_query( self, - client_with_mocked_request: tuple[PatentDataClient, MagicMock], + client_with_mocked_get_json: tuple[PatentDataClient, MagicMock], mock_get_search_results_empty: list[ApplicationMetaData], ) -> None: """Test GET path of get_search_results with direct query, always requests JSON.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_get_search_results_empty + client, mock_get_json = client_with_mocked_get_json + mock_get_json.return_value = mock_get_search_results_empty method_params: dict[str, Any] = {"query": "bulk test"} expected_api_params = { @@ -2082,7 +2159,7 @@ def test_get_search_results_get_direct_query( result = client.get_search_results(**method_params) - mock_make_request.assert_called_once_with( + mock_get_json.assert_called_once_with( method="GET", endpoint="api/v1/patent/applications/search/download", params=expected_api_params, @@ -2091,12 +2168,12 @@ def test_get_search_results_get_direct_query( def test_get_search_results_get_with_combined_q_convenience_params( self, - client_with_mocked_request: tuple[PatentDataClient, MagicMock], + client_with_mocked_get_json: tuple[PatentDataClient, MagicMock], mock_get_search_results_empty: list[ApplicationMetaData], ) -> None: """Test get_search_results GET path with a combination of _q convenience params.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_get_search_results_empty + client, mock_get_json = client_with_mocked_get_json + mock_get_json.return_value = mock_get_search_results_empty client.get_search_results( inventor_name_q="Doe", filing_date_from_q="2021-01-01", limit=5 @@ -2108,7 +2185,7 @@ def test_get_search_results_get_with_combined_q_convenience_params( "offset": 0, "format": "json", } - mock_make_request.assert_called_once_with( + mock_get_json.assert_called_once_with( method="GET", endpoint="api/v1/patent/applications/search/download", params=expected_api_params, @@ -2169,12 +2246,12 @@ def test_get_search_results_get_various_q_convenience_filters( self, search_q_params: dict[str, Any], expected_q_part: str, - client_with_mocked_request: tuple[PatentDataClient, MagicMock], + client_with_mocked_get_json: tuple[PatentDataClient, MagicMock], mock_get_search_results_empty: list[ApplicationMetaData], ) -> None: """Test get_search_results GET path with various individual _q convenience filters.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_get_search_results_empty + client, mock_get_json = client_with_mocked_get_json + mock_get_json.return_value = mock_get_search_results_empty limit = 15 offset = 5 @@ -2193,12 +2270,12 @@ def test_get_search_results_get_various_q_convenience_filters( "format": "json", } - mock_make_request.assert_called_once_with( + mock_get_json.assert_called_once_with( method="GET", endpoint="api/v1/patent/applications/search/download", params=expected_call_params, ) - # mock_make_request.reset_mock() # Removed to avoid issues with parametrize if tests are run in certain ways + # mock_get_json.reset_mock() # Removed to avoid issues with parametrize if tests are run in certain ways @pytest.mark.parametrize( "method_param_name, param_value, expected_api_key", @@ -2218,12 +2295,12 @@ def test_get_search_results_get_with_openapi_params( method_param_name: str, param_value: str, expected_api_key: str, - client_with_mocked_request: tuple[PatentDataClient, MagicMock], + client_with_mocked_get_json: tuple[PatentDataClient, MagicMock], mock_get_search_results_empty: list[ApplicationMetaData], ) -> None: """Test get_search_results GET path with various direct OpenAPI parameters.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_get_search_results_empty + client, mock_get_json = client_with_mocked_get_json + mock_get_json.return_value = mock_get_search_results_empty method_kwargs: dict[str, Any] = { method_param_name: param_value, @@ -2238,21 +2315,21 @@ def test_get_search_results_get_with_openapi_params( "offset": 1, "format": "json", } - mock_make_request.assert_called_once_with( + mock_get_json.assert_called_once_with( method="GET", endpoint="api/v1/patent/applications/search/download", params=expected_api_params, ) - # mock_make_request.reset_mock() # Parametrized tests should not reset mock if one instance per test function + # mock_get_json.reset_mock() # Parametrized tests should not reset mock if one instance per test function def test_get_search_results_get_with_additional_query_params( # New test self, - client_with_mocked_request: tuple[PatentDataClient, MagicMock], + client_with_mocked_get_json: tuple[PatentDataClient, MagicMock], mock_get_search_results_empty: list[ApplicationMetaData], ) -> None: """Test get_search_results GET path with additional_query_params.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_get_search_results_empty + client, mock_get_json = client_with_mocked_get_json + mock_get_json.return_value = mock_get_search_results_empty client.get_search_results( query="main_download_query", @@ -2273,7 +2350,7 @@ def test_get_search_results_get_with_additional_query_params( # New test "offset": 0, "format": "json", } - mock_make_request.assert_called_once_with( + mock_get_json.assert_called_once_with( method="GET", endpoint="api/v1/patent/applications/search/download", params=expected_api_params, @@ -2281,12 +2358,12 @@ def test_get_search_results_get_with_additional_query_params( # New test def test_get_search_results_post( self, - client_with_mocked_request: tuple[PatentDataClient, MagicMock], + client_with_mocked_get_json: tuple[PatentDataClient, MagicMock], mock_get_search_results_empty: PatentDataResponse, ) -> None: """Test POST path of get_search_results.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_get_search_results_empty + client, mock_get_json = client_with_mocked_get_json + mock_get_json.return_value = mock_get_search_results_empty post_body_request = {"q": "Test POST", "fields": ["patentNumber"]} @@ -2298,7 +2375,7 @@ def test_get_search_results_post( result = client.get_search_results(post_body=post_body_request) - mock_make_request.assert_called_once_with( + mock_get_json.assert_called_once_with( method="POST", endpoint="api/v1/patent/applications/search/download", json_data=expected_post_body_sent_to_api, @@ -2317,13 +2394,13 @@ def test_get_application_metadata( mock_patent_file_wrapper: PatentFileWrapper, ) -> None: """Test get_application_metadata returns ApplicationMetaData.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_with_data app_num = mock_patent_file_wrapper.application_number_text assert app_num is not None result = client.get_application_metadata(application_number=app_num) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint=f"api/v1/patent/applications/{app_num}/meta-data", response_class=PatentDataResponse, @@ -2336,12 +2413,12 @@ def test_get_application_adjustment( mock_patent_data_response_with_data: PatentDataResponse, mock_patent_file_wrapper: PatentFileWrapper, ) -> None: - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_with_data app_num = mock_patent_file_wrapper.application_number_text assert app_num is not None result = client.get_application_adjustment(application_number=app_num) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint=f"api/v1/patent/applications/{app_num}/adjustment", response_class=PatentDataResponse, @@ -2356,22 +2433,20 @@ def test_get_status_codes( self, client_with_mocked_request: tuple[PatentDataClient, MagicMock] ) -> None: """Test get_status_codes method.""" - client, mock_make_request = client_with_mocked_request - mock_api_response = { - "count": 1, - "statusCodeBag": [ - { - "applicationStatusCode": 100, - "applicationStatusDescriptionText": "Active", - } - ], - } - mock_make_request.return_value = mock_api_response + client, mock_get_model = client_with_mocked_request + mock_api_response = StatusCodeSearchResponse( + count=1, + status_code_bag=StatusCodeCollection( + [StatusCode(code=100, description="Active")] + ), + ) + mock_get_model.return_value = mock_api_response result = client.get_status_codes(params={"limit": 1}) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint="api/v1/patent/status-codes", + response_class=StatusCodeSearchResponse, params={"limit": 1}, ) assert isinstance(result, StatusCodeSearchResponse) @@ -2382,24 +2457,22 @@ def test_search_status_codes( self, client_with_mocked_request: tuple[PatentDataClient, MagicMock] ) -> None: """Test search_status_codes method.""" - client, mock_make_request = client_with_mocked_request - mock_api_response = { - "count": 1, - "statusCodeBag": [ - { - "applicationStatusCode": 150, - "applicationStatusDescriptionText": "Pending", - } - ], - } - mock_make_request.return_value = mock_api_response + client, mock_get_model = client_with_mocked_request + mock_api_response = StatusCodeSearchResponse( + count=1, + status_code_bag=StatusCodeCollection( + [StatusCode(code=150, description="Pending")] + ), + ) + mock_get_model.return_value = mock_api_response search_request = {"q": "Pending"} result = client.search_status_codes(search_request=search_request) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="POST", endpoint="api/v1/patent/status-codes", + response_class=StatusCodeSearchResponse, json_data=search_request, ) assert isinstance(result, StatusCodeSearchResponse) @@ -2469,8 +2542,8 @@ def client_for_return_type_tests( ], # Use the existing fixture mock_patent_data_response_with_data: PatentDataResponse, ) -> PatentDataClient: - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_patent_data_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_patent_data_response_with_data return client def test_get_application_metadata_type( @@ -2507,12 +2580,12 @@ def client_with_minimal_wrapper_for_return_types( ], # Use the existing patching fixture mock_patent_file_wrapper_minimal: PatentFileWrapper, ) -> PatentDataClient: - """Client whose _make_request returns a response with a minimal wrapper (only app number).""" - client, mock_make_request = client_with_mocked_request + """Client whose _get_model returns a response with a minimal wrapper (only app number).""" + client, mock_get_model = client_with_mocked_request response = PatentDataResponse( count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper_minimal] ) - mock_make_request.return_value = response + mock_get_model.return_value = response return client def test_specific_getters_handle_missing_fields_in_wrapper( @@ -2552,12 +2625,12 @@ def test_get_application_by_number_app_num_mismatch_in_bag( a USPTODataMismatchWarning should be issued to alert the user of the data inconsistency. """ - client, mock_make_request = client_with_mocked_request + client, mock_get_model = client_with_mocked_request requested_app_num = "87654321" response_with_original_wrapper = PatentDataResponse( count=1, patent_file_wrapper_data_bag=[mock_patent_file_wrapper] ) - mock_make_request.return_value = response_with_original_wrapper + mock_get_model.return_value = response_with_original_wrapper with pytest.warns( USPTODataMismatchWarning, @@ -2571,34 +2644,15 @@ def test_get_application_by_number_app_num_mismatch_in_bag( assert result is not None assert result.application_number_text == "12345678" - def test_get_application_by_number_unexpected_response_type( - self, client_with_mocked_request: tuple[PatentDataClient, MagicMock] - ) -> None: - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = ["not", "a", "PatentDataResponse"] - - with pytest.raises(AssertionError): - client.get_application_by_number(application_number="32165487") - def test_api_error_handling( self, client_with_mocked_request: tuple[PatentDataClient, MagicMock] ) -> None: - client, mock_make_request = client_with_mocked_request - mock_make_request.side_effect = USPTOApiBadRequestError( - "Mocked API Bad Request" - ) + client, mock_get_model = client_with_mocked_request + mock_get_model.side_effect = USPTOApiBadRequestError("Mocked API Bad Request") with pytest.raises(USPTOApiBadRequestError, match="Mocked API Bad Request"): client.search_applications(query="test") - def test_search_applications_post_assertion_error( - self, client_with_mocked_request: tuple[PatentDataClient, MagicMock] - ) -> None: - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = {"not_a_patent_data_response": True} - - with pytest.raises(AssertionError): - client.search_applications(post_body={"q": "test"}) class TestApplicationNumberSanitization: @@ -2697,7 +2751,10 @@ def test_sanitize_invalid_series_code_format_raises( patent_data_client.sanitize_application_number("08/123/456") # Already-sanitized PCT number passes through unchanged - assert patent_data_client.sanitize_application_number("PCTUS0812705") == "PCTUS0812705" + assert ( + patent_data_client.sanitize_application_number("PCTUS0812705") + == "PCTUS0812705" + ) class TestRawDataFeature: @@ -2707,9 +2764,9 @@ def test_raw_data_disabled_by_default( self, client_with_mocked_request: tuple[PatentDataClient, MagicMock] ) -> None: """Test that raw_data is None by default.""" - client, mock_make_request = client_with_mocked_request + client, mock_get_model = client_with_mocked_request mock_response = PatentDataResponse(count=1, patent_file_wrapper_data_bag=[]) - mock_make_request.return_value = mock_response + mock_get_model.return_value = mock_response result = client.search_applications(query="test") @@ -3339,6 +3396,7 @@ def test_returns_ifw_result_with_zip( assert result.output_path.endswith("12345678_ifw.zip") assert result.downloaded_documents == {"DOC001": "doc001.pdf"} import zipfile as zf + with zf.ZipFile(result.output_path) as z: assert "doc001.pdf" in z.namelist() @@ -3532,8 +3590,9 @@ def test_directory_skips_xml_only_docs( destination=str(tmp_path), as_zip=False, ) - mock_dl.assert_not_called() - assert result.downloaded_documents == {} + mock_dl.assert_not_called() + assert result is not None + assert result.downloaded_documents == {} def test_directory_warns_on_download_failure( self, patent_data_client: PatentDataClient, pdf_doc: Document, tmp_path @@ -3555,7 +3614,8 @@ def test_directory_warns_on_download_failure( destination=str(tmp_path), as_zip=False, ) - assert result.downloaded_documents == {} + assert result is not None + assert result.downloaded_documents == {} def test_skips_docs_with_no_identifier( self, patent_data_client: PatentDataClient, tmp_path @@ -3575,7 +3635,9 @@ def test_skips_docs_with_no_identifier( for as_zip in (True, False): with ( - patch.object(patent_data_client, "get_IFW_metadata", return_value=wrapper), + patch.object( + patent_data_client, "get_IFW_metadata", return_value=wrapper + ), patch.object(patent_data_client, "_download_and_extract") as mock_dl, ): result = patent_data_client.get_IFW( @@ -3584,5 +3646,6 @@ def test_skips_docs_with_no_identifier( as_zip=as_zip, overwrite=True, ) - mock_dl.assert_not_called() - assert result.downloaded_documents == {} + mock_dl.assert_not_called() + assert result is not None + assert result.downloaded_documents == {} diff --git a/tests/clients/test_petition_decision_clients.py b/tests/clients/test_petition_decision_clients.py index 85e6dab..fc61687 100644 --- a/tests/clients/test_petition_decision_clients.py +++ b/tests/clients/test_petition_decision_clients.py @@ -82,11 +82,11 @@ def mock_petition_response_empty() -> PetitionDecisionResponse: def client_with_mocked_request( petition_client: FinalPetitionDecisionsClient, ) -> Iterator[tuple[FinalPetitionDecisionsClient, MagicMock]]: - """Provides a client with mocked _make_request method.""" + """Provides a client with mocked _get_model method.""" with patch.object( - petition_client, "_make_request", autospec=True - ) as mock_make_request: - yield petition_client, mock_make_request + petition_client, "_get_model", autospec=True + ) as mock_get_model: + yield petition_client, mock_get_model @pytest.fixture @@ -145,12 +145,12 @@ def test_search_get_direct_query( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with direct query string.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data result = client.search_decisions(query="Test", limit=10, offset=0) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint="api/v1/petition/decisions/search", params={"q": "Test", "limit": 10, "offset": 0}, @@ -164,8 +164,8 @@ def test_search_with_application_number( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with application number convenience parameter.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data result = client.search_decisions(application_number_q="17765301", limit=25) @@ -174,7 +174,7 @@ def test_search_with_application_number( "limit": 25, "offset": 0, } - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint="api/v1/petition/decisions/search", params=expected_params, @@ -188,8 +188,8 @@ def test_search_with_date_range( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with date range.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data client.search_decisions( decision_date_from_q="2022-01-01", @@ -202,7 +202,7 @@ def test_search_with_date_range( "limit": 25, "offset": 0, } - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint="api/v1/petition/decisions/search", params=expected_params, @@ -215,8 +215,8 @@ def test_search_with_multiple_params( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search combining multiple parameters.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data client.search_decisions( applicant_name_q="Test Corp", @@ -226,7 +226,7 @@ def test_search_with_multiple_params( ) # Check that the query contains all three conditions joined with AND - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert "q" in params query = params["q"] @@ -242,13 +242,13 @@ def test_search_post_request( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with POST body.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data post_body = {"q": "technologyCenter:1700", "limit": 100} client.search_decisions(post_body=post_body) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="POST", endpoint="api/v1/petition/decisions/search", json_data=post_body, @@ -262,12 +262,12 @@ def test_search_with_patent_number( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with patent_number_q parameter.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data client.search_decisions(patent_number_q="10123456") - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert "patentNumber:10123456" in params["q"] @@ -277,12 +277,12 @@ def test_search_with_inventor_name( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with inventor_name_q parameter.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data client.search_decisions(inventor_name_q="John Doe") - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert 'inventorBag:"John Doe"' in params["q"] @@ -292,12 +292,12 @@ def test_search_with_invention_title( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with invention_title_q parameter.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data client.search_decisions(invention_title_q="Test Invention") - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert 'inventionTitle:"Test Invention"' in params["q"] @@ -307,12 +307,12 @@ def test_search_with_final_deciding_office_name( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with final_deciding_office_name_q parameter.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data client.search_decisions(final_deciding_office_name_q="TC Director") - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert 'finalDecidingOfficeName:"TC Director"' in params["q"] @@ -322,12 +322,12 @@ def test_search_with_decision_date_from_only( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with only decision_date_from_q parameter.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data client.search_decisions(decision_date_from_q="2023-01-01") - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert "decisionDate:>=2023-01-01" in params["q"] @@ -337,12 +337,12 @@ def test_search_with_decision_date_to_only( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with only decision_date_to_q parameter.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data client.search_decisions(decision_date_to_q="2023-12-31") - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert "decisionDate:<=2023-12-31" in params["q"] @@ -352,12 +352,12 @@ def test_search_with_petition_mail_date_from_only( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with only petition_mail_date_from_q parameter.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data client.search_decisions(petition_mail_date_from_q="2023-01-01") - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert "petitionMailDate:>=2023-01-01" in params["q"] @@ -367,12 +367,12 @@ def test_search_with_petition_mail_date_to_only( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with only petition_mail_date_to_q parameter.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data client.search_decisions(petition_mail_date_to_q="2023-12-31") - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert "petitionMailDate:<=2023-12-31" in params["q"] @@ -382,15 +382,15 @@ def test_search_with_petition_mail_date_range( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with petition mail date range.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data client.search_decisions( petition_mail_date_from_q="2023-01-01", petition_mail_date_to_q="2023-12-31", ) - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert "petitionMailDate:[2023-01-01 TO 2023-12-31]" in params["q"] @@ -400,8 +400,8 @@ def test_search_with_optional_params( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with optional sort, facets, fields, filters, and range_filters.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data client.search_decisions( query="test", @@ -412,7 +412,7 @@ def test_search_with_optional_params( range_filters="decisionDate:[2020 TO 2024]", ) - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert params["sort"] == "decisionDate desc" assert params["facets"] == "technologyCenter" @@ -426,14 +426,14 @@ def test_search_with_additional_query_params( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test search with additional_query_params.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data client.search_decisions( query="test", additional_query_params={"customParam": "customValue"} ) - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert params["customParam"] == "customValue" @@ -447,13 +447,13 @@ def test_get_decision_by_id( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test retrieving decision by ID.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data record_id = "9f1a4a2b-eee1-58ec-a3aa-167c4075aed4" result = client.get_decision_by_id(record_id) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint=f"api/v1/petition/decisions/{record_id}", params=None, @@ -468,13 +468,13 @@ def test_get_decision_by_id_with_documents( mock_petition_response_with_data: PetitionDecisionResponse, ) -> None: """Test retrieving decision by ID with includeDocuments parameter.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_with_data + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_with_data record_id = "9f1a4a2b-eee1-58ec-a3aa-167c4075aed4" result = client.get_decision_by_id(record_id, include_documents=True) - mock_make_request.assert_called_once_with( + mock_get_model.assert_called_once_with( method="GET", endpoint=f"api/v1/petition/decisions/{record_id}", params={"includeDocuments": "true"}, @@ -488,8 +488,8 @@ def test_get_decision_by_id_not_found( mock_petition_response_empty: PetitionDecisionResponse, ) -> None: """Test retrieving non-existent decision returns None.""" - client, mock_make_request = client_with_mocked_request - mock_make_request.return_value = mock_petition_response_empty + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = mock_petition_response_empty result = client.get_decision_by_id("nonexistent-id") assert result is None @@ -503,20 +503,32 @@ def test_download_json( client_with_mocked_request: tuple[FinalPetitionDecisionsClient, MagicMock], ) -> None: """Test downloading decisions in JSON format.""" - client, mock_make_request = client_with_mocked_request + client, mock_get_model = client_with_mocked_request - mock_dict = {"petitionDecisionData": [{"applicationNumberText": "12345678"}]} - mock_make_request.return_value = mock_dict + mock_response = PetitionDecisionDownloadResponse( + petition_decision_data=[ + PetitionDecision( + application_number_text="12345678", + petition_decision_record_identifier="test-id", + ) + ] + ) + mock_get_model.return_value = mock_response result = client.download_decisions(format="json", applicant_name_q="Test Corp") assert isinstance(result, PetitionDecisionDownloadResponse) assert len(result.petition_decision_data) == 1 - call_args = mock_make_request.call_args - params = call_args[1]["params"] - assert params["format"] == "json" - assert 'firstApplicantName:"Test Corp"' in params.get("q", "") + mock_get_model.assert_called_once_with( + method="GET", + endpoint="api/v1/petition/decisions/search/download", + response_class=PetitionDecisionDownloadResponse, + params={ + "format": "json", + "q": 'firstApplicantName:"Test Corp"', + }, + ) def test_download_csv( self, @@ -573,13 +585,14 @@ def test_download_with_patent_number( client_with_mocked_request: tuple[FinalPetitionDecisionsClient, MagicMock], ) -> None: """Test download with patent_number_q parameter.""" - client, mock_make_request = client_with_mocked_request - mock_dict = {"petitionDecisionData": []} - mock_make_request.return_value = mock_dict + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PetitionDecisionDownloadResponse( + petition_decision_data=[] + ) client.download_decisions(format="json", patent_number_q="10123456") - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert "patentNumber:10123456" in params["q"] @@ -588,13 +601,14 @@ def test_download_with_application_number( client_with_mocked_request: tuple[FinalPetitionDecisionsClient, MagicMock], ) -> None: """Test download with application_number_q parameter.""" - client, mock_make_request = client_with_mocked_request - mock_dict = {"petitionDecisionData": []} - mock_make_request.return_value = mock_dict + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PetitionDecisionDownloadResponse( + petition_decision_data=[] + ) client.download_decisions(format="json", application_number_q="17765301") - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert "applicationNumberText:17765301" in params["q"] @@ -603,13 +617,14 @@ def test_download_with_inventor_name( client_with_mocked_request: tuple[FinalPetitionDecisionsClient, MagicMock], ) -> None: """Test download with inventor_name_q parameter.""" - client, mock_make_request = client_with_mocked_request - mock_dict = {"petitionDecisionData": []} - mock_make_request.return_value = mock_dict + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PetitionDecisionDownloadResponse( + petition_decision_data=[] + ) client.download_decisions(format="json", inventor_name_q="Jane Doe") - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert 'inventorBag:"Jane Doe"' in params["q"] @@ -618,13 +633,14 @@ def test_download_with_decision_date_from_only( client_with_mocked_request: tuple[FinalPetitionDecisionsClient, MagicMock], ) -> None: """Test download with only decision_date_from_q parameter.""" - client, mock_make_request = client_with_mocked_request - mock_dict = {"petitionDecisionData": []} - mock_make_request.return_value = mock_dict + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PetitionDecisionDownloadResponse( + petition_decision_data=[] + ) client.download_decisions(format="json", decision_date_from_q="2023-01-01") - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert "decisionDate:>=2023-01-01" in params["q"] @@ -633,13 +649,14 @@ def test_download_with_decision_date_to_only( client_with_mocked_request: tuple[FinalPetitionDecisionsClient, MagicMock], ) -> None: """Test download with only decision_date_to_q parameter.""" - client, mock_make_request = client_with_mocked_request - mock_dict = {"petitionDecisionData": []} - mock_make_request.return_value = mock_dict + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PetitionDecisionDownloadResponse( + petition_decision_data=[] + ) client.download_decisions(format="json", decision_date_to_q="2023-12-31") - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert "decisionDate:<=2023-12-31" in params["q"] @@ -648,9 +665,10 @@ def test_download_with_optional_params( client_with_mocked_request: tuple[FinalPetitionDecisionsClient, MagicMock], ) -> None: """Test download with optional sort, fields, filters, and range_filters.""" - client, mock_make_request = client_with_mocked_request - mock_dict = {"petitionDecisionData": []} - mock_make_request.return_value = mock_dict + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PetitionDecisionDownloadResponse( + petition_decision_data=[] + ) client.download_decisions( format="json", @@ -661,7 +679,7 @@ def test_download_with_optional_params( range_filters="decisionDate:[2020 TO 2024]", ) - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert params["sort"] == "decisionDate desc" assert params["fields"] == "applicationNumberText" @@ -673,13 +691,14 @@ def test_download_with_offset_and_limit( client_with_mocked_request: tuple[FinalPetitionDecisionsClient, MagicMock], ) -> None: """Test download with offset and limit parameters.""" - client, mock_make_request = client_with_mocked_request - mock_dict = {"petitionDecisionData": []} - mock_make_request.return_value = mock_dict + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PetitionDecisionDownloadResponse( + petition_decision_data=[] + ) client.download_decisions(format="json", offset=50, limit=100) - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert params["offset"] == 50 assert params["limit"] == 100 @@ -689,15 +708,16 @@ def test_download_with_additional_query_params( client_with_mocked_request: tuple[FinalPetitionDecisionsClient, MagicMock], ) -> None: """Test download with additional_query_params.""" - client, mock_make_request = client_with_mocked_request - mock_dict = {"petitionDecisionData": []} - mock_make_request.return_value = mock_dict + client, mock_get_model = client_with_mocked_request + mock_get_model.return_value = PetitionDecisionDownloadResponse( + petition_decision_data=[] + ) client.download_decisions( format="json", additional_query_params={"customParam": "customValue"} ) - call_args = mock_make_request.call_args + call_args = mock_get_model.call_args params = call_args[1]["params"] assert params["customParam"] == "customValue" From 5a8d6c0491511c46e934a5453dacda79a2d482db Mon Sep 17 00:00:00 2001 From: Andrew <3300522+dpieski@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:16:54 -0500 Subject: [PATCH 6/6] fix: update integration tests --- .../test_patent_data_integration.py | 30 ++++++------ .../test_petition_decisions_integration.py | 46 +++++++++---------- .../test_ptab_appeals_integration.py | 32 +++++++------ 3 files changed, 56 insertions(+), 52 deletions(-) diff --git a/tests/integration/test_patent_data_integration.py b/tests/integration/test_patent_data_integration.py index d34639b..41a3d4b 100644 --- a/tests/integration/test_patent_data_integration.py +++ b/tests/integration/test_patent_data_integration.py @@ -144,7 +144,7 @@ def test_to_dict_matches_raw_api_response(self, api_key: str | None) -> None: # API returns naive datetime strings (e.g., '2025-12-03T07:21:12') without timezone # indicators, but we serialize with UTC 'Z' suffix (e.g., '2025-12-03T12:21:12Z'). # Waiting for USPTO ODP to adopt UTC standard for datetime fields. - # pytest.skip( + # pytest.fail( # "Test disabled pending USPTO API fix for datetime format. See issue #17" # ) @@ -621,15 +621,15 @@ def test_download_application_document( assert os.path.exists(file_path) assert os.path.getsize(file_path) > 0 except USPTOApiNotFoundError: - pytest.skip( + pytest.fail( f"Document or application not found (404) for download: {self.KNOWN_APP_NUM_WITH_DOCS}" ) except USPTOApiError as e: - pytest.skip( + pytest.fail( f"Document download failed for {self.KNOWN_APP_NUM_WITH_DOCS}: {e}" ) except IndexError: - pytest.skip( + pytest.fail( f"No documents available in bag for {self.KNOWN_APP_NUM_WITH_DOCS} to test download." ) @@ -704,7 +704,7 @@ def test_get_search_results_post( f"No PatentData returned for US App No.: {self.KNOWN_APP_NUM_WITH_DOCS} with: {post_body_request}" ) except USPTOApiError as e: - pytest.skip(f"get_search_results POST test failed: {e}") + pytest.fail(f"get_search_results POST test failed: {e}") def test_search_status_codes_post( self, patent_data_client: PatentDataClient @@ -732,7 +732,7 @@ def test_search_status_codes_post( ) except USPTOApiError as e: - pytest.skip(f"Status codes POST search failed: {e}") + pytest.fail(f"Status codes POST search failed: {e}") def test_download_xml_document_extracts_tar( self, patent_data_client: PatentDataClient @@ -743,7 +743,7 @@ def test_download_xml_document_extracts_tar( "19312841", document_codes=["CTNF", "CTFR"] ) if not docs or not docs.documents: - pytest.skip( + pytest.fail( "No CTNF/CTFR documents found for test application 19312841" ) @@ -758,7 +758,7 @@ def test_download_xml_document_extracts_tar( break if not xml_doc: - pytest.skip("No XML format document found for test") + pytest.fail("No XML format document found for test") file_path = patent_data_client.download_document( document=xml_doc, @@ -772,7 +772,7 @@ def test_download_xml_document_extracts_tar( assert os.path.getsize(file_path) > 0 except (USPTOApiNotFoundError, USPTOApiError) as e: - pytest.skip(f"API error during XML download test: {e}") + pytest.fail(f"API error during XML download test: {e}") def test_content_disposition_filename_used( self, patent_data_client: PatentDataClient @@ -783,13 +783,13 @@ def test_content_disposition_filename_used( self.KNOWN_APP_NUM_WITH_DOCS ) if not docs or not docs.documents: - pytest.skip(f"No documents found for {self.KNOWN_APP_NUM_WITH_DOCS}") + pytest.fail(f"No documents found for {self.KNOWN_APP_NUM_WITH_DOCS}") doc_to_download = next( (d for d in docs.documents if d.document_formats), None ) if not doc_to_download: - pytest.skip("No downloadable document found") + pytest.fail("No downloadable document found") file_path = patent_data_client.download_document( document=doc_to_download, @@ -802,7 +802,7 @@ def test_content_disposition_filename_used( assert os.path.basename(file_path) != "download" except (USPTOApiNotFoundError, USPTOApiError) as e: - pytest.skip(f"API error during Content-Disposition test: {e}") + pytest.fail(f"API error during Content-Disposition test: {e}") def test_download_with_format_enum( self, patent_data_client: PatentDataClient @@ -813,13 +813,13 @@ def test_download_with_format_enum( self.KNOWN_APP_NUM_WITH_DOCS ) if not docs or not docs.documents: - pytest.skip(f"No documents found for {self.KNOWN_APP_NUM_WITH_DOCS}") + pytest.fail(f"No documents found for {self.KNOWN_APP_NUM_WITH_DOCS}") doc_to_download = next( (d for d in docs.documents if d.document_formats), None ) if not doc_to_download: - pytest.skip("No downloadable document found") + pytest.fail("No downloadable document found") file_path = patent_data_client.download_document( document=doc_to_download, @@ -833,7 +833,7 @@ def test_download_with_format_enum( assert os.path.getsize(file_path) > 0 except (USPTOApiNotFoundError, USPTOApiError) as e: - pytest.skip(f"API error during enum format test: {e}") + pytest.fail(f"API error during enum format test: {e}") def test_invalid_application_number_handling( self, patent_data_client: PatentDataClient diff --git a/tests/integration/test_petition_decisions_integration.py b/tests/integration/test_petition_decisions_integration.py index 02f93f2..1b290ec 100644 --- a/tests/integration/test_petition_decisions_integration.py +++ b/tests/integration/test_petition_decisions_integration.py @@ -68,14 +68,14 @@ def sample_petition_decision( ): return response.petition_decision_data_bag[0] - pytest.skip( + pytest.fail( "Could not retrieve a sample petition decision. Ensure API is reachable." ) except USPTOApiError as e: - pytest.skip(f"Could not fetch sample petition decision due to API error: {e}") + pytest.fail(f"Could not fetch sample petition decision due to API error: {e}") except Exception as e: - pytest.skip( + pytest.fail( f"Could not fetch sample petition decision due to unexpected error: {e}" ) # This return is unreachable but satisfies type checker @@ -90,7 +90,7 @@ def sample_petition_decision_id(sample_petition_decision: PetitionDecision) -> s """ if sample_petition_decision.petition_decision_record_identifier: return sample_petition_decision.petition_decision_record_identifier - pytest.skip("Sample petition decision does not have a record identifier") + pytest.fail("Sample petition decision does not have a record identifier") return "" @@ -138,9 +138,9 @@ def test_search_decisions_with_query( assert len(response.petition_decision_data_bag) <= 3 except USPTOApiNotFoundError: # 404 may be returned if no records match the query - pytest.skip("No records found matching query criteria") + pytest.fail("No records found matching query criteria") except USPTOApiError as e: - pytest.skip(f"Query search failed: {e}") + pytest.fail(f"Query search failed: {e}") def test_search_decisions_with_application_number( self, @@ -150,7 +150,7 @@ def test_search_decisions_with_application_number( """Test searching using convenience application_number_q parameter.""" # Use the application number from the sample decision if not sample_petition_decision.application_number_text: - pytest.skip( + pytest.fail( "Sample decision does not have an application number to test with" ) @@ -174,7 +174,7 @@ def test_search_decisions_with_application_number( found ), f"Expected to find application number {app_num} in results" except USPTOApiError as e: - pytest.skip(f"Application number search failed: {e}") + pytest.fail(f"Application number search failed: {e}") def test_search_decisions_with_patent_number( self, @@ -190,7 +190,7 @@ def test_search_decisions_with_patent_number( if not patent_num: response = petition_decisions_client.search_decisions(limit=20) if response.count == 0: - pytest.skip("No decisions available to test patent number search") + pytest.fail("No decisions available to test patent number search") # Find a decision with a patent number for decision in response.petition_decision_data_bag: @@ -199,7 +199,7 @@ def test_search_decisions_with_patent_number( break if not patent_num: - pytest.skip( + pytest.fail( "No decisions with patent numbers found in first 20 results" ) @@ -223,7 +223,7 @@ def test_search_decisions_with_patent_number( found ), f"Expected to find patent number {patent_num} in results but count is {response.count}" except USPTOApiError as e: - pytest.skip(f"Patent number search failed: {e}") + pytest.fail(f"Patent number search failed: {e}") def test_search_decisions_with_technology_center( self, petition_decisions_client: FinalPetitionDecisionsClient @@ -275,11 +275,11 @@ def test_get_decision_by_id( ) assert decision.decision_type_code is not None except USPTOApiNotFoundError: - pytest.skip( + pytest.fail( f"Decision not found (404) for ID {sample_petition_decision_id}" ) except USPTOApiError as e: - pytest.skip(f"Failed to get decision by ID: {e}") + pytest.fail(f"Failed to get decision by ID: {e}") def test_round_trip_data_integrity( self, @@ -294,7 +294,7 @@ def test_round_trip_data_integrity( ) if original is None: - pytest.skip( + pytest.fail( f"No decision data returned for {sample_petition_decision_id}" ) @@ -321,11 +321,11 @@ def test_round_trip_data_integrity( ) except USPTOApiNotFoundError: - pytest.skip( + pytest.fail( f"Decision not found (404) for round-trip test: {sample_petition_decision_id}" ) except USPTOApiError as e: - pytest.skip(f"Round-trip test failed due to API error: {e}") + pytest.fail(f"Round-trip test failed due to API error: {e}") def test_to_dict_matches_raw_api_response( self, api_key: str | None, sample_petition_decision_id: str @@ -340,7 +340,7 @@ def test_to_dict_matches_raw_api_response( # API returns naive datetime strings (e.g., '2025-12-03T07:21:12') without timezone # indicators, but we serialize with UTC 'Z' suffix (e.g., '2025-12-03T12:21:12Z'). # Waiting for USPTO ODP to adopt UTC standard for datetime fields. - # pytest.skip( + # pytest.fail( # "Test disabled pending USPTO API fix for datetime format. See issue #17" # ) @@ -356,7 +356,7 @@ def test_to_dict_matches_raw_api_response( ) if response is None or response.count == 0: - pytest.skip( + pytest.fail( f"No decision found for raw API comparison: {sample_petition_decision_id}" ) @@ -456,11 +456,11 @@ def compare_dicts(dict1, dict2, path=""): ) except USPTOApiNotFoundError: - pytest.skip( + pytest.fail( f"Decision not found (404) for raw API comparison: {sample_petition_decision_id}" ) except USPTOApiError as e: - pytest.skip(f"Raw API comparison failed due to API error: {e}") + pytest.fail(f"Raw API comparison failed due to API error: {e}") def test_get_decision_by_invalid_id( self, petition_decisions_client: FinalPetitionDecisionsClient @@ -497,7 +497,7 @@ def test_download_decisions_json( assert len(response.petition_decision_data) <= 2 assert isinstance(response.petition_decision_data[0], PetitionDecision) except USPTOApiError as e: - pytest.skip(f"Download endpoint failed: {e}") + pytest.fail(f"Download endpoint failed: {e}") def test_download_decisions_csv( self, petition_decisions_client: FinalPetitionDecisionsClient @@ -528,7 +528,7 @@ def test_download_decisions_csv( assert "," in first_line except USPTOApiError as e: - pytest.skip(f"CSV download endpoint failed: {e}") + pytest.fail(f"CSV download endpoint failed: {e}") def test_paginate_decisions( self, petition_decisions_client: FinalPetitionDecisionsClient @@ -554,7 +554,7 @@ def test_paginate_decisions( assert total_decisions > 0, "Should have retrieved at least one decision" except USPTOApiError as e: - pytest.skip(f"Pagination test failed: {e}") + pytest.fail(f"Pagination test failed: {e}") def test_decision_with_documents_and_download( self, diff --git a/tests/integration/test_ptab_appeals_integration.py b/tests/integration/test_ptab_appeals_integration.py index 576b8c6..6fe7152 100644 --- a/tests/integration/test_ptab_appeals_integration.py +++ b/tests/integration/test_ptab_appeals_integration.py @@ -56,7 +56,7 @@ def appeals_with_download_uris( PTABAppealResponse: Response containing appeals with download URIs """ return ptab_appeals_client.search_decisions( - query="appealMetaData.applicationTypeCategory:Appeal", + query="appealMetaData.applicationTypeCategory:REGULAR", limit=5, ) @@ -68,7 +68,7 @@ def test_search_decisions_get(self, ptab_appeals_client: PTABAppealsClient) -> N """Test searching PTAB appeal decisions using GET method.""" try: response = ptab_appeals_client.search_decisions( - query="appealMetaData.applicationTypeCategory:Appeal", + query="appealMetaData.applicationTypeCategory:REGULAR", limit=2, ) @@ -123,7 +123,7 @@ def test_search_decisions_post( ) -> None: """Test searching PTAB appeal decisions using POST method.""" post_body = { - "q": "appealMetaData.applicationTypeCategory:Appeal", + "q": "appealMetaData.applicationTypeCategory:REGULAR", "pagination": {"offset": 0, "limit": 2}, } @@ -223,7 +223,7 @@ def test_paginate_decisions(self, ptab_appeals_client: PTABAppealsClient) -> Non try: for decision in ptab_appeals_client.paginate_decisions( - query="appealMetaData.applicationTypeCategory:Appeal", + query="appealMetaData.applicationTypeCategory:REGULAR", limit=page_size, ): assert isinstance(decision, PTABAppealDecision) @@ -245,7 +245,7 @@ def test_search_with_optional_params( """Test searching with optional parameters like sort and facets.""" try: response = ptab_appeals_client.search_decisions( - query="appealMetaData.applicationTypeCategory:Appeal", + query="appealMetaData.applicationTypeCategory:REGULAR", limit=2, sort="appealNumber desc", offset=0, @@ -380,7 +380,7 @@ def test_download_appeal_archive( not appeals_with_download_uris or not appeals_with_download_uris.patent_appeal_data_bag ): - pytest.skip("No appeal decisions found for archive download test") + pytest.fail("No appeal decisions found for archive download test") # Try multiple appeals until one downloads successfully last_error = None @@ -412,9 +412,11 @@ def test_download_appeal_archive( # If we tried all appeals and none worked, fail with the last error if last_error: - pytest.fail(f"All appeal archives failed to download. Last error: {last_error}") + pytest.fail( + f"All appeal archives failed to download. Last error: {last_error}" + ) else: - pytest.skip("No appeal with valid file_download_uri found") + pytest.fail("No appeal with valid file_download_uri found") except USPTOApiError as e: pytest.fail(f"PTAB Appeals API error during search: {e}") @@ -430,7 +432,7 @@ def test_download_appeal_documents( not appeals_with_download_uris or not appeals_with_download_uris.patent_appeal_data_bag ): - pytest.skip("No appeal decisions found for documents download test") + pytest.fail("No appeal decisions found for documents download test") # Try multiple appeals until one downloads successfully last_error = None @@ -460,9 +462,11 @@ def test_download_appeal_documents( # If we tried all appeals and none worked, fail with the last error if last_error: - pytest.fail(f"All appeal documents failed to download. Last error: {last_error}") + pytest.fail( + f"All appeal documents failed to download. Last error: {last_error}" + ) else: - pytest.skip("No appeal with valid file_download_uri found") + pytest.fail("No appeal with valid file_download_uri found") except USPTOApiError as e: pytest.fail(f"PTAB Appeals API error during search: {e}") @@ -473,19 +477,19 @@ def test_download_appeal_document( """Test downloading individual appeal document.""" try: response = ptab_appeals_client.search_decisions( - query="appealMetaData.applicationTypeCategory:Appeal", + query="appealMetaData.applicationTypeCategory:REGULAR", limit=1, ) if not response or not response.patent_appeal_data_bag: - pytest.skip("No appeal decisions found for document download test") + pytest.fail("No appeal decisions found for document download test") decision = response.patent_appeal_data_bag[0] if ( not decision.document_data or not decision.document_data.file_download_uri ): - pytest.skip("No file_download_uri available for test") + pytest.fail("No file_download_uri available for test") file_path = ptab_appeals_client.download_appeal_document( decision.document_data,