Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-awscrt for openSUSE:Factory checked in at 2026-11-02 16:40:48 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-awscrt (Old) and /work/SRC/openSUSE:Factory/.python-awscrt.new.1255 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-awscrt" Mon Nov 2 16:40:48 2026 rev:13 rq:1394460 version:0.38.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-awscrt/python-awscrt.changes 2026-10-14 18:34:59.041524534 +0200 +++ /work/SRC/openSUSE:Factory/.python-awscrt.new.1255/python-awscrt.changes 2026-11-02 16:41:32.674191808 +0100 @@ -1,0 +2,11 @@ +Sat Oct 31 15:01:56 UTC 2026 - John Paul Adrian Glaubitz <[email protected]> + +- Update to version 0.38.0 + * Python 3.15 by @sbSteveK in (#768) + * bind out expected checksum by @DmitriyMusatkin in (#770) + * feat: add support for windows-arm64 by @azkrishpy in (#767) + * Add eventual consistency to fix mqtt test failures + by @xiazhvera in (#771) +- Refresh skip-test-requiring-network.patch + +------------------------------------------------------------------- Old: ---- awscrt-0.37.0.tar.gz New: ---- awscrt-0.38.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-awscrt.spec ++++++ --- /var/tmp/diff_new_pack.y2aPFv/_old 2026-11-02 16:41:33.421222973 +0100 +++ /var/tmp/diff_new_pack.y2aPFv/_new 2026-11-02 16:41:33.423223056 +0100 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-awscrt -Version: 0.37.0 +Version: 0.38.0 Release: 0 Summary: A common runtime for AWS Python projects License: Apache-2.0 ++++++ awscrt-0.37.0.tar.gz -> awscrt-0.38.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-crt-python-0.37.0/.github/workflows/ci.yml new/aws-crt-python-0.38.0/.github/workflows/ci.yml --- old/aws-crt-python-0.37.0/.github/workflows/ci.yml 2026-09-23 09:04:30.000000000 +0200 +++ new/aws-crt-python-0.38.0/.github/workflows/ci.yml 2026-10-16 16:15:10.000000000 +0200 @@ -7,7 +7,7 @@ - 'docs' env: - BUILDER_VERSION: v0.9.96 + BUILDER_VERSION: latest BUILDER_SOURCE: releases BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net PACKAGE_NAME: aws-crt-python @@ -57,6 +57,8 @@ - cp313-cp313 - cp314-cp314 - cp314-cp314t + - cp315-cp315 + - cp315-cp315t permissions: id-token: write # This is required for requesting the JWT steps: @@ -83,6 +85,8 @@ - cp313-cp313 - cp314-cp314 - cp314-cp314t + - cp315-cp315 + - cp315-cp315t permissions: id-token: write # This is required for requesting the JWT steps: @@ -308,6 +312,58 @@ python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" python builder.pyz build -p ${{ env.PACKAGE_NAME }} --python "${{ steps.python38.outputs.python-path }}" + windows-315: + # Python 3.15 (standard and free-threaded). Free-threaded exercises the + # abi3t build path (Py_TARGET_ABI3T / PyModExport, see setup.py). + runs-on: windows-2022 + strategy: + fail-fast: false + matrix: + freethreaded: [false, true] + permissions: + id-token: write # This is required for requesting the JWT + steps: + - uses: actions/setup-python@v5 + id: python315 + with: + python-version: '3.15' + allow-prereleases: true + freethreaded: ${{ matrix.freethreaded }} + - uses: ilammy/setup-nasm@v1 + - name: configure AWS credentials (containers) + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.CRT_CI_ROLE }} + aws-region: ${{ env.AWS_DEFAULT_REGION }} + - name: Build ${{ env.PACKAGE_NAME }} + consumers + run: | + python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" + python builder.pyz build -p ${{ env.PACKAGE_NAME }} --python "${{ steps.python315.outputs.python-path }}" + + windows-arm64: + runs-on: windows-11-arm + strategy: + fail-fast: false + matrix: + python: ['3.11', '3.14', '3.15'] + permissions: + id-token: write # This is required for requesting the JWT + steps: + - uses: actions/setup-python@v5 + id: python + with: + python-version: ${{ matrix.python }} + architecture: arm64 + - name: configure AWS credentials (containers) + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.CRT_CI_ROLE }} + aws-region: ${{ env.AWS_DEFAULT_REGION }} + - name: Build ${{ env.PACKAGE_NAME }} + consumers + run: | + python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" + python builder.pyz build -p ${{ env.PACKAGE_NAME }} --python "${{ steps.python.outputs.python-path }}" + macos: runs-on: macos-14 # latest permissions: @@ -324,6 +380,34 @@ chmod a+x builder ./builder build -p ${{ env.PACKAGE_NAME }} + macos-315: + # Python 3.15 (standard and free-threaded). Free-threaded exercises the + # abi3t build path (Py_TARGET_ABI3T / PyModExport, see setup.py). + runs-on: macos-14 # latest + strategy: + fail-fast: false + matrix: + freethreaded: [false, true] + permissions: + id-token: write # This is required for requesting the JWT + steps: + - uses: actions/setup-python@v5 + id: python315 + with: + python-version: '3.15' + allow-prereleases: true + freethreaded: ${{ matrix.freethreaded }} + - name: configure AWS credentials (containers) + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.CRT_CI_ROLE }} + aws-region: ${{ env.AWS_DEFAULT_REGION }} + - name: Build ${{ env.PACKAGE_NAME }} + consumers + run: | + python3 -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder')" + chmod a+x builder + ./builder build -p ${{ env.PACKAGE_NAME }} --python "${{ steps.python315.outputs.python-path }}" + macos-x64: runs-on: macos-14-large # latest permissions: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-crt-python-0.37.0/awscrt/s3.py new/aws-crt-python-0.38.0/awscrt/s3.py --- old/aws-crt-python-0.37.0/awscrt/s3.py 2026-09-23 09:04:30.000000000 +0200 +++ new/aws-crt-python-0.38.0/awscrt/s3.py 2026-10-16 16:15:10.000000000 +0200 @@ -137,6 +137,41 @@ """ +class S3ChecksumValidationMode(IntEnum): + """ + Which checksums a download is validated against. + + A download is made of one or more part responses, each of which may carry a checksum + of its own body, and the whole download may also be covered by a single checksum + (the object's, or one supplied via :attr:`S3ChecksumConfig.expected_checksum`). + These modes pick which of the two the client checks. + + Only applies to S3RequestType.GET_OBJECT. + """ + + DEFAULT = 0 + """Default behavior, currently the same as FULL_OBJECT.""" + + REQUEST_ONLY = 1 + """ + Validate each response against the checksum that same response reports, and nothing else. + When a download is split into parts, each part is validated on its own and nothing spanning + multiple parts is checked, which reduces durability guarantees. + The client makes no attempt to discover a full-object checksum. + Cannot be combined with :attr:`S3ChecksumConfig.expected_checksum`. + """ + + FULL_OBJECT = 2 + """ + Validate the whole download against a single checksum covering it, on top of validating + each response against its own checksum. + The client uses :attr:`S3ChecksumConfig.expected_checksum` if given, and otherwise discovers + a checksum from the service (which may cost a HeadObject request). This is best effort: + when the service has no checksum covering the requested bytes, the download still succeeds, + with `did_validate_checksum` False. + """ + + @dataclass class S3ChecksumConfig: """Configures how the S3Client calculates and verifies checksums.""" @@ -151,7 +186,43 @@ """Where to put the request checksum.""" validate_response: bool = False - """Whether to retrieve and validate response checksums.""" + """ + Whether to retrieve and validate response checksums. + Which checksums are checked is controlled by `response_validation_mode`. + """ + + expected_checksum: Optional[str] = None + """ + The checksum you expect of the data this request returns, base64-encoded + as S3 reports checksums (e.g. "NSRBwg=="). + + It covers exactly the bytes the request asks for: the whole object for a plain + GetObject, or just the requested bytes if the request has a Range header or a partNumber. + + When set, the client validates the downloaded data against this value instead of a + checksum reported by the service. This skips the HeadObject request that `validate_response` + may otherwise make, and covers cases where the service has no checksum for the requested + bytes (a ranged download, or an object uploaded via multipart upload with a composite checksum). + + Setting this is by itself a request to validate, whether or not `validate_response` is set. + The request fails with AWS_ERROR_S3_RESPONSE_CHECKSUM_MISMATCH if the data does not match. + + If you set this, you must also set `expected_checksum_algorithm`. + Only applies to downloads (S3RequestType.GET_OBJECT, or S3RequestType.DEFAULT with + operation_name "GetObject"). Cannot be combined with + `response_validation_mode` = S3ChecksumValidationMode.REQUEST_ONLY. + """ + + expected_checksum_algorithm: Optional[S3ChecksumAlgorithm] = None + """The algorithm of `expected_checksum`. Must be set if `expected_checksum` is set, and vice versa.""" + + response_validation_mode: Optional[S3ChecksumValidationMode] = None + """ + Which checksums a download is validated against. See :class:`S3ChecksumValidationMode`. + This only refines validation already requested through `validate_response` or + `expected_checksum`; it does not turn validation on by itself. + Only applies to S3RequestType.GET_OBJECT. + """ @dataclass @@ -323,6 +394,9 @@ retry_config (Optional[S3RetryConfig]): Configuration for the retry strategy. See :class:`S3RetryConfig` for details. If not set, S3 client defaults are used. + + connect_timeout_ms (Optional[int]): Timeout, in milliseconds, for establishing a connection. + If not set, the S3 client default is used. """ __slots__ = ('shutdown_event', '_region') @@ -344,7 +418,8 @@ network_interface_names: Optional[Sequence[str]] = None, fio_options: Optional['S3FileIoOptions'] = None, max_active_connections_override: Optional[int] = None, - retry_config: Optional[S3RetryConfig] = None): + retry_config: Optional[S3RetryConfig] = None, + connect_timeout_ms: Optional[int] = None): assert isinstance(bootstrap, ClientBootstrap) or bootstrap is None assert isinstance(region, str) assert isinstance(signing_config, AwsSigningConfig) or signing_config is None @@ -360,6 +435,7 @@ assert isinstance(network_interface_names, Sequence) or network_interface_names is None assert isinstance(fio_options, S3FileIoOptions) or fio_options is None assert isinstance(max_active_connections_override, int) or max_active_connections_override is None + assert isinstance(connect_timeout_ms, int) or connect_timeout_ms is None if credential_provider and signing_config: raise ValueError("'credential_provider' has been deprecated in favor of 'signing_config'. " @@ -401,6 +477,8 @@ network_interface_names = list(network_interface_names) if max_active_connections_override is None: max_active_connections_override = 0 + if connect_timeout_ms is None: + connect_timeout_ms = 0 # Retry config: 0 means "use S3 client defaults" if retry_config is None: @@ -440,7 +518,8 @@ retry_config.backoff_scale_factor_ms, retry_config.max_backoff_secs, int(retry_config.jitter_mode), - retry_config.initial_bucket_capacity) + retry_config.initial_bucket_capacity, + connect_timeout_ms) def make_request( self, @@ -704,12 +783,20 @@ checksum_algorithm = 0 checksum_location = 0 validate_response_checksum = False + expected_checksum = None + expected_checksum_algorithm = 0 + response_validation_mode = 0 if checksum_config is not None: if checksum_config.algorithm is not None: checksum_algorithm = checksum_config.algorithm.value if checksum_config.location is not None: checksum_location = checksum_config.location.value validate_response_checksum = checksum_config.validate_response + expected_checksum = checksum_config.expected_checksum + if checksum_config.expected_checksum_algorithm is not None: + expected_checksum_algorithm = checksum_config.expected_checksum_algorithm.value + if checksum_config.response_validation_mode is not None: + response_validation_mode = checksum_config.response_validation_mode.value fio_options_set = False should_stream = False disk_throughput_gbps = 0.0 @@ -747,6 +834,9 @@ checksum_algorithm, checksum_location, validate_response_checksum, + expected_checksum, + expected_checksum_algorithm, + response_validation_mode, part_size, multipart_upload_threshold, fio_options_set, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-crt-python-0.37.0/setup.py new/aws-crt-python-0.38.0/setup.py --- old/aws-crt-python-0.37.0/setup.py 2026-09-23 09:04:30.000000000 +0200 +++ new/aws-crt-python-0.38.0/setup.py 2026-10-16 16:15:10.000000000 +0200 @@ -35,6 +35,23 @@ # Regex to match a Windows SDK version directory name in the format of major.minor.build.revision SDK_VERSION_RE = re.compile(r'^(\d+)\.(\d+)\.(\d+)\.(\d+)$') +# On Windows ARM64 we build with clang-cl, which accepts MSVC-only flags and then +# ignores them, warning once per translation unit. We get /MP from the CRT's CMake +# and /GL from distutils. Those are two independent compiler invocations that share +# no configuration, so these have to be passed to each of them separately. +# +# Keep this a list, one flag per entry: extra_compile_args entries each become a +# single argv element, so "-Wa -Wb" in one entry reaches clang-cl as one unknown +# option and is silently ignored. +# +# Note these only reach the CRT build via CMAKE_C_FLAGS, which CMake emits *before* +# a target's own options. So a flag here cannot turn off anything the CRT's /W4 +# re-enables (i.e. anything in -Wall or -Wextra) -- that has to be fixed in +# aws-c-common's AwsCFlags.cmake instead. +CLANG_CL_IGNORED_FLAG_WARNINGS = [ + '-Wno-unused-command-line-argument', +] + def parse_version(version_string): return tuple(int(x) for x in version_string.split(".")) @@ -48,6 +65,10 @@ return is_64bit() == False +def is_windows_arm64(): + return sys.platform == 'win32' and sysconfig.get_platform() == 'win-arm64' + + # TODO: Fix this. Since adding pyproject.toml, it always returns False def is_development_mode(): """Return whether we're building in Development Mode (a.k.a. “Editable Installs”). @@ -196,6 +217,26 @@ print('Using Visual Studio', vs_version, vs_year) + if is_windows_arm64(): + if not shutil.which('clang-cl'): + raise RuntimeError('clang-cl is required to build for Windows ARM64') + print('Using Ninja and clang-cl for Windows ARM64') + return [ + '-G', 'Ninja', + f'-DCMAKE_SYSTEM_VERSION={windows_sdk_version}', + '-DCMAKE_C_COMPILER=clang-cl', + '-DCMAKE_CXX_COMPILER=clang-cl', + '-DCMAKE_ASM_COMPILER=clang-cl', + # Silences /MP, which the CRT's CMake adds. This lands ahead of the + # /W4 that aws_set_common_properties() adds, which is fine: /W4 maps + # to -Wall -Wextra, and neither group includes this warning, so they + # do not turn it back on. + # Appends to, rather than replaces, what CMake would have taken from + # the CFLAGS env var -- an explicit -D on the command line wins over + # that, and the extension half already honors CFLAGS (see awscrt_ext). + '-DCMAKE_C_FLAGS=' + ' '.join([os.environ.get('CFLAGS', '')] + CLANG_CL_IGNORED_FLAG_WARNINGS).strip(), + ] + if vs_year <= 2017: # For VS2017 and earlier, architecture goes at end of generator string if is_64bit(): @@ -325,6 +366,54 @@ class awscrt_build_ext(setuptools.command.build_ext.build_ext): + def build_extensions(self): + if is_windows_arm64(): + if not self.compiler.initialized: + self.compiler.initialize() + clang_cl = shutil.which('clang-cl') + if not clang_cl: + raise RuntimeError('clang-cl is required to build for Windows ARM64') + self.compiler.cc = clang_cl + super().build_extensions() + + def get_ext_filename(self, ext_name): + filename = super().get_ext_filename(ext_name) + if FREE_THREADED_BUILD and sys.version_info[:2] >= (3, 15): + # setuptools' build_ext (which this class extends) names extensions with the + # interpreter's EXT_SUFFIX, e.g. "_awscrt.cpython-315t-x86_64-linux-gnu.so" + # -- a name only a 3.15t interpreter will import. This abi3t (free-threaded + # stable ABI) build produces one binary for ALL 3.15+ free-threaded + # interpreters, so swap in ".abi3t.so", the version-agnostic suffix the + # import system accepts. setuptools does this automatically for classic abi3 + # ("_awscrt.abi3.so") but doesn't know abi3t yet. Dropping the arch/OS part + # is safe: platform selection happens via the wheel's platform tag + # (e.g. manylinux2014_x86_64), never via the .so filename. Windows needs no + # rename (abi3t modules keep the plain ".pyd" name); the ".so" check skips it. + # + # We can remove this override once setuptools supports abi3t natively (tracked + # in pypa/setuptools#5205; the check makes it a harmless no-op if setuptools + # starts emitting ".abi3t.so" itself). + ext_suffix = sysconfig.get_config_var('EXT_SUFFIX') + if ext_suffix.endswith('.so') and filename.endswith(ext_suffix): + filename = filename[:-len(ext_suffix)] + '.abi3t.so' + return filename + + def get_export_symbols(self, ext): + if FREE_THREADED_BUILD and sys.version_info[:2] >= (3, 15): + # On Windows, setuptools tells the linker to export "PyInit_<name>" + # (/EXPORT:PyInit__awscrt), but abi3t modules define "PyModExport_<name>" + # instead (PEP 793, see source/module.c) -- PyInit doesn't exist in + # that build, so the link fails with an unresolved external + # (pypa/distutils#387). The /EXPORT flag is redundant anyway: + # PyMODEXPORT_FUNC already declares dllexport, which puts the hook + # in the DLL's export table. So suppress the export list entirely. + # No-op on non-Windows (the list is already empty there). + # + # Like get_ext_filename above, remove this override once + # setuptools supports abi3t natively (pypa/setuptools#5205). + return [] + return super().get_export_symbols(ext) + def _build_dependencies_impl(self, build_dir, install_path, osx_arch=None): cmake = get_cmake_path() @@ -505,9 +594,19 @@ class bdist_wheel_abi3(bdist_wheel): def get_tag(self): python, abi, plat = super().get_tag() - # on CPython, our wheels are abi3 and compatible back to 3.11 + # Rewrite the wheel's compatibility tags to match what awscrt_ext() + # actually compiled against -- pip trusts these tags to pick a wheel, + # so each branch below MUST mirror a compile-flag branch there. if FREE_THREADED_BUILD: - # free-threaded builds don't use limited API, so skip abi3 tag + if python.startswith("cp") and sys.version_info >= (3, 15): + # Built against abi3t (see awscrt_ext), the stable ABI for + # free-threaded builds. The bare "abi3t" tag is only accepted + # by free-threaded interpreters. If we ever want ONE wheel for + # all 3.15+ interpreters (free-threaded and not), we can promote + # this tag to "abi3.abi3t" -- cp313-abi3 would then serve 3.13/3.14 only. + return "cp315", "abi3t", plat + # 3.13/3.14 free-threaded builds don't support limited API or + # abi3t, so no stable-ABI tag (version-specific wheel) return python, abi, plat elif python.startswith("cp") and sys.version_info >= (3, 13): # 3.13 deprecates PyWeakref_GetObject(), adds alternative @@ -540,6 +639,12 @@ extra_compile_args += ['/Z7'] extra_link_args += ['/DEBUG'] + if is_windows_arm64(): + # Silences /GL, which distutils hardcodes into its MSVC defaults + # ("/O2 /W3 /GL /DNDEBUG /MD"). This is a separate compiler invocation + # from the CMake one above, so it needs the flag of its own. + extra_compile_args += CLANG_CL_IGNORED_FLAG_WARNINGS + elif sys.platform == 'darwin': extra_link_args += ['-framework', 'Security'] @@ -613,9 +718,20 @@ extra_link_args += ['-Wl,--fatal-warnings'] # prefer building with stable ABI, so a wheel can work with multiple major versions - if FREE_THREADED_BUILD and sys.version_info[:2] <= (3, 14): - # 3.14 free threaded (aka no gil) does not support limited api. - # disable it for now. 3.15 promises to support limited api + free threading combo + if FREE_THREADED_BUILD and sys.version_info[:2] >= (3, 15): + # 3.15 introduces abi3t: the stable ABI for free-threaded builds. + # Py_LIMITED_API is not supported on free-threaded builds as of 3.15; + # Py_TARGET_ABI3T is the free-threaded equivalent. It requires the + # PyModExport_* module export hook (see source/module.c). + # https://docs.python.org/3.15/howto/abi3t-migration.html + define_macros.append(('Py_TARGET_ABI3T', '0x030F0000')) + # setuptools' py_limited_api machinery is abi3-only; the abi3t wheel + # tag and extension filename are handled manually in bdist_wheel_abi3 + # and awscrt_build_ext.get_ext_filename. + py_limited_api = False + elif FREE_THREADED_BUILD: + # 3.13/3.14 free threaded (aka no gil) support neither limited api + # nor abi3t. Build version-specific wheels (cp313t/cp314t). py_limited_api = False elif sys.version_info >= (3, 13): # 3.13 deprecates PyWeakref_GetObject(), adds alternative diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-crt-python-0.37.0/source/module.c new/aws-crt-python-0.38.0/source/module.c --- old/aws-crt-python-0.37.0/source/module.c 2026-09-23 09:04:30.000000000 +0200 +++ new/aws-crt-python-0.38.0/source/module.c 2026-10-16 16:15:10.000000000 +0200 @@ -1175,27 +1175,28 @@ * Module Init ******************************************************************************/ -PyMODINIT_FUNC PyInit__awscrt(void) { - static struct PyModuleDef s_module_def = { - PyModuleDef_HEAD_INIT, - s_module_name, - s_module_doc, - -1, /* size of per-interpreter state of the module, or -1 if the module keeps state in global variables. */ - s_module_methods, - NULL, /* slots for multi-phase initialization */ - NULL, /* traversal fn to call during GC traversal */ - NULL, /* clear fn to call during GC clear */ - NULL, /* fn to call during deallocation of the module object */ - }; - - PyObject *m = PyModule_Create(&s_module_def); - if (!m) { - return NULL; +/** + * One-time process-wide initialization, shared by both module init paths below. + * + * WARNING: everything this touches is process-global state (the allocator, + * crash handler, aws-c-* library init, error tables). It must run at most once + * per process. The single-phase PyInit path below guarantees this naturally. + * The abi3t multi-phase path can re-run exec if the module is removed from + * sys.modules and re-imported. This function guards itself with a static + * flag. Python's import machinery serializes extension-module exec, so the plain + * static is not a data race. + * + * Returns 0 on success, -1 (with a Python exception set) on failure, + * matching the Py_mod_exec slot contract. + */ +static int s_module_exec(PyObject *module) { + (void)module; + + static bool s_module_initialized = false; + if (s_module_initialized) { + return 0; } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif + s_module_initialized = true; s_init_allocator(); @@ -1224,9 +1225,85 @@ aws_register_error_info(&s_error_list); s_error_map_init(); + return 0; +} + +#ifdef Py_TARGET_ABI3T + +/* + * Module export hook (PEP 793) for abi3t builds: the stable ABI for + * free-threaded Python, introduced in CPython 3.15. Py_TARGET_ABI3T is + * defined by setup.py (awscrt_ext) for free-threaded 3.15+ builds. abi3t + * removes PyModuleDef-based single-phase init, so the module is described by + * a static PySlot array instead. Notes on each slot: + * + * - Py_mod_gil = Py_MOD_GIL_NOT_USED replaces the PyUnstable_Module_SetGIL() + * call used on the non-abi3t path (PyUnstable_* is not in any stable ABI). + * - Py_mod_multiple_interpreters = NOT_SUPPORTED is REQUIRED for correctness: + * _awscrt keeps process-global state (see s_module_exec), so subinterpreter + * imports must be refused by the interpreter rather than corrupting that + * state with a second exec. + * - No Py_mod_state_size slot: the module keeps state in globals (the + * equivalent of m_size = -1 in the legacy PyModuleDef). + * + * https://docs.python.org/3.15/howto/abi3t-migration.html + */ + +PyABIInfo_VAR(s_abi_info); + +static PySlot s_module_slots[] = { + PySlot_STATIC_DATA(Py_mod_abi, &s_abi_info), + /* PySlot.sl_ptr is a plain `void *`. These two are read-only strings, so + * casting away const is safe: CPython never writes through the slot. */ + PySlot_STATIC_DATA(Py_mod_name, (void *)s_module_name), + PySlot_STATIC_DATA(Py_mod_doc, (void *)s_module_doc), + PySlot_STATIC_DATA(Py_mod_methods, s_module_methods), + PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED), + PySlot_DATA(Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED), + PySlot_FUNC(Py_mod_exec, s_module_exec), + PySlot_END, +}; + +PyMODEXPORT_FUNC PyModExport__awscrt(void) { + /* Must ONLY return a pointer to static data; all runtime initialization + * happens later in the Py_mod_exec slot (s_module_exec). */ + return s_module_slots; +} + +#else /* !Py_TARGET_ABI3T */ + +PyMODINIT_FUNC PyInit__awscrt(void) { + static struct PyModuleDef s_module_def = { + PyModuleDef_HEAD_INIT, + s_module_name, + s_module_doc, + -1, /* size of per-interpreter state of the module, or -1 if the module keeps state in global variables. */ + s_module_methods, + NULL, /* slots for multi-phase initialization */ + NULL, /* traversal fn to call during GC traversal */ + NULL, /* clear fn to call during GC clear */ + NULL, /* fn to call during deallocation of the module object */ + }; + + PyObject *m = PyModule_Create(&s_module_def); + if (!m) { + return NULL; + } + +# ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +# endif + + if (s_module_exec(m) != 0) { + Py_DECREF(m); + return NULL; + } + return m; } +#endif /* Py_TARGET_ABI3T */ + /** * align with the the vanilla C types Python tends to use. * This is important when passing arguments between C and Python diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-crt-python-0.37.0/source/s3_client.c new/aws-crt-python-0.38.0/source/s3_client.c --- old/aws-crt-python-0.37.0/source/s3_client.c 2026-09-23 09:04:30.000000000 +0200 +++ new/aws-crt-python-0.38.0/source/s3_client.c 2026-10-16 16:15:10.000000000 +0200 @@ -270,10 +270,11 @@ uint64_t retry_max_backoff_secs; /* K */ int retry_jitter_mode; /* i */ uint64_t retry_initial_bucket_capacity; /* K */ + uint32_t connect_timeout_ms; /* I */ if (!PyArg_ParseTuple( args, - "OOOOOs#iKKdpKOppdpKOKKKiK", + "OOOOOs#iKKdpKOppdpKOKKKiKI", &bootstrap_py, &signing_config_py, &credential_provider_py, @@ -298,7 +299,8 @@ &retry_backoff_scale_factor_ms, &retry_max_backoff_secs, &retry_jitter_mode, - &retry_initial_bucket_capacity)) { + &retry_initial_bucket_capacity, + &connect_timeout_ms)) { return NULL; } @@ -423,6 +425,7 @@ /* If fio options not set, let native code to decide the default instead */ .fio_opts = fio_options_set ? &fio_opts : NULL, .max_active_connections_override = max_active_connections_override, + .connect_timeout_ms = connect_timeout_ms, }; s3_config.retry_config.max_retries = (size_t)retry_max_retries; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-crt-python-0.37.0/source/s3_meta_request.c new/aws-crt-python-0.38.0/source/s3_meta_request.c --- old/aws-crt-python-0.37.0/source/s3_meta_request.c 2026-09-23 09:04:30.000000000 +0200 +++ new/aws-crt-python-0.38.0/source/s3_meta_request.c 2026-10-16 16:15:10.000000000 +0200 @@ -317,30 +317,33 @@ struct aws_allocator *allocator = aws_py_get_allocator(); - PyObject *py_s3_request; /* O */ - PyObject *s3_client_py; /* O */ - PyObject *http_request_py; /* O */ - int type; /* i */ - const char *operation_name; /* z */ - PyObject *signing_config_py; /* O */ - PyObject *credential_provider_py; /* O */ - const char *recv_filepath; /* z */ - const char *send_filepath; /* z */ - struct aws_byte_cursor region; /* s# */ - enum aws_s3_checksum_algorithm checksum_algorithm; /* i */ - enum aws_s3_checksum_location checksum_location; /* i */ - int validate_response_checksum; /* p - boolean predicate */ - uint64_t part_size; /* K */ - uint64_t multipart_upload_threshold; /* K */ - int fio_options_set; /* p - boolean predicate */ - int should_stream; /* p - boolean predicate */ - double disk_throughput_gbps; /* d */ - int direct_io; /* p - boolean predicate */ - uint64_t max_active_connections_override; /* K */ - PyObject *py_core; /* O */ + PyObject *py_s3_request; /* O */ + PyObject *s3_client_py; /* O */ + PyObject *http_request_py; /* O */ + int type; /* i */ + const char *operation_name; /* z */ + PyObject *signing_config_py; /* O */ + PyObject *credential_provider_py; /* O */ + const char *recv_filepath; /* z */ + const char *send_filepath; /* z */ + struct aws_byte_cursor region; /* s# */ + enum aws_s3_checksum_algorithm checksum_algorithm; /* i */ + enum aws_s3_checksum_location checksum_location; /* i */ + int validate_response_checksum; /* p - boolean predicate */ + struct aws_byte_cursor expected_checksum; /* z# */ + enum aws_s3_checksum_algorithm expected_checksum_algorithm; /* i */ + enum aws_s3_checksum_validation_mode response_checksum_validation_mode; /* i */ + uint64_t part_size; /* K */ + uint64_t multipart_upload_threshold; /* K */ + int fio_options_set; /* p - boolean predicate */ + int should_stream; /* p - boolean predicate */ + double disk_throughput_gbps; /* d */ + int direct_io; /* p - boolean predicate */ + uint64_t max_active_connections_override; /* K */ + PyObject *py_core; /* O */ if (!PyArg_ParseTuple( args, - "OOOizOOzzs#iipKKppdpKO", + "OOOizOOzzs#iipz#iiKKppdpKO", &py_s3_request, &s3_client_py, &http_request_py, @@ -355,6 +358,10 @@ &checksum_algorithm, &checksum_location, &validate_response_checksum, + &expected_checksum.ptr, + &expected_checksum.len, + &expected_checksum_algorithm, + &response_checksum_validation_mode, &part_size, &multipart_upload_threshold, &fio_options_set, @@ -402,6 +409,9 @@ .checksum_algorithm = checksum_algorithm, .location = checksum_location, .validate_response_checksum = validate_response_checksum != 0, + .expected_checksum = expected_checksum, + .expected_checksum_algorithm = expected_checksum_algorithm, + .response_checksum_validation_mode = response_checksum_validation_mode, }; struct s3_meta_request_binding *meta_request = aws_mem_calloc(allocator, 1, sizeof(struct s3_meta_request_binding)); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-crt-python-0.37.0/test/test_aiohttp_client.py new/aws-crt-python-0.38.0/test/test_aiohttp_client.py --- old/aws-crt-python-0.37.0/test/test_aiohttp_client.py 2026-09-23 09:04:30.000000000 +0200 +++ new/aws-crt-python-0.38.0/test/test_aiohttp_client.py 2026-10-16 16:15:10.000000000 +0200 @@ -916,37 +916,20 @@ asyncio.run(self._test_h2_stream_flow_control_blocks_and_resumes()) -class AIOHttp2RemoteEndStreamTest(NativeResourceTest): [email protected](os.environ.get('AWS_TEST_LOCALHOST'), 'set env var to run test: AWS_TEST_LOCALHOST') +class AIOHttp2RemoteEndStreamTest(AsyncMockServerTestBase): """Test suite for HTTP/2 on_h2_remote_end_stream callback in asyncio""" timeout = 10.0 - async def _new_httpbin_h2_connection(self): - """Create HTTP/2 connection to httpbin.org""" - event_loop_group = EventLoopGroup() - host_resolver = DefaultHostResolver(event_loop_group) - bootstrap = ClientBootstrap(event_loop_group, host_resolver) - - tls_ctx_options = TlsContextOptions() - tls_ctx = ClientTlsContext(tls_ctx_options) - tls_conn_opt = tls_ctx.new_connection_options() - tls_conn_opt.set_server_name("httpbin.org") - tls_conn_opt.set_alpn_list(["h2"]) - - connection = await AIOHttp2ClientConnection.new( - host_name="httpbin.org", - port=443, - bootstrap=bootstrap, - tls_connection_options=tls_conn_opt) - - return connection - async def _test_h2_remote_end_stream_ordering(self): """Test that on_h2_remote_end_stream fires before on_complete when server finishes first""" - connection = await self._new_httpbin_h2_connection() + connection = await self._new_mock_h2_connection() - # Use httpbin.org 404 path - server responds immediately - request = HttpRequest('POST', '/this-path-does-not-exist-deliberately-404') - request.headers.add('host', 'httpbin.org') + # POST to the mock server's early-response path: the server returns 404 + # as soon as the request headers arrive, before the client finishes + # sending its body. + request = HttpRequest('POST', '/respond-immediately') + request.headers.add('host', self.mock_server_url.hostname) complete_success = asyncio.Event() remote_finished = asyncio.Event() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-crt-python-0.37.0/test/test_http_client.py new/aws-crt-python-0.38.0/test/test_http_client.py --- old/aws-crt-python-0.37.0/test/test_http_client.py 2026-09-23 09:04:30.000000000 +0200 +++ new/aws-crt-python-0.38.0/test/test_http_client.py 2026-10-16 16:15:10.000000000 +0200 @@ -903,37 +903,20 @@ self.assertEqual(None, connection.close().result(self.timeout)) -class Http2RemoteEndStreamTest(NativeResourceTest): [email protected](os.environ.get('AWS_TEST_LOCALHOST'), 'set env var to run test: AWS_TEST_LOCALHOST') +class Http2RemoteEndStreamTest(MockServerTestBase): """Test suite for HTTP/2 on_h2_remote_end_stream callback""" timeout = 10 # seconds - def _new_httpbin_h2_connection(self): - """Create HTTP/2 connection to httpbin.org""" - event_loop_group = EventLoopGroup() - host_resolver = DefaultHostResolver(event_loop_group) - bootstrap = ClientBootstrap(event_loop_group, host_resolver) - - tls_ctx_options = TlsContextOptions() - tls_ctx = ClientTlsContext(tls_ctx_options) - tls_conn_opt = tls_ctx.new_connection_options() - tls_conn_opt.set_server_name("httpbin.org") - tls_conn_opt.set_alpn_list(["h2"]) - - connection_future = Http2ClientConnection.new( - host_name="httpbin.org", - port=443, - bootstrap=bootstrap, - tls_connection_options=tls_conn_opt) - - return connection_future.result(self.timeout) - def test_h2_remote_end_stream_ordering(self): """Test that on_h2_remote_end_stream fires before on_complete when server finishes first""" - connection = self._new_httpbin_h2_connection() + connection = self._new_mock_h2_connection() - # Use httpbin.org 404 path - server responds immediately - request = HttpRequest('POST', '/this-path-does-not-exist-deliberately-404') - request.headers.add('host', 'httpbin.org') + # POST to the mock server's early-response path: the server returns 404 + # as soon as the request headers arrive, before the client finishes + # sending its body. + request = HttpRequest('POST', '/respond-immediately') + request.headers.add('host', self.mock_server_url.hostname) response = Response() stream = connection.request(request, response.on_response, response.on_body, manual_write=True) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-crt-python-0.37.0/test/test_mqtt.py new/aws-crt-python-0.38.0/test/test_mqtt.py --- old/aws-crt-python-0.37.0/test/test_mqtt.py 2026-09-23 09:04:30.000000000 +0200 +++ new/aws-crt-python-0.38.0/test/test_mqtt.py 2026-10-16 16:15:10.000000000 +0200 @@ -136,6 +136,9 @@ self.assertEqual(self.TEST_TOPIC, suback['topic']) self.assertIs(QoS.AT_LEAST_ONCE, suback['qos']) + # subscribe eventual consistency + time.sleep(0.5) + # publish published, packet_id = connection.publish(self.TEST_TOPIC, self.TEST_MSG, QoS.AT_LEAST_ONCE) puback = published.result(TIMEOUT) @@ -275,6 +278,9 @@ subscribed, packet_id = connection.subscribe(self.TEST_TOPIC, QoS.AT_LEAST_ONCE) subscribed.result(TIMEOUT) + # subscribe eventual consistency + time.sleep(0.5) + # publish published, packet_id = connection.publish(self.TEST_TOPIC, self.TEST_MSG, QoS.AT_LEAST_ONCE) puback = published.result(TIMEOUT) @@ -322,6 +328,9 @@ subscribed, packet_id = connection.subscribe(self.TEST_TOPIC, QoS.AT_LEAST_ONCE, on_sub_message) subscribed.result(TIMEOUT) + # subscribe eventual consistency + time.sleep(0.5) + # publish published, packet_id = connection.publish(self.TEST_TOPIC, self.TEST_MSG, QoS.AT_LEAST_ONCE) puback = published.result(TIMEOUT) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-crt-python-0.37.0/test/test_mqtt5.py new/aws-crt-python-0.38.0/test/test_mqtt5.py --- old/aws-crt-python-0.37.0/test/test_mqtt5.py 2026-09-23 09:04:30.000000000 +0200 +++ new/aws-crt-python-0.38.0/test/test_mqtt5.py 2026-10-16 16:15:10.000000000 +0200 @@ -1074,6 +1074,8 @@ subscribe_future = client.subscribe(subscribe_packet=subscribe_packet) suback_packet = subscribe_future.result(TIMEOUT) self.assertIsInstance(suback_packet, mqtt5.SubackPacket) + # subscribe eventual consistency + time.sleep(0.5) publish_packet = mqtt5.PublishPacket( payload=payload, @@ -1093,6 +1095,8 @@ unsubscribe_future = client.unsubscribe(unsubscribe_packet) unsuback_packet = unsubscribe_future.result(TIMEOUT) self.assertIsInstance(unsuback_packet, mqtt5.UnsubackPacket) + # unsubscribe eventual consistency + time.sleep(0.5) publish_future = client.publish(publish_packet=publish_packet) publish_completion_data = publish_future.result(TIMEOUT) @@ -1230,6 +1234,9 @@ suback_packet = subscribe_future.result(TIMEOUT) self.assertIsInstance(suback_packet, mqtt5.SubackPacket) + # subscribe eventual consistency + time.sleep(0.5) + disconnect_packet = mqtt5.DisconnectPacket(reason_code=mqtt5.DisconnectReasonCode.DISCONNECT_WITH_WILL_MESSAGE) client1.stop(disconnect_packet=disconnect_packet) callbacks1.future_stopped.result(TIMEOUT) @@ -1310,6 +1317,8 @@ subscribe_future = client.subscribe(subscribe_packet=subscribe_packet) suback_packet = subscribe_future.result(TIMEOUT) self.assertIsInstance(suback_packet, mqtt5.SubackPacket) + # subscribe eventual consistency + time.sleep(0.5) publish_packet = mqtt5.PublishPacket( payload=payload, @@ -1331,6 +1340,8 @@ unsubscribe_future = client.unsubscribe(unsubscribe_packet) unsuback_packet = unsubscribe_future.result(TIMEOUT) self.assertIsInstance(unsuback_packet, mqtt5.UnsubackPacket) + # unsubscribe eventual consistency + time.sleep(0.5) publish_future = client.publish(publish_packet=publish_packet) publish_completion_data = publish_future.result(TIMEOUT) @@ -1376,6 +1387,8 @@ subscribe_future = client.subscribe(subscribe_packet=subscribe_packet) suback_packet = subscribe_future.result(TIMEOUT) self.assertIsInstance(suback_packet, mqtt5.SubackPacket) + # subscribe eventual consistency + time.sleep(0.5) publish_packet = mqtt5.PublishPacket( payload=payload, @@ -1586,6 +1599,8 @@ subscribe_future = client2.subscribe(subscribe_packet=subscribe_packet) suback_packet = subscribe_future.result(TIMEOUT) self.assertIsInstance(suback_packet, mqtt5.SubackPacket) + # subscribe eventual consistency + time.sleep(0.5) publishes = 10 @@ -1665,6 +1680,8 @@ subscribe_future = client.subscribe(subscribe_packet=subscribe_packet) suback_packet = subscribe_future.result(TIMEOUT) self.assertIsInstance(suback_packet, mqtt5.SubackPacket) + # subscribe eventual consistency + time.sleep(0.5) # Publish a QoS 1 message with a unique UUID payload publish_packet = mqtt5.PublishPacket( @@ -1748,6 +1765,8 @@ subscribe_future = client.subscribe(subscribe_packet=subscribe_packet) suback_packet = subscribe_future.result(TIMEOUT) self.assertIsInstance(suback_packet, mqtt5.SubackPacket) + # subscribe eventual consistency + time.sleep(0.5) # Publish a QoS 1 message with a unique UUID payload publish_packet = mqtt5.PublishPacket( @@ -1835,6 +1854,8 @@ subscriptions = [mqtt5.Subscription(topic_filter=topic_filter, qos=mqtt5.QoS.AT_LEAST_ONCE)] subscribe_future = client.subscribe(mqtt5.SubscribePacket(subscriptions=subscriptions)) subscribe_future.result(TIMEOUT) + # subscribe eventual consistency + time.sleep(0.5) publish_future = client.publish(mqtt5.PublishPacket( payload=payload, topic=topic_filter, qos=mqtt5.QoS.AT_LEAST_ONCE)) @@ -1886,6 +1907,8 @@ subscriptions = [mqtt5.Subscription(topic_filter=topic_filter, qos=mqtt5.QoS.AT_LEAST_ONCE)] subscribe_future = client.subscribe(mqtt5.SubscribePacket(subscriptions=subscriptions)) subscribe_future.result(TIMEOUT) + # subscribe eventual consistency + time.sleep(0.5) publish_future = client.publish(mqtt5.PublishPacket( payload=payload, topic=topic_filter, qos=mqtt5.QoS.AT_LEAST_ONCE)) @@ -1940,6 +1963,8 @@ subscriptions = [mqtt5.Subscription(topic_filter=topic_filter, qos=mqtt5.QoS.AT_LEAST_ONCE)] subscribe_future = client.subscribe(mqtt5.SubscribePacket(subscriptions=subscriptions)) subscribe_future.result(TIMEOUT) + # subscribe eventual consistency + time.sleep(0.5) # Publish at QoS 0, there's no PUBACK involved publish_future = client.publish(mqtt5.PublishPacket( @@ -2008,6 +2033,8 @@ subscribe_future = client.subscribe(subscribe_packet=subscribe_packet) suback_packet = subscribe_future.result(TIMEOUT) self.assertIsInstance(suback_packet, mqtt5.SubackPacket) + # subscribe eventual consistency + time.sleep(0.5) # Publish a QoS 1 message with a unique UUID payload publish_packet = mqtt5.PublishPacket( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-crt-python-0.37.0/test/test_mqtt5to3_adapter.py new/aws-crt-python-0.38.0/test/test_mqtt5to3_adapter.py --- old/aws-crt-python-0.37.0/test/test_mqtt5to3_adapter.py 2026-09-23 09:04:30.000000000 +0200 +++ new/aws-crt-python-0.38.0/test/test_mqtt5to3_adapter.py 2026-10-16 16:15:10.000000000 +0200 @@ -6,6 +6,7 @@ from awscrt.mqtt import Connection, ConnectReturnCode, OnConnectionSuccessData, OnConnectionFailureData, OnConnectionClosedData, QoS from test import test_retry_wrapper, NativeResourceTest from test.test_mqtt5 import Mqtt5TestCallbacks, _get_env_variable, create_client_id +import time import unittest import uuid @@ -363,6 +364,9 @@ # subscribe subscribed, packet_id = connection.subscribe(TEST_TOPIC, QoS.AT_LEAST_ONCE, mqtt311_callbacks.on_message) suback = subscribed.result(TIMEOUT) + # subscribe eventual consistency + time.sleep(0.5) + self.assertEqual(packet_id, suback['packet_id']) self.assertEqual(TEST_TOPIC, suback['topic']) self.assertIs(QoS.AT_LEAST_ONCE, suback['qos']) @@ -383,6 +387,9 @@ # unsubscribe unsubscribed, packet_id = connection.unsubscribe(TEST_TOPIC) unsuback = unsubscribed.result(TIMEOUT) + # unsubscribe eventual consistency + time.sleep(0.5) + self.assertEqual(packet_id, unsuback['packet_id']) # publish @@ -529,6 +536,8 @@ subscribed, packet_id = connection3.subscribe( TEST_TOPIC3, QoS.AT_LEAST_ONCE, mqtt311_callbacks3.on_message) suback = subscribed.result(TIMEOUT) + # subscribe eventual consistency + time.sleep(0.5) # publish on topic1 publish_packet = mqtt5.PublishPacket( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-crt-python-0.37.0/test/test_mqtt_request_response.py new/aws-crt-python-0.38.0/test/test_mqtt_request_response.py --- old/aws-crt-python-0.37.0/test/test_mqtt_request_response.py 2026-09-23 09:04:30.000000000 +0200 +++ new/aws-crt-python-0.38.0/test/test_mqtt_request_response.py 2026-10-16 16:15:10.000000000 +0200 @@ -394,6 +394,9 @@ assert subscribed_future.result(30) + # eventual consistency if this is an op + time.sleep(0.5) + if isinstance(protocol_client, mqtt5.Client): publish = mqtt5.PublishPacket( payload=payload, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-crt-python-0.37.0/test/test_s3.py new/aws-crt-python-0.38.0/test/test_s3.py --- old/aws-crt-python-0.37.0/test/test_s3.py 2026-09-23 09:04:30.000000000 +0200 +++ new/aws-crt-python-0.38.0/test/test_s3.py 2026-10-16 16:15:10.000000000 +0200 @@ -21,6 +21,7 @@ S3ChecksumAlgorithm, S3ChecksumConfig, S3ChecksumLocation, + S3ChecksumValidationMode, S3Client, S3RequestType, S3ResponseError, @@ -191,7 +192,8 @@ is_cancel_test=False, enable_s3express=False, mem_limit=None, - network_interface_names=None): + network_interface_names=None, + connect_timeout_ms=None): if is_cancel_test: # for cancellation tests, make things slow, so it's less likely that @@ -222,7 +224,8 @@ throughput_target_gbps=throughput_target_gbps, enable_s3express=enable_s3express, memory_limit=mem_limit, - network_interface_names=network_interface_names) + network_interface_names=network_interface_names, + connect_timeout_ms=connect_timeout_ms) return s3_client @@ -258,6 +261,10 @@ with self.assertRaises(Exception): s3_client_new(True, self.region, network_interface_names=("eth0", "invalid-network-interface")) + def test_sanity_connect_timeout_ms(self): + s3_client = s3_client_new(True, self.region, connect_timeout_ms=3000) + self.assertIsNotNone(s3_client) + def test_wait_shutdown(self): s3_client = s3_client_new(False, self.region) self.assertIsNotNone(s3_client) @@ -398,6 +405,10 @@ enable_s3express=False, mem_limit=None, **kwargs): + # reset per-request state, so tests can make several requests + self.received_body_len = 0 + self.response_headers = None + self.response_status_code = None s3_client = s3_client_new( False, self.region, @@ -756,6 +767,16 @@ self.assertEqual(self.done_checksum_validation_algorithm, algo) self.assertEqual(HttpHeaders(self.response_headers).get(checksum_header_name), checksum_str) + + # download again, validating against a caller-provided expected checksum + download_request = self._get_object_request(path) + download_checksum_config = S3ChecksumConfig( + expected_checksum=checksum_str, + expected_checksum_algorithm=algo) + self._test_s3_put_get_object(download_request, S3RequestType.GET_OBJECT, + checksum_config=download_checksum_config) + self.assertTrue(self.done_did_validate_checksum) + self.assertEqual(self.done_checksum_validation_algorithm, algo) put_body_stream.close() def test_round_trip_with_trailing_checksum(self): @@ -779,6 +800,58 @@ def test_round_trip_with_full_object_checksum_single_part_crc32(self): self._round_trip_with_checksums_helper(S3ChecksumAlgorithm.CRC32, mpu=False, provide_full_object_checksum=True) + def test_get_object_expected_checksum_mismatch(self): + # 'AAAAAA==' is a well-formed CRC32 that doesn't match the object + request = self._get_object_request(self.get_test_object_path) + checksum_config = S3ChecksumConfig( + expected_checksum='AAAAAA==', + expected_checksum_algorithm=S3ChecksumAlgorithm.CRC32) + self._test_s3_put_get_object(request, S3RequestType.GET_OBJECT, + exception_name='AWS_ERROR_S3_RESPONSE_CHECKSUM_MISMATCH', + checksum_config=checksum_config) + + def _assert_invalid_checksum_config(self, request, request_type, checksum_config): + s3_client = s3_client_new(False, self.region, self.part_size) + with self.assertRaisesRegex(RuntimeError, 'AWS_ERROR_INVALID_ARGUMENT'): + s3_client.make_request( + request=request, + type=request_type, + checksum_config=checksum_config) + + def test_expected_checksum_invalid_config(self): + get_request = self._get_object_request(self.get_test_object_path) + # algorithm missing + self._assert_invalid_checksum_config( + get_request, S3RequestType.GET_OBJECT, + S3ChecksumConfig(expected_checksum='AAAAAA==')) + # checksum missing + self._assert_invalid_checksum_config( + get_request, S3RequestType.GET_OBJECT, + S3ChecksumConfig(expected_checksum_algorithm=S3ChecksumAlgorithm.CRC32)) + # wrong length for the algorithm + self._assert_invalid_checksum_config( + get_request, S3RequestType.GET_OBJECT, + S3ChecksumConfig(expected_checksum='AAAAAA==', expected_checksum_algorithm=S3ChecksumAlgorithm.SHA256)) + # contradicts REQUEST_ONLY validation + self._assert_invalid_checksum_config( + get_request, S3RequestType.GET_OBJECT, + S3ChecksumConfig( + expected_checksum='AAAAAA==', + expected_checksum_algorithm=S3ChecksumAlgorithm.CRC32, + response_validation_mode=S3ChecksumValidationMode.REQUEST_ONLY)) + # not a download + put_request = self._put_object_request(BytesIO(b'hello'), 5) + self._assert_invalid_checksum_config( + put_request, S3RequestType.PUT_OBJECT, + S3ChecksumConfig(expected_checksum='AAAAAA==', expected_checksum_algorithm=S3ChecksumAlgorithm.CRC32)) + + def test_get_object_request_only_validation(self): + request = self._get_object_request(self.get_test_object_path) + checksum_config = S3ChecksumConfig( + validate_response=True, + response_validation_mode=S3ChecksumValidationMode.REQUEST_ONLY) + self._test_s3_put_get_object(request, S3RequestType.GET_OBJECT, checksum_config=checksum_config) + def _on_progress_cancel_after_first_chunk(self, progress): self.transferred_len += progress self.progress_invoked += 1 ++++++ skip-test-requiring-network.patch ++++++ ++++ 2404 lines (skipped) ++++ between /work/SRC/openSUSE:Factory/python-awscrt/skip-test-requiring-network.patch ++++ and /work/SRC/openSUSE:Factory/.python-awscrt.new.1255/skip-test-requiring-network.patch
