diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..5c11031 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,10 @@ +# Coverage gate for the generated SDK. +# +# pytest-cov reads this file automatically for `make test` / `make cover` / tox, +# all of which run `pytest --cov=aspose_barcode_cloud`. The build fails when line +# coverage of the SDK drops below the 80% requirement (see the repo Agents.md). +[run] +source = aspose_barcode_cloud + +[report] +fail_under = 80 diff --git a/README.md b/README.md index c7ebdd5..3e25e8d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![PyPI](https://img.shields.io/pypi/v/aspose-barcode-cloud)](https://pypi.org/project/aspose-barcode-cloud/) - API version: 4.0 -- Package version: 26.6.0 +- Package version: 26.7.0 ## SDK and API Version Compatibility: diff --git a/aspose_barcode_cloud/api_client.py b/aspose_barcode_cloud/api_client.py index b025171..989c28f 100644 --- a/aspose_barcode_cloud/api_client.py +++ b/aspose_barcode_cloud/api_client.py @@ -60,13 +60,13 @@ def __init__(self, configuration=None, header_name=None, header_value=None, cook self.rest_client = RESTClientObject(configuration) self.default_headers = { "x-aspose-client": "python sdk", - "x-aspose-client-version": "26.6.0", + "x-aspose-client-version": "26.7.0", } if header_name is not None: self.default_headers[header_name] = header_value self.cookie = cookie # Set default User-Agent. - self.user_agent = "Aspose-Barcode-SDK/26.6.0/python" + self.user_agent = "Aspose-Barcode-SDK/26.7.0/python" def __del__(self): self.rest_client.close() diff --git a/aspose_barcode_cloud/api_response.py b/aspose_barcode_cloud/api_response.py deleted file mode 100644 index 1ce1372..0000000 --- a/aspose_barcode_cloud/api_response.py +++ /dev/null @@ -1,20 +0,0 @@ -"""API response object.""" - -from __future__ import annotations -from typing import Optional, Generic, Mapping, TypeVar -from pydantic import Field, StrictInt, StrictBytes, BaseModel - -T = TypeVar("T") - - -class ApiResponse(BaseModel, Generic[T]): - """ - API response object - """ - - status_code: StrictInt = Field(description="HTTP status code") - headers: Optional[Mapping[str, str]] = Field(None, description="HTTP headers") - data: T = Field(description="Deserialized data given the data type") - raw_data: StrictBytes = Field(description="Raw data (HTTP response body)") - - model_config = {"arbitrary_types_allowed": True} diff --git a/aspose_barcode_cloud/configuration.py b/aspose_barcode_cloud/configuration.py index 58d4cbc..e93fad6 100644 --- a/aspose_barcode_cloud/configuration.py +++ b/aspose_barcode_cloud/configuration.py @@ -258,7 +258,7 @@ def to_debug_report(self): "OS: {env}\n" "Python Version: {pyversion}\n" "Version of the API: 4.0\n" - "SDK Package Version: 26.6.0".format(env=sys.platform, pyversion=sys.version) + "SDK Package Version: 26.7.0".format(env=sys.platform, pyversion=sys.version) ) @staticmethod diff --git a/aspose_barcode_cloud/exceptions.py b/aspose_barcode_cloud/exceptions.py deleted file mode 100644 index 267e3c1..0000000 --- a/aspose_barcode_cloud/exceptions.py +++ /dev/null @@ -1,210 +0,0 @@ -# coding: utf-8 - -""" - - Copyright (c) 2026 Aspose.BarCode for Cloud - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -""" - -from typing import Any, Optional -from typing_extensions import Self - - -class OpenApiException(Exception): - """The base exception class for all OpenAPIExceptions""" - - -class ApiTypeError(OpenApiException, TypeError): - def __init__(self, msg, path_to_item=None, valid_classes=None, key_type=None) -> None: - """Raises an exception for TypeErrors - - Args: - msg (str): the exception message - - Keyword Args: - path_to_item (list): a list of keys an indices to get to the - current_item - None if unset - valid_classes (tuple): the primitive classes that current item - should be an instance of - None if unset - key_type (bool): False if our value is a value in a dict - True if it is a key in a dict - False if our item is an item in a list - None if unset - """ - self.path_to_item = path_to_item - self.valid_classes = valid_classes - self.key_type = key_type - full_msg = msg - if path_to_item: - full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) - super(ApiTypeError, self).__init__(full_msg) - - -class ApiValueError(OpenApiException, ValueError): - def __init__(self, msg, path_to_item=None) -> None: - """ - Args: - msg (str): the exception message - - Keyword Args: - path_to_item (list) the path to the exception in the - received_data dict. None if unset - """ - - self.path_to_item = path_to_item - full_msg = msg - if path_to_item: - full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) - super(ApiValueError, self).__init__(full_msg) - - -class ApiAttributeError(OpenApiException, AttributeError): - def __init__(self, msg, path_to_item=None) -> None: - """ - Raised when an attribute reference or assignment fails. - - Args: - msg (str): the exception message - - Keyword Args: - path_to_item (None/list) the path to the exception in the - received_data dict - """ - self.path_to_item = path_to_item - full_msg = msg - if path_to_item: - full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) - super(ApiAttributeError, self).__init__(full_msg) - - -class ApiKeyError(OpenApiException, KeyError): - def __init__(self, msg, path_to_item=None) -> None: - """ - Args: - msg (str): the exception message - - Keyword Args: - path_to_item (None/list) the path to the exception in the - received_data dict - """ - self.path_to_item = path_to_item - full_msg = msg - if path_to_item: - full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) - super(ApiKeyError, self).__init__(full_msg) - - -class ApiException(OpenApiException): - - def __init__( - self, - status=None, - reason=None, - http_resp=None, - *, - body: Optional[str] = None, - data: Optional[Any] = None, - ) -> None: - self.status = status - self.reason = reason - self.body = body - self.data = data - self.headers = None - - if http_resp: - if self.status is None: - self.status = http_resp.status - if self.reason is None: - self.reason = http_resp.reason - if self.body is None: - try: - self.body = http_resp.data.decode("utf-8") - except Exception: - pass - self.headers = http_resp.getheaders() - - @classmethod - def from_response( - cls, - *, - http_resp, - body: Optional[str], - data: Optional[Any], - ) -> Self: - if http_resp.status == 400: - raise BadRequestException(http_resp=http_resp, body=body, data=data) - - if http_resp.status == 401: - raise UnauthorizedException(http_resp=http_resp, body=body, data=data) - - if http_resp.status == 403: - raise ForbiddenException(http_resp=http_resp, body=body, data=data) - - if http_resp.status == 404: - raise NotFoundException(http_resp=http_resp, body=body, data=data) - - if 500 <= http_resp.status <= 599: - raise ServiceException(http_resp=http_resp, body=body, data=data) - raise ApiException(http_resp=http_resp, body=body, data=data) - - def __str__(self): - """Custom error messages for exception""" - error_message = "({0})\n" "Reason: {1}\n".format(self.status, self.reason) - if self.headers: - error_message += "HTTP response headers: {0}\n".format(self.headers) - - if self.data or self.body: - error_message += "HTTP response body: {0}\n".format(self.data or self.body) - - return error_message - - -class BadRequestException(ApiException): - pass - - -class NotFoundException(ApiException): - pass - - -class UnauthorizedException(ApiException): - pass - - -class ForbiddenException(ApiException): - pass - - -class ServiceException(ApiException): - pass - - -def render_path(path_to_item): - """Returns a string representation of a path""" - result = "" - for pth in path_to_item: - if isinstance(pth, int): - result += "[{0}]".format(pth) - else: - result += "['{0}']".format(pth) - return result diff --git a/scripts/check-badges.bash b/scripts/check-badges.bash index 578d958..a79bc39 100755 --- a/scripts/check-badges.bash +++ b/scripts/check-badges.bash @@ -16,7 +16,7 @@ pushd "${SCRIPT_DIR}/.." >/dev/null fi done -(grep -oP '[^/]+\.yml/badge.svg(?!\?branch=main)' "${readme_file}" || echo ) | while read -r badge_without_branch; do +(grep -oP '[^/]+\.yml/badge.svg(?!\?branch=(main|v4)\b)' "${readme_file}" || echo ) | while read -r badge_without_branch; do if [ -z "${badge_without_branch}" ]; then continue; fi >&2 echo "Badge without branch \"${badge_without_branch}\"" exit 1 diff --git a/setup.py b/setup.py index 73c658c..71586d6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages NAME = "aspose-barcode-cloud" -VERSION = "26.6.0" +VERSION = "26.7.0" # To install the library, run the following # # python setup.py install diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..56fd9c0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,63 @@ +# coding: utf-8 + +"""Shared pytest fixtures for the offline request-building tests. + +Provides the mocked ``RESTClientObject`` transport and an ``ApiClient`` wired to it +(the same pattern as ``test_headers.py``), so the ``test_*_offline.py`` files can drive +every generated API method without a single live API call. +""" + +import json +import typing +from unittest import mock + +import pytest + +from aspose_barcode_cloud.api_client import ApiClient +from aspose_barcode_cloud.configuration import Configuration +from aspose_barcode_cloud.rest import RESTClientObject + +FILE_URL: typing.Final = "https://barcode.example/image.png" + +BARCODES_JSON: typing.Final = json.dumps( + {"barcodes": [{"barcodeValue": "Test", "type": "QR", "region": [{"x": 1, "y": 2}], "checksum": "123"}]} +).encode("utf-8") + + +class _FakeRestResponse: + """Explicit stand-in for ``rest.RESTResponse`` returned by the mocked transport.""" + + def __init__(self, data=BARCODES_JSON, headers=None): + # type: (bytes, typing.Optional[typing.Dict[str, str]]) -> None + self.status = 200 + self.reason = "OK" + self.data = data + self._headers = headers if headers is not None else {"Content-Type": "application/json"} + + def getheaders(self): + # type: () -> typing.Dict[str, str] + return self._headers + + def getheader(self, name, default=None): + # type: (str, typing.Optional[str]) -> typing.Optional[str] + return self._headers.get(name, default) + + +@pytest.fixture() +def rest_client(): + # type: () -> mock.Mock + client = mock.Mock(spec_set=RESTClientObject) + for method in ("GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"): + getattr(client, method).return_value = _FakeRestResponse() + return client + + +@pytest.fixture() +def api_client(rest_client): + # type: (mock.Mock) -> ApiClient + client = ApiClient( + Configuration(access_token="fake-token", host="http://localhost"), + cookie="session=fake-cookie", + ) + client.rest_client = rest_client + return client diff --git a/tests/model_factories.py b/tests/model_factories.py new file mode 100644 index 0000000..d2236fb --- /dev/null +++ b/tests/model_factories.py @@ -0,0 +1,204 @@ +# coding: utf-8 + +"""Factories building fully populated instances of the generated models and enums. + +Shared between the value-object tests in ``test_models_coverage.py`` / ``test_api_client_offline.py`` +and the offline request-building tests (mirrors the Java SDK's ``newModel`` helpers). +""" + +import datetime +import typing + +from aspose_barcode_cloud.models import ( + ApiError, + ApiErrorResponse, + BarcodeImageFormat, + BarcodeImageParams, + BarcodeResponse, + BarcodeResponseList, + Code128EncodeMode, + Code128Params, + CodeLocation, + DecodeBarcodeType, + ECIEncodings, + EncodeBarcodeType, + EncodeData, + EncodeDataType, + GenerateParams, + GraphicsUnit, + MacroCharacter, + MicroQRVersion, + Pdf417EncodeMode, + Pdf417ErrorLevel, + Pdf417Params, + QREncodeMode, + QRErrorLevel, + QRVersion, + QrParams, + RecognitionImageKind, + RecognitionMode, + RecognizeBase64Request, + RectMicroQRVersion, + RegionPoint, + ScanBase64Request, +) + +ENUM_TYPES: typing.Final = ( + BarcodeImageFormat, + Code128EncodeMode, + CodeLocation, + DecodeBarcodeType, + ECIEncodings, + EncodeBarcodeType, + EncodeDataType, + GraphicsUnit, + MacroCharacter, + MicroQRVersion, + Pdf417EncodeMode, + Pdf417ErrorLevel, + QREncodeMode, + QRErrorLevel, + QRVersion, + RecognitionImageKind, + RecognitionMode, + RectMicroQRVersion, +) + +MODEL_TYPES: typing.Final = ( + ApiError, + ApiErrorResponse, + BarcodeImageParams, + BarcodeResponse, + BarcodeResponseList, + Code128Params, + EncodeData, + GenerateParams, + Pdf417Params, + QrParams, + RecognizeBase64Request, + RegionPoint, + ScanBase64Request, +) + + +def _enum_value(enum_type: type, index: int) -> str: + """Return one of the wire string constants declared on a generated enum class.""" + constants = [value for name, value in vars(enum_type).items() if name.isupper() and isinstance(value, str)] + assert constants, "enum {0} declares no string constants".format(enum_type.__name__) + return constants[index % len(constants)] + + +def _barcode_image_params(variant: int) -> BarcodeImageParams: + return BarcodeImageParams( + image_format=_enum_value(BarcodeImageFormat, variant), + text_location=_enum_value(CodeLocation, variant), + foreground_color="Black-{0}".format(variant), + background_color="White-{0}".format(variant), + units=_enum_value(GraphicsUnit, variant), + resolution=100.0 + variant, + image_height=200.0 + variant, + image_width=300.0 + variant, + rotation_angle=90 + variant, + ) + + +def _qr_params(variant: int) -> QrParams: + return QrParams( + qr_encode_mode=_enum_value(QREncodeMode, variant), + qr_error_level=_enum_value(QRErrorLevel, variant), + qr_version=_enum_value(QRVersion, variant), + qr_eci_encoding=_enum_value(ECIEncodings, variant), + qr_aspect_ratio=0.75, + micro_qr_version=_enum_value(MicroQRVersion, variant), + rect_micro_qr_version=_enum_value(RectMicroQRVersion, variant), + ) + + +def _code128_params(variant: int) -> Code128Params: + return Code128Params(code128_encode_mode=_enum_value(Code128EncodeMode, variant)) + + +def _pdf417_params(variant: int) -> Pdf417Params: + return Pdf417Params( + pdf417_encode_mode=_enum_value(Pdf417EncodeMode, variant), + pdf417_error_level=_enum_value(Pdf417ErrorLevel, variant), + pdf417_truncate=variant == 1, + pdf417_columns=5, + pdf417_rows=12, + pdf417_aspect_ratio=3.0, + pdf417_eci_encoding=_enum_value(ECIEncodings, variant), + pdf417_is_reader_initialization=False, + pdf417_macro_characters=_enum_value(MacroCharacter, variant), + pdf417_is_linked=False, + pdf417_is_code128_emulation=False, + ) + + +def _encode_data(variant: int) -> EncodeData: + return EncodeData(data="data-{0}".format(variant), data_type=_enum_value(EncodeDataType, variant)) + + +def _region_point(variant: int) -> RegionPoint: + return RegionPoint(x=10 + variant, y=20 + variant) + + +def _barcode_response(variant: int) -> BarcodeResponse: + return BarcodeResponse( + barcode_value="value-{0}".format(variant), + type="QR", + region=[_region_point(variant)], + checksum="checksum-{0}".format(variant), + ) + + +def _api_error(variant: int, with_inner: bool = True) -> ApiError: + return ApiError( + code="code-{0}".format(variant), + message="message-{0}".format(variant), + description="description-{0}".format(variant), + date_time=datetime.datetime(2026, 6, 29, 0, 0, variant), + inner_error=_api_error(variant, with_inner=False) if with_inner else None, + ) + + +def _make_model(model_type: type, variant: int): + """Build a fully populated instance of a generated model (mirrors Java ``newModel``).""" + if model_type is ApiError: + return _api_error(variant) + if model_type is ApiErrorResponse: + return ApiErrorResponse(request_id="request-{0}".format(variant), error=_api_error(variant)) + if model_type is BarcodeImageParams: + return _barcode_image_params(variant) + if model_type is BarcodeResponse: + return _barcode_response(variant) + if model_type is BarcodeResponseList: + return BarcodeResponseList(barcodes=[_barcode_response(variant)]) + if model_type is Code128Params: + return _code128_params(variant) + if model_type is EncodeData: + return _encode_data(variant) + if model_type is GenerateParams: + return GenerateParams( + barcode_type=_enum_value(EncodeBarcodeType, variant), + encode_data=_encode_data(variant), + barcode_image_params=_barcode_image_params(variant), + qr_params=_qr_params(variant), + code128_params=_code128_params(variant), + pdf417_params=_pdf417_params(variant), + ) + if model_type is Pdf417Params: + return _pdf417_params(variant) + if model_type is QrParams: + return _qr_params(variant) + if model_type is RecognizeBase64Request: + return RecognizeBase64Request( + barcode_types=[_enum_value(DecodeBarcodeType, variant)], + file_base64="file-{0}".format(variant), + recognition_mode=_enum_value(RecognitionMode, variant), + recognition_image_kind=_enum_value(RecognitionImageKind, variant), + ) + if model_type is RegionPoint: + return _region_point(variant) + if model_type is ScanBase64Request: + return ScanBase64Request(file_base64="file-{0}".format(variant)) + raise AssertionError("no fixture for model {0}".format(model_type.__name__)) diff --git a/tests/test_api_client_offline.py b/tests/test_api_client_offline.py new file mode 100644 index 0000000..798c943 --- /dev/null +++ b/tests/test_api_client_offline.py @@ -0,0 +1,136 @@ +# coding: utf-8 + +"""Offline coverage for ``ApiClient``. + +Exercises the network-free ``ApiClient`` helpers (serialization sanitising, collection +formatting, header selection, file/post-parameter preparation), request dispatch and +response deserialization through the mocked ``RESTClientObject`` fixtures from +``conftest.py``. The network-free half mirrors the Java SDK's ``SdkCoreCoverageTest`` so +the plumbing in ``api_client.py`` stays above the 80% line-coverage gate regardless of +which live API calls run. +""" + +import datetime +import io +import typing + +import pytest + +from aspose_barcode_cloud.api_client import ApiClient +from aspose_barcode_cloud.configuration import Configuration +from aspose_barcode_cloud.models import GenerateParams, RegionPoint +from aspose_barcode_cloud.rest import ApiException + +from .conftest import _FakeRestResponse +from .model_factories import _make_model + + +def test_api_client_offline_helpers() -> None: + """Exercise the network-free helpers on ``ApiClient`` (mirrors Java ``SdkCoreCoverageTest``).""" + client = ApiClient(Configuration(access_token="fake-token")) + + client.user_agent = "unit-test" + assert client.user_agent == "unit-test" + client.set_default_header("X-Default", "default-value") + assert client.default_headers["X-Default"] == "default-value" + + assert client.sanitize_for_serialization(None) is None + assert client.sanitize_for_serialization("text") == "text" + assert client.sanitize_for_serialization([1, 2]) == [1, 2] + assert client.sanitize_for_serialization((1, 2)) == (1, 2) + assert client.sanitize_for_serialization({"k": "v"}) == {"k": "v"} + assert client.sanitize_for_serialization(datetime.date(2026, 6, 29)) == "2026-06-29" + assert client.sanitize_for_serialization(datetime.datetime(2026, 6, 29, 1, 2, 3)).startswith("2026-06-29") + + serialized = client.sanitize_for_serialization(_make_model(GenerateParams, 1)) + assert serialized["barcodeType"] + assert "encodeData" in serialized + + collection_formats = {"m": "multi", "c": "csv", "s": "ssv", "t": "tsv", "p": "pipes"} + formatted = dict( + client.parameters_to_tuples( + {"m": ["a", "b"], "c": ["a", "b"], "s": ["a", "b"], "t": ["a", "b"], "p": ["a", "b"], "plain": "x"}, + collection_formats, + ) + ) + assert formatted["c"] == "a,b" + assert formatted["s"] == "a b" + assert formatted["t"] == "a\tb" + assert formatted["p"] == "a|b" + assert formatted["plain"] == "x" + + assert client.select_header_accept([]) is None + assert client.select_header_accept(["application/json"]) == "application/json" + assert client.select_header_accept(["text/plain", "image/png"]) == "text/plain, image/png" + + assert client.select_header_content_type([]) == "application/json" + assert client.select_header_content_type(["*/*"]) == "application/json" + assert client.select_header_content_type(["text/plain"]) == "text/plain" + + assert client.prepare_one_file(b"raw-bytes") is not None + assert client.prepare_one_file(io.BytesIO(b"stream-bytes")) is not None + with pytest.raises(ApiException): + client.prepare_one_file(12345) + + post_params = client.prepare_post_parameters([("name", "value")], {"upload": b"file-bytes"}) + assert ("name", "value") in post_params + assert any(field == "upload" for field, _ in post_params) + + headers: typing.Dict[str, str] = {} + querys: typing.List[typing.Tuple[str, str]] = [] + assert client.update_params_for_auth(headers, querys, None) is None + + +def test_api_client_request_dispatch_offline(api_client, rest_client): + # type: (ApiClient, mock.Mock) -> None + for method in ("GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"): + response = api_client.request(method, "http://localhost/resource") + assert response is getattr(rest_client, method).return_value + assert getattr(rest_client, method).call_count == 1 + + with pytest.raises(ValueError, match="http method"): + api_client.request("TRACE", "http://localhost/resource") + + +class _FakeDeserializeResponse: + """Explicit stand-in carrying a raw JSON body for ``ApiClient.deserialize``.""" + + def __init__(self, data: bytes) -> None: + self.data = data + + +def test_api_client_deserialize_offline() -> None: + client = ApiClient(Configuration(access_token="fake-token")) + + assert client.deserialize(_FakeDeserializeResponse(b'"hello"'), "str") == "hello" + assert client.deserialize(_FakeDeserializeResponse(b"123"), "int") == 123 + assert client.deserialize(_FakeDeserializeResponse(b"1.5"), "float") == 1.5 + assert client.deserialize(_FakeDeserializeResponse(b"true"), "bool") is True + assert client.deserialize(_FakeDeserializeResponse(b'{"a": 1}'), "object") == {"a": 1} + assert client.deserialize(_FakeDeserializeResponse(b'"2026-06-29"'), "date") == datetime.date(2026, 6, 29) + assert client.deserialize(_FakeDeserializeResponse(b'"2026-06-29T01:02:03Z"'), "datetime").year == 2026 + + region = client.deserialize(_FakeDeserializeResponse(b'{"x": 10, "y": 20}'), "RegionPoint") + assert isinstance(region, RegionPoint) + assert region.x == 10 + assert region.y == 20 + + assert client.deserialize(_FakeDeserializeResponse(b"[1, 2, 3]"), "list[int]") == [1, 2, 3] + assert client.deserialize(_FakeDeserializeResponse(b'{"a": 1, "b": 2}'), "dict(str, int)") == {"a": 1, "b": 2} + + +def test_api_client_deserialize_file_offline(api_client, tmp_path): + # type: (ApiClient, typing.Any) -> None + api_client.configuration.temp_folder_path = str(tmp_path) + + named = api_client.deserialize( + _FakeRestResponse(data=b"binary", headers={"Content-Disposition": 'attachment; filename="result.png"'}), + "file", + ) + assert named.endswith("result.png") + with open(named, "rb") as f: + assert f.read() == b"binary" + + anonymous = api_client.deserialize(_FakeRestResponse(data=b"binary", headers={}), "file") + with open(anonymous, "rb") as f: + assert f.read() == b"binary" diff --git a/tests/test_configuration_offline.py b/tests/test_configuration_offline.py new file mode 100644 index 0000000..5530028 --- /dev/null +++ b/tests/test_configuration_offline.py @@ -0,0 +1,44 @@ +# coding: utf-8 + +"""Offline coverage for the ``Configuration`` accessors. + +Split out of the former ``test_core_coverage.py`` (mirrors the Java SDK's +``SdkCoreCoverageTest``): exercises the network-free ``Configuration`` accessors — auth +settings, API-key prefixing, debug toggling and the file logger — so the generated SDK +stays above the 80% line-coverage gate regardless of which live API calls run. +""" + +import pathlib + +from aspose_barcode_cloud.configuration import Configuration + + +def test_configuration_offline_accessors(tmp_path: pathlib.Path) -> None: + config = Configuration(access_token="fake-token", client_id="client-id", client_secret="client-secret") + + assert config.access_token == "fake-token" + assert config.token_url + assert "Bearer fake-token" in config.auth_settings()["JWT"]["value"] + assert config.get_basic_auth_token() + assert "Python SDK Debug Report" in config.to_debug_report() + + config.api_key["JWT"] = "secret-key" + config.api_key_prefix["JWT"] = "Bearer" + assert config.get_api_key_with_prefix("JWT") == "Bearer secret-key" + config.api_key_prefix["JWT"] = None + assert config.get_api_key_with_prefix("JWT") == "secret-key" + + config.debug = True + assert config.debug is True + config.debug = False + assert config.debug is False + + assert config.logger_format + config.logger_file = str(tmp_path / "sdk.log") + assert config.logger_file.endswith("sdk.log") + file_handler = config.logger_file_handler + config.logger_file = None + assert config.logger_file is None + # The generated setter never closes the replaced FileHandler; close it here + # so the test does not leak a ResourceWarning under -Werror (make cover). + file_handler.close() diff --git a/tests/test_exception_offline.py b/tests/test_exception_offline.py new file mode 100644 index 0000000..fa38c10 --- /dev/null +++ b/tests/test_exception_offline.py @@ -0,0 +1,36 @@ +# coding: utf-8 + +"""Offline coverage for the ``rest.ApiException`` hierarchy. + +Split out of the former ``test_core_coverage.py`` (mirrors the Java SDK's +``SdkCoreCoverageTest``): builds ``ApiException`` both directly and from a stand-in HTTP +response without touching the network, so the exception plumbing in ``rest.py`` stays +above the 80% line-coverage gate regardless of which live API calls run. + +The live counterpart lives in ``test_exception.py``, which asserts the SDK parses a real +400 error body returned by the API. +""" + +from aspose_barcode_cloud.rest import ApiException + + +class _FakeRestResponse: + """Explicit stand-in for the urllib3 response ``rest.ApiException`` reads from.""" + + def __init__(self) -> None: + self.status = 500 + self.reason = "Server Error" + self.data = b'{"error": "boom"}' + + +def test_rest_api_exception_offline() -> None: + plain = ApiException(status=404, reason="Not Found") + assert plain.status == 404 + assert plain.body is None + assert "404" in str(plain) + assert "HTTP response body" not in str(plain) + + from_response = ApiException(http_resp=_FakeRestResponse()) + assert from_response.status == 500 + assert from_response.body == b'{"error": "boom"}' + assert "HTTP response body" in str(from_response) diff --git a/tests/test_generate_api_offline.py b/tests/test_generate_api_offline.py new file mode 100644 index 0000000..5d16a42 --- /dev/null +++ b/tests/test_generate_api_offline.py @@ -0,0 +1,195 @@ +# coding: utf-8 + +"""Offline request-building coverage for ``GenerateApi``. + +Complements ``test_models_coverage.py`` / ``test_api_client_offline.py``: drives every generated +``GenerateApi`` method through ``ApiClient`` against a mocked ``RESTClientObject`` (fixtures +in ``conftest.py``, the same pattern as ``test_headers.py``), so the request-building code in +``aspose_barcode_cloud/api/generate_api.py`` stays covered without a single live API call. +""" + +import pytest + +from aspose_barcode_cloud.api.generate_api import GenerateApi +from aspose_barcode_cloud.models import ( + BarcodeImageParams, + EncodeBarcodeType, + EncodeDataType, + GenerateParams, + Pdf417Params, + QrParams, +) + +from .model_factories import ( + _barcode_image_params, + _code128_params, + _make_model, + _pdf417_params, + _qr_params, +) + + +def test_generate_offline_builds_get_request(api_client, rest_client): + # type: (ApiClient, mock.Mock) -> None + api = GenerateApi(api_client) + + response = api.generate( + EncodeBarcodeType.QR, + "Sample", + data_type=EncodeDataType.STRINGDATA, + barcode_image_params=_barcode_image_params(1), + qr_params=_qr_params(1), + code128_params=_code128_params(1), + pdf417_params=_pdf417_params(1), + ) + + assert response is rest_client.GET.return_value + assert rest_client.GET.call_count == 1 + url = rest_client.GET.call_args[0][0] + assert url.endswith("/barcode/generate/QR") + + query = dict(rest_client.GET.call_args[1]["query_params"]) + assert query["data"] == "Sample" + assert query["dataType"] == EncodeDataType.STRINGDATA + for flattened_param in ( + "imageFormat", + "textLocation", + "foregroundColor", + "backgroundColor", + "units", + "resolution", + "imageHeight", + "imageWidth", + "rotationAngle", + "qrEncodeMode", + "qrErrorLevel", + "qrVersion", + "qrECIEncoding", + "qrAspectRatio", + "microQRVersion", + "rectMicroQrVersion", + "code128EncodeMode", + "pdf417EncodeMode", + "pdf417ErrorLevel", + "pdf417Truncate", + "pdf417Columns", + "pdf417Rows", + "pdf417AspectRatio", + "pdf417ECIEncoding", + "pdf417IsReaderInitialization", + "pdf417MacroCharacters", + "pdf417IsLinked", + "pdf417IsCode128Emulation", + ): + assert flattened_param in query, flattened_param + + headers = rest_client.GET.call_args[1]["headers"] + assert headers["Authorization"] == "Bearer fake-token" + assert headers["Cookie"] == "session=fake-cookie" + + +def test_generate_offline_async_request(api_client, rest_client): + # type: (ApiClient, mock.Mock) -> None + api = GenerateApi(api_client) + + thread = api.generate_with_http_info(EncodeBarcodeType.CODE128, "Sample", async_req=True) + result, status, headers = thread.get() + + assert result is rest_client.GET.return_value + assert status == 200 + assert headers["Content-Type"] == "application/json" + assert rest_client.GET.call_count == 1 + + +def test_generate_offline_rejects_missing_and_unknown_params(api_client): + # type: (ApiClient) -> None + api = GenerateApi(api_client) + + with pytest.raises(ValueError, match="barcode_type"): + api.generate(None, "Sample") + with pytest.raises(ValueError, match="'data'"): + api.generate(EncodeBarcodeType.QR, None) + with pytest.raises(TypeError, match="bogus"): + api.generate(EncodeBarcodeType.QR, "Sample", bogus="value") + + +def _params_with_raw_attribute(model_type, attribute, value): + # type: (type, str, float) -> object + """Bypass the model setter validation to exercise the API-side range guards.""" + instance = model_type() + setattr(instance, "_" + attribute, value) + return instance + + +@pytest.mark.parametrize( + "group, model_type, attribute, value", + [ + ("barcode_image_params", BarcodeImageParams, "resolution", 100001.0), + ("barcode_image_params", BarcodeImageParams, "resolution", 0.5), + ("qr_params", QrParams, "qr_aspect_ratio", 1.5), + ("qr_params", QrParams, "qr_aspect_ratio", 0.0001), + ("pdf417_params", Pdf417Params, "pdf417_columns", 31), + ("pdf417_params", Pdf417Params, "pdf417_columns", -1), + ("pdf417_params", Pdf417Params, "pdf417_rows", 91), + ("pdf417_params", Pdf417Params, "pdf417_rows", -1), + ("pdf417_params", Pdf417Params, "pdf417_aspect_ratio", 10.5), + ("pdf417_params", Pdf417Params, "pdf417_aspect_ratio", 1.5), + ], +) +def test_generate_offline_rejects_out_of_range_params(api_client, group, model_type, attribute, value): + # type: (ApiClient, str, type, str, float) -> None + api = GenerateApi(api_client) + grouped_params = {group: _params_with_raw_attribute(model_type, attribute, value)} + + with pytest.raises(ValueError, match=attribute): + api.generate(EncodeBarcodeType.QR, "Sample", **grouped_params) + with pytest.raises(ValueError, match=attribute): + api.generate_multipart(EncodeBarcodeType.QR, "Sample", **grouped_params) + + +def test_generate_body_offline_builds_post_request(api_client, rest_client): + # type: (ApiClient, mock.Mock) -> None + api = GenerateApi(api_client) + + response = api.generate_body(_make_model(GenerateParams, 1)) + + assert response is rest_client.POST.return_value + assert rest_client.POST.call_count == 1 + url = rest_client.POST.call_args[0][0] + assert url.endswith("/barcode/generate-body") + body = rest_client.POST.call_args[1]["body"] + assert body["barcodeType"] + assert "encodeData" in body + + with pytest.raises(ValueError, match="generate_params"): + api.generate_body(None) + + +def test_generate_multipart_offline_builds_form_request(api_client, rest_client): + # type: (ApiClient, mock.Mock) -> None + api = GenerateApi(api_client) + + response = api.generate_multipart( + EncodeBarcodeType.QR, + "Sample", + data_type=EncodeDataType.STRINGDATA, + barcode_image_params=_barcode_image_params(1), + qr_params=_qr_params(1), + code128_params=_code128_params(1), + pdf417_params=_pdf417_params(1), + ) + + assert response is rest_client.POST.return_value + assert rest_client.POST.call_count == 1 + url = rest_client.POST.call_args[0][0] + assert url.endswith("/barcode/generate-multipart") + form = dict(rest_client.POST.call_args[1]["post_params"]) + assert form["barcodeType"] == EncodeBarcodeType.QR + assert form["data"] == "Sample" + assert "qrEncodeMode" in form + assert "pdf417EncodeMode" in form + + with pytest.raises(ValueError, match="barcode_type"): + api.generate_multipart(None, "Sample") + with pytest.raises(ValueError, match="'data'"): + api.generate_multipart(EncodeBarcodeType.QR, None) diff --git a/tests/test_models_coverage.py b/tests/test_models_coverage.py new file mode 100644 index 0000000..db8c926 --- /dev/null +++ b/tests/test_models_coverage.py @@ -0,0 +1,44 @@ +# coding: utf-8 + +"""Offline coverage for the generated models and enums. + +Mirrors the Java SDK's ``GeneratedModelCoverageTest``: it exercises the value-object +boilerplate (constructors, property accessors, ``to_dict`` / ``to_str`` / ``__repr__`` / +``__eq__`` / ``__ne__``) without touching the network, so the generated SDK stays above +the 80% line-coverage gate regardless of which live API calls run. +""" + +import pytest + +from .model_factories import ENUM_TYPES, MODEL_TYPES, _enum_value, _make_model + + +@pytest.mark.parametrize("enum_type", ENUM_TYPES) +def test_enum_value_objects(enum_type: type) -> None: + first = enum_type() + second = enum_type() + + assert first == second + assert not first != second + assert first != object() + assert first.to_dict() == {} + assert isinstance(first.to_str(), str) + assert isinstance(repr(first), str) + assert _enum_value(enum_type, 0) + + +@pytest.mark.parametrize("model_type", MODEL_TYPES) +def test_model_value_objects(model_type: type) -> None: + first = _make_model(model_type, 1) + same = _make_model(model_type, 1) + + assert first == same + assert not first != same + assert first != object() + assert repr(first) + + assert isinstance(first.to_dict(), dict) + assert isinstance(first.to_str(), str) + + for attribute in model_type.swagger_types: + assert getattr(first, attribute) is not None diff --git a/tests/test_recognize_api_offline.py b/tests/test_recognize_api_offline.py new file mode 100644 index 0000000..8531a20 --- /dev/null +++ b/tests/test_recognize_api_offline.py @@ -0,0 +1,112 @@ +# coding: utf-8 + +"""Offline request-building coverage for ``RecognizeApi``. + +Complements ``test_models_coverage.py`` / ``test_api_client_offline.py``: drives every generated +``RecognizeApi`` method through ``ApiClient`` against a mocked ``RESTClientObject`` (fixtures +in ``conftest.py``, the same pattern as ``test_headers.py``), so the request-building code in +``aspose_barcode_cloud/api/recognize_api.py`` stays covered without a single live API call. +""" + +import pytest + +from aspose_barcode_cloud.api.recognize_api import RecognizeApi +from aspose_barcode_cloud.models import ( + BarcodeResponseList, + DecodeBarcodeType, + RecognitionImageKind, + RecognitionMode, + RecognizeBase64Request, +) + +from .conftest import FILE_URL + + +def test_recognize_offline_builds_get_request(api_client, rest_client): + # type: (ApiClient, mock.Mock) -> None + api = RecognizeApi(api_client) + + result = api.recognize( + DecodeBarcodeType.QR, + FILE_URL, + recognition_mode=RecognitionMode.NORMAL, + recognition_image_kind=RecognitionImageKind.PHOTO, + ) + + assert isinstance(result, BarcodeResponseList) + assert result.barcodes[0].barcode_value == "Test" + assert rest_client.GET.call_count == 1 + query = dict(rest_client.GET.call_args[1]["query_params"]) + assert query["barcodeType"] == DecodeBarcodeType.QR + assert query["fileUrl"] == FILE_URL + assert query["recognitionMode"] == RecognitionMode.NORMAL + assert query["recognitionImageKind"] == RecognitionImageKind.PHOTO + + with pytest.raises(ValueError, match="barcode_type"): + api.recognize(None, FILE_URL) + with pytest.raises(ValueError, match="file_url"): + api.recognize(DecodeBarcodeType.QR, None) + with pytest.raises(TypeError, match="bogus"): + api.recognize(DecodeBarcodeType.QR, FILE_URL, bogus="value") + + +def test_recognize_offline_with_http_info_returns_status_and_headers(api_client): + # type: (ApiClient) -> None + api = RecognizeApi(api_client) + + result, status, headers = api.recognize_with_http_info(DecodeBarcodeType.QR, FILE_URL, _return_http_data_only=False) + + assert isinstance(result, BarcodeResponseList) + assert status == 200 + assert headers["Content-Type"] == "application/json" + + +def test_recognize_base64_offline_builds_post_request(api_client, rest_client): + # type: (ApiClient, mock.Mock) -> None + api = RecognizeApi(api_client) + request = RecognizeBase64Request( + barcode_types=[DecodeBarcodeType.QR], + file_base64="QVFJRA==", + recognition_mode=RecognitionMode.FAST, + recognition_image_kind=RecognitionImageKind.CLEARIMAGE, + ) + + result = api.recognize_base64(request) + + assert isinstance(result, BarcodeResponseList) + assert rest_client.POST.call_count == 1 + url = rest_client.POST.call_args[0][0] + assert url.endswith("/barcode/recognize-body") + body = rest_client.POST.call_args[1]["body"] + assert body["fileBase64"] == "QVFJRA==" + + with pytest.raises(ValueError, match="recognize_base64_request"): + api.recognize_base64(None) + + +def test_recognize_multipart_offline_builds_form_request(api_client, rest_client): + # type: (ApiClient, mock.Mock) -> None + api = RecognizeApi(api_client) + + result = api.recognize_multipart( + DecodeBarcodeType.QR, + b"fake-image-bytes", + recognition_mode=RecognitionMode.EXCELLENT, + recognition_image_kind=RecognitionImageKind.SCANNEDDOCUMENT, + ) + + assert isinstance(result, BarcodeResponseList) + assert rest_client.POST.call_count == 1 + url = rest_client.POST.call_args[0][0] + assert url.endswith("/barcode/recognize-multipart") + form = dict(rest_client.POST.call_args[1]["post_params"]) + assert form["barcodeType"] == DecodeBarcodeType.QR + # sanitize_for_serialization turns the FileFieldData namedtuple into a plain tuple + file_name, file_bytes, mime_type = form["file"] + assert file_bytes == b"fake-image-bytes" + assert mime_type == "application/octet-stream" + + with pytest.raises(ValueError, match="barcode_type"): + api.recognize_multipart(None, b"fake-image-bytes") + with pytest.raises(ValueError, match="'file'"): + api.recognize_multipart(DecodeBarcodeType.QR, None) diff --git a/tests/test_scan_api_offline.py b/tests/test_scan_api_offline.py new file mode 100644 index 0000000..19d6cbe --- /dev/null +++ b/tests/test_scan_api_offline.py @@ -0,0 +1,68 @@ +# coding: utf-8 + +"""Offline request-building coverage for ``ScanApi``. + +Complements ``test_models_coverage.py`` / ``test_api_client_offline.py``: drives every generated +``ScanApi`` method through ``ApiClient`` against a mocked ``RESTClientObject`` (fixtures in +``conftest.py``, the same pattern as ``test_headers.py``), so the request-building code in +``aspose_barcode_cloud/api/scan_api.py`` stays covered without a single live API call. +""" + +import pytest + +from aspose_barcode_cloud.api.scan_api import ScanApi +from aspose_barcode_cloud.models import BarcodeResponseList, ScanBase64Request + +from .conftest import FILE_URL + + +def test_scan_offline_builds_get_request(api_client, rest_client): + # type: (ApiClient, mock.Mock) -> None + api = ScanApi(api_client) + + result = api.scan(FILE_URL) + + assert isinstance(result, BarcodeResponseList) + assert rest_client.GET.call_count == 1 + query = dict(rest_client.GET.call_args[1]["query_params"]) + assert query["fileUrl"] == FILE_URL + + with pytest.raises(ValueError, match="file_url"): + api.scan(None) + with pytest.raises(TypeError, match="bogus"): + api.scan(FILE_URL, bogus="value") + + +def test_scan_base64_offline_builds_post_request(api_client, rest_client): + # type: (ApiClient, mock.Mock) -> None + api = ScanApi(api_client) + + result = api.scan_base64(ScanBase64Request(file_base64="QVFJRA==")) + + assert isinstance(result, BarcodeResponseList) + assert rest_client.POST.call_count == 1 + url = rest_client.POST.call_args[0][0] + assert url.endswith("/barcode/scan-body") + body = rest_client.POST.call_args[1]["body"] + assert body["fileBase64"] == "QVFJRA==" + + with pytest.raises(ValueError, match="scan_base64_request"): + api.scan_base64(None) + + +def test_scan_multipart_offline_builds_form_request(api_client, rest_client): + # type: (ApiClient, mock.Mock) -> None + api = ScanApi(api_client) + + result = api.scan_multipart(b"fake-image-bytes") + + assert isinstance(result, BarcodeResponseList) + assert rest_client.POST.call_count == 1 + url = rest_client.POST.call_args[0][0] + assert url.endswith("/barcode/scan-multipart") + form = dict(rest_client.POST.call_args[1]["post_params"]) + file_name, file_bytes, mime_type = form["file"] + assert file_bytes == b"fake-image-bytes" + + with pytest.raises(ValueError, match="'file'"): + api.scan_multipart(None)