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

Reply via email to