Fix Python 3.12 build issues in thrift Python (#3276)

- Add pyproject.toml with setuptools build requirement for PEP 517 compliance
- Replace distutils imports with setuptools equivalents
- Use setuptools error names directly (CompileError, ExecError, PlatformError)
- Fix macOS header collision with ntohll/htonll macros in endian.h
- Add a matrix of MacOS versions (macos-15-intel, macos-14, macos-15,
  macos-26)
- Add a matrix of non-EOL Python versions for testing
- Remove MSVC2015 from the test matrix (very old).
- Support MSVC2022, the latest in AppVeyor.
- Upgrade tornado, twisted, and zope.interface versions to the first
  that support Python 3.12.
- Try to make the test_socket, RunClientServer, and TestServer tests less flaky.

This fixes the ModuleNotFoundError: No module named 'distutils' error
when building thrift with Python 3.12+.
diff --git a/lib/cpp/CMakeLists.txt b/lib/cpp/CMakeLists.txt
index 5980734..d189922 100644
--- a/lib/cpp/CMakeLists.txt
+++ b/lib/cpp/CMakeLists.txt
@@ -178,6 +178,9 @@
     else()
         target_link_libraries(thriftnb PUBLIC ${LIBEVENT_LIBRARIES})
     endif()
+    if(WIN32)
+        target_link_libraries(thriftnb PUBLIC iphlpapi)
+    endif()
     ADD_PKGCONFIG_THRIFT(thrift-nb)
 endif()
 
diff --git a/lib/nodejs/test/package-lock.json b/lib/nodejs/test/package-lock.json
index e7f9543..6faee9d 100644
--- a/lib/nodejs/test/package-lock.json
+++ b/lib/nodejs/test/package-lock.json
@@ -9,7 +9,7 @@
       }
     },
     "../../..": {
-      "version": "0.22.0",
+      "version": "0.23.0",
       "dev": true,
       "license": "Apache-2.0",
       "dependencies": {
diff --git a/lib/py/pyproject.toml b/lib/py/pyproject.toml
new file mode 100644
index 0000000..b61373e
--- /dev/null
+++ b/lib/py/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools>=61.0", "wheel"]
+build-backend = "setuptools.build_meta"
diff --git a/lib/py/setup.py b/lib/py/setup.py
index a02cc4f..2dd2a77 100644
--- a/lib/py/setup.py
+++ b/lib/py/setup.py
@@ -20,13 +20,10 @@
 #
 
 import sys
-try:
-    from setuptools import setup, Extension
-except Exception:
-    from distutils.core import setup, Extension
 
-from distutils.command.build_ext import build_ext
-from distutils.errors import CCompilerError, DistutilsExecError, DistutilsPlatformError
+from setuptools import Extension, setup
+from setuptools.command.build_ext import build_ext
+from setuptools.errors import CompileError, ExecError, PlatformError
 
 # Fix to build sdist under vagrant
 import os
@@ -39,9 +36,9 @@
 include_dirs = ['src']
 if sys.platform == 'win32':
     include_dirs.append('compat/win32')
-    ext_errors = (CCompilerError, DistutilsExecError, DistutilsPlatformError, IOError)
+    ext_errors = (CompileError, ExecError, PlatformError, IOError)
 else:
-    ext_errors = (CCompilerError, DistutilsExecError, DistutilsPlatformError)
+    ext_errors = (CompileError, ExecError, PlatformError)
 
 
 class BuildFailed(Exception):
@@ -52,7 +49,7 @@
     def run(self):
         try:
             build_ext.run(self)
-        except DistutilsPlatformError:
+        except PlatformError:
             raise BuildFailed()
 
     def build_extension(self, ext):
@@ -99,8 +96,8 @@
     ssl_deps = []
     if sys.hexversion < 0x03050000:
         ssl_deps.append('backports.ssl_match_hostname>=3.5')
-    tornado_deps = ['tornado>=4.0']
-    twisted_deps = ['twisted']
+    tornado_deps = ['tornado>=6.3.0']
+    twisted_deps = ['twisted>=24.3.0', 'zope.interface>=6.1']
 
     setup(name='thrift',
           version='0.23.0',
diff --git a/lib/py/src/ext/endian.h b/lib/py/src/ext/endian.h
index 8f9e978..bf21a4f 100644
--- a/lib/py/src/ext/endian.h
+++ b/lib/py/src/ext/endian.h
@@ -29,6 +29,7 @@
 #else
 #include <netinet/in.h>
 
+#ifndef ntohll
 static inline unsigned long long ntohll(unsigned long long n) {
   union {
     unsigned long long f;
@@ -43,8 +44,11 @@
          | static_cast<unsigned long long>(u.t[5]) << 16
          | static_cast<unsigned long long>(u.t[6]) << 8 | static_cast<unsigned long long>(u.t[7]);
 }
+#endif
 
+#ifndef htonll
 #define htonll(n) ntohll(n)
+#endif
 
 #endif // !_WIN32
 
diff --git a/lib/py/test/test_socket.py b/lib/py/test/test_socket.py
index af09515..5e25f1a 100644
--- a/lib/py/test/test_socket.py
+++ b/lib/py/test/test_socket.py
@@ -18,6 +18,7 @@
 #
 
 import errno
+import time
 import unittest
 
 from test_sslsocket import ServerAcceptor
@@ -98,7 +99,11 @@
             acc.close()
 
             self.assertIsNotNone(sock.handle)
-            self.assertFalse(sock.isOpen())
+            # Give the kernel a moment to propagate FIN before asserting.
+            deadline = time.monotonic() + 0.5
+            while sock.isOpen() and time.monotonic() < deadline:
+                time.sleep(0.01)
+            self.assertFalse(sock.isOpen(), "socket still open after 0.5s")
             # after isOpen() returned False the socket should be closed (THRIFT-5813)
             self.assertIsNone(sock.handle)