Add Python 3.14 support (#20195)

* First pass add 3.14

* Update bitarray

* Update markupsafe

* Update zstd

* Update tach to version with 3.14 support

* Changes for 3.14 related things

* Refactor shutdown handling in sync method

* Ignore unclosed database ResourceWarning

Added a new warning to ignore for unclosed database resources.

* Update pytest.ini to ignore additional warnings

* ignore resourcewarnings about tempdir cleanup

* Minor cleanup for tests running under python 3.14

* Reduce consensus tests for test_ban_for_mismatched_tx_cost_fee

* make sure to close RPC client at end of test

* different idea for TempFile

* oh, no cleanup function

* small fixes for tests detected by 3.14

* update pytest.ini

* ignore sqlite errors during closing in __del__

* add code to ignore deprecatewarnings for loop policy

* Pass in ClientSession to wschiaconnection for proper cleanup

* Some more resource cleanup

* Trying to find unclosed sessions

* One more time needing a close

* wait for harvesters to connect to the farmer

* properly teardown vdf_server using context manager

* Testing cleanup for simulator tests

* Add in a sleep to allow sockets time to cleanup

* Revert "Testing cleanup for simulator tests"

This reverts commit c5a4ef0cc3.

* Revert "properly teardown vdf_server using context manager"

This reverts commit 18b456a7b6.

* Revert "Revert "Testing cleanup for simulator tests""

This reverts commit 8c8efc2e03.

* vdf_server cleanup

* attempt 47 of timelord cleanup

* Some other cleanup fixes in tests

* yet another attempt at timelord shutdown

* Some more adjustments for 3.14

* ignore unclosed socket resource warnings

* some more adjustments and cleanup

* remove commented code

* correct poetry.lock file

* Address some review comments

* fix merge indentation

* fix install script print

* update requests

* fix problems with timelord tests on 3.14

* Make sure to close dummy session on any exception

* Use different cleanup pattern to help with flakes on Windows

* better shutdown handling

* hopefully better awaiting for cleanup

* yet more better timelord cleanup

* some test adjustments

* more time for cleanup
This commit is contained in:
Earle Lowe
2026-05-13 09:32:01 -05:00
committed by GitHub
parent 05398e14ac
commit c32a4a7493
31 changed files with 1509 additions and 1083 deletions
@@ -43,7 +43,7 @@ jobs:
matrix: arm
- name: Intel
matrix: intel
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
exclude:
- os:
matrix: windows
+1
View File
@@ -45,6 +45,7 @@ jobs:
- major_dot_minor: "3.11"
- major_dot_minor: "3.12"
- major_dot_minor: "3.13"
- major_dot_minor: "3.14"
exclude:
- os:
matrix: windows
+10
View File
@@ -103,6 +103,16 @@ jobs:
exclude_from:
limited: True
main: True
- name: "3.14"
file_name: "3.14"
action: "3.14"
apt: "3.14"
install_sh: "3.14"
matrix: "3.14"
exclude_from:
limited: True
main: True
exclude:
- arch:
matrix: arm
+1
View File
@@ -63,6 +63,7 @@ jobs:
- major_dot_minor: "3.11"
- major_dot_minor: "3.12"
- major_dot_minor: "3.13"
- major_dot_minor: "3.14"
check:
- name: mypy
command: |
+1 -1
View File
@@ -46,7 +46,7 @@ if ($null -eq (Get-Command py -ErrorAction SilentlyContinue))
Exit 1
}
$supportedPythonVersions = "3.13", "3.12", "3.11", "3.10"
$supportedPythonVersions = "3.14", "3.13", "3.12", "3.11", "3.10"
if ("$env:INSTALL_PYTHON_VERSION" -ne "")
{
$pythonVersion = $env:INSTALL_PYTHON_VERSION
+5
View File
@@ -79,6 +79,7 @@ async def add_dummy_connection_wsc(
]
timeout = aiohttp.ClientTimeout(total=10)
session = aiohttp.ClientSession(timeout=timeout)
try:
config = load_config(server.root_path, "config.yaml")
ca_crt_path: Path
@@ -115,8 +116,12 @@ async def add_dummy_connection_wsc(
30,
local_capabilities_for_handshake=default_capabilities[type] + additional_capabilities,
stub_metadata_for_type=StubMetadataRegistry,
session=session,
)
await wsc.perform_handshake(server._network_id, dummy_port, type)
except Exception:
await session.close()
raise
if wait_for_peer_added:
await time_out_assert(5, lambda: peer_id in server.all_connections)
if wsc.incoming_message_task is not None:
+13 -12
View File
@@ -96,6 +96,7 @@ class TestDos:
await session.ws_connect(
url, autoclose=True, autoping=True, ssl=ssl_context, max_msg_size=100 * 1024 * 1024
)
await session.close()
@pytest.mark.anyio
async def test_large_message_disconnect_and_ban(
@@ -110,15 +111,15 @@ class TestDos:
# Use the server_2 ssl information to connect to server_1, and send a huge message
timeout = ClientTimeout(total=10)
session = ClientSession(timeout=timeout)
url = f"wss://{self_hostname}:{server_1._port}/ws"
ssl_context = server_2.ssl_client_context
ws = await session.ws_connect(
url, autoclose=True, autoping=True, ssl=ssl_context, max_msg_size=100 * 1024 * 1024
)
assert not ws.closed
async with (
ClientSession(timeout=timeout) as session,
session.ws_connect(
url, autoclose=True, autoping=True, ssl=ssl_context, max_msg_size=100 * 1024 * 1024
) as ws,
):
large_msg: bytes = bytes([0] * (60 * 1024 * 1024))
with monkeypatch.context() as monkey_patch_context:
monkey_patch_context.setattr(chia.server.server, "is_localhost", not_localhost)
@@ -130,7 +131,6 @@ class TestDos:
print(response)
assert response.type == WSMsgType.CLOSE
assert response.data == WSCloseCode.MESSAGE_TOO_BIG
await ws.close()
@pytest.mark.anyio
async def test_bad_handshake_and_ban(
@@ -146,13 +146,15 @@ class TestDos:
server_1.invalid_protocol_ban_seconds = int(10 + adjusted_timeout(1))
# Use the server_2 ssl information to connect to server_1, and send a huge message
timeout = ClientTimeout(total=10)
session = ClientSession(timeout=timeout)
url = f"wss://{self_hostname}:{server_1._port}/ws"
ssl_context = server_2.ssl_client_context
ws = await session.ws_connect(
async with (
ClientSession(timeout=timeout) as session,
session.ws_connect(
url, autoclose=True, autoping=True, ssl=ssl_context, max_msg_size=100 * 1024 * 1024
)
) as ws,
):
with monkeypatch.context() as monkey_patch_context:
monkey_patch_context.setattr(chia.server.server, "is_localhost", not_localhost)
await ws.send_bytes(bytes([1] * 1024))
@@ -163,7 +165,6 @@ class TestDos:
print(response)
assert response.type == WSMsgType.CLOSE
assert response.data == WSCloseCode.PROTOCOL_ERROR
await ws.close()
@pytest.mark.anyio
async def test_invalid_protocol_handshake(
@@ -114,4 +114,7 @@ def test_base_event_loop_has_methods() -> None:
assert str(inspect.signature(method)) == "()"
finally:
if sys.platform != "win32":
if pausable_server is not None:
pausable_server.close()
selector_event_loop.close()
+3
View File
@@ -8,6 +8,7 @@ import random
import subprocess
import sys
import threading
import warnings
from collections.abc import AsyncIterator
from dataclasses import dataclass, field
@@ -115,6 +116,8 @@ class ServeInThread:
def _run(self) -> None:
# TODO: yuck yuck, messes with a single global
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
original_event_loop_policy = asyncio.get_event_loop_policy()
asyncio.set_event_loop_policy(chia_policy.ChiaPolicy())
try:
@@ -88,7 +88,7 @@ async def test_empty_request(setup_node_and_rpc: tuple[FullNodeRpcClient, FullNo
@pytest.mark.anyio
async def test_empty_peak(one_node_no_blocks: tuple[FullNodeRpcClient, FullNodeRpcApi]) -> None:
_client, full_node_rpc_api = one_node_no_blocks
client, full_node_rpc_api = one_node_no_blocks
response = await full_node_rpc_api.get_fee_estimate({"target_times": [], "cost": 1})
del response["node_time_utc"]
assert response == {
@@ -108,6 +108,10 @@ async def test_empty_peak(one_node_no_blocks: tuple[FullNodeRpcClient, FullNodeR
"num_spends": 0,
}
# one_node_no_blocks is not an async generator, so we have to close the client ourselves
client.close()
await client.await_closed()
@pytest.mark.anyio
async def test_no_target_times(setup_node_and_rpc: tuple[FullNodeRpcClient, FullNodeRpcApi]) -> None:
@@ -67,6 +67,7 @@ class FakeDNSResolver:
return []
@pytest.mark.filterwarnings("ignore:unclosed:ResourceWarning")
class TestSimulation:
@pytest.mark.limit_consensus_modes(reason="This test only supports one running at a time.")
@pytest.mark.anyio
+2 -1
View File
@@ -20,6 +20,7 @@ from chia.wallet.wallet_node import WalletNode
@pytest.mark.anyio
@pytest.mark.filterwarnings("ignore:unclosed:ResourceWarning")
@pytest.mark.parametrize(argnames="count", argvalues=[0, 1, 2, 5, 10])
@pytest.mark.parametrize(argnames="guarantee_transaction_blocks", argvalues=[False, True])
async def test_simulation_farm_blocks_to_puzzlehash(
@@ -176,7 +177,7 @@ async def test_process_transaction_records(
)
await full_node_api.process_transaction_records(records=action_scope.side_effects.transactions)
assert full_node_api.full_node.coin_store.get_coin_record(coin.name()) is not None
assert await full_node_api.full_node.coin_store.get_coin_record(coin.name()) is not None
@pytest.mark.anyio
+7 -1
View File
@@ -22,11 +22,16 @@ async def test_timelord_has_no_server(timelord_service: TimelordService) -> None
class _NullTransport(asyncio.Transport):
_closing = False
def write(self, data: bytes) -> None:
pass
def close(self) -> None:
self._closing = True
def is_closing(self) -> bool:
return False
return self._closing
def _make_null_writer() -> asyncio.StreamWriter:
@@ -72,6 +77,7 @@ async def test_invalid_vdf_proof_is_ignored_in_process_communication(
writer = _make_null_writer()
await timelord._do_process_communication(chain, challenge, initial_form, "127.0.0.1", reader, writer, proof_label=1)
writer.close()
assert timelord.proofs_finished == []
assert state_changed_calls == []
+20 -1
View File
@@ -1,8 +1,11 @@
from __future__ import annotations
import asyncio
import cProfile
import gc
import logging
import shutil
import sys
import tempfile
import time
from collections.abc import Callable, Iterator
@@ -131,7 +134,8 @@ async def run_sync_test(
check_log = ExitOnError()
logger.addHandler(check_log)
with tempfile.TemporaryDirectory() as root_dir:
root_dir = tempfile.mkdtemp()
try:
root_path = Path(root_dir, "root")
if start_at_checkpoint is not None:
shutil.copytree(start_at_checkpoint, root_path)
@@ -252,3 +256,18 @@ async def run_sync_test(
logger.warning(f"end-height: {height}")
if node_profiler:
(root_path / "profile-node").rename("./profile-node")
finally:
# On Windows, SQLite WAL/SHM handles may not be released immediately
# after closing, causing PermissionError on cleanup. Retry with
# backoff — same pattern CPython uses in its own test infrastructure:
# https://github.com/python/cpython/issues/59701
# https://github.com/python/cpython/issues/98219
gc.collect()
for attempt in range(20):
try:
shutil.rmtree(root_dir)
break
except PermissionError:
if attempt == 19 or sys.platform != "win32":
raise
await asyncio.sleep(2)
+14
View File
@@ -23,6 +23,7 @@ from chia.full_node.full_node_service import FullNodeService
from chia.harvester.harvester import Harvester
from chia.harvester.harvester_service import HarvesterService
from chia.introducer.introducer_api import IntroducerAPI
from chia.protocols.outbound_message import NodeType
from chia.protocols.shared_protocol import Capability
from chia.server.server import ChiaServer
from chia.simulator.block_tools import BlockTools, create_block_tools_async
@@ -360,6 +361,19 @@ async def setup_farmer_solver_multi_harvester(
for i in range(harvester_count)
]
# Ensure all harvesters are connected to the farmer
# this helps with proper test setup and with proper teardown
if start_services:
with anyio.fail_after(delay=adjusted_timeout(10)):
for backoff in backoff_times():
all_connected = all(
len(harvester_service._node.server.get_connections(NodeType.FARMER)) > 0
for harvester_service in harvester_services
)
if all_connected:
break
await asyncio.sleep(backoff)
yield harvester_services, farmer_service, block_tools
+3 -1
View File
@@ -8,7 +8,9 @@ from pathlib import Path
@contextlib.contextmanager
def TempFile() -> Iterator[Path]:
path = Path(tempfile.NamedTemporaryFile().name)
t = tempfile.NamedTemporaryFile(delete=False)
path = Path(t.name)
t.close()
yield path
if path.exists():
path.unlink()
@@ -1,5 +1,7 @@
from __future__ import annotations
import sys
import pytest
from chia_rs import ConsensusConstants
from chia_rs.sized_bytes import bytes32
@@ -139,7 +141,11 @@ def test_replace_str_to_bytes_deprecated_field(caplog: pytest.LogCaptureFixture)
def test_replace_str_to_bytes_invalid_value() -> None:
# invalid value
with pytest.raises(ValueError, match="non-hexadecimal number found in"):
if sys.version_info >= (3, 14):
matchstr = "arg must contain an even number of hexadecimal digits"
else:
matchstr = "non-hexadecimal number found in"
with pytest.raises(ValueError, match=matchstr):
replace_str_to_bytes(
test_constants,
GENESIS_PRE_FARM_FARMER_PUZZLE_HASH="fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+2 -3
View File
@@ -394,13 +394,12 @@ class TestWalletRpc:
wallet_node.config["trusted_peers"] = {}
assert wallet_service.rpc_server is not None
client = await WalletRpcClient.create(
async with WalletRpcClient.create_as_context(
self_hostname,
wallet_service.rpc_server.listen_port,
wallet_service.root_path,
wallet_service.config,
)
) as client:
with pytest.raises(ValueError, match="No peer connected"):
await wallet_service.rpc_server.rpc_api.dl_verify_proof(fake_gpr.to_json_dict())
+30 -3
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
from asyncio import Queue
from collections import OrderedDict
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from dataclasses import dataclass
from random import Random
@@ -11,7 +12,7 @@ from chia_rs import AugSchemeMPL, Coin, CoinRecord, CoinSpend, CoinState, Progra
from chia_rs.sized_bytes import bytes32
from chia_rs.sized_ints import uint8, uint16, uint32, uint64
from chia._tests.connection_utils import add_dummy_connection
from chia._tests.connection_utils import add_dummy_connection, add_dummy_connection_wsc
from chia._tests.util.coin_store import add_coin_records_to_db
from chia.full_node.coin_store import CoinStore
from chia.full_node.full_node import FullNode
@@ -38,6 +39,33 @@ Mpu = tuple[FullNodeSimulator, Queue[Message], WSChiaConnection]
ALL_FILTER = wallet_protocol.CoinStateFilters(True, True, True, uint64(0))
@asynccontextmanager
async def connect_to_simulator_context(
one_node: OneNode, self_hostname: str, mempool_updates: bool = True
) -> AsyncGenerator[tuple[FullNodeSimulator, Queue[Message], WSChiaConnection], None]:
[full_node_service], _, _ = one_node
full_node_api = full_node_service._api
fn_server = full_node_api.server
wsc, peer_id = await add_dummy_connection_wsc(
fn_server,
self_hostname,
41723,
NodeType.WALLET,
additional_capabilities=[(uint16(Capability.MEMPOOL_UPDATES), "1")] if mempool_updates else [],
)
peer = fn_server.all_connections[peer_id]
incoming_queue = wsc.incoming_queue
try:
yield full_node_api, incoming_queue, peer
finally:
# closing the client side of the dummy connection
await wsc.close()
await wsc.wait_until_closed()
async def connect_to_simulator(
one_node: OneNode, self_hostname: str, mempool_updates: bool = True
) -> tuple[FullNodeSimulator, Queue[Message], WSChiaConnection]:
@@ -546,8 +574,7 @@ async def test_request_puzzle_state_reorg(one_node: OneNode, self_hostname: str)
@pytest.mark.anyio
async def test_request_puzzle_state_limit(one_node: OneNode, self_hostname: str) -> None:
simulator, _, peer = await connect_to_simulator(one_node, self_hostname)
async with connect_to_simulator_context(one_node, self_hostname) as (simulator, _, peer):
# Farm blocks 0-11 and make sure the last one is farmed
await simulator.farm_blocks_to_puzzlehash(12)
+9
View File
@@ -682,6 +682,15 @@ async def test_transaction_send_cache(self_hostname: str, simulator_and_wallet:
await time_out_assert(5, check_wallet_cache_empty, True)
# Wait for the rejection to be persisted to sent_to before testing resend behavior
async def check_sent_to_has_failed() -> bool:
record = await wallet.wallet_state_manager.tx_store.get_transaction_record(tx.name)
return record is not None and any(
status == MempoolInclusionStatus.FAILED.value for _, status, _ in record.sent_to
)
await time_out_assert(10, check_sent_to_has_failed, True)
# Re-process the queue — the peer already rejected it, so it should NOT be resent
await wallet_node._resend_queue()
with pytest.raises(AssertionError):
@@ -878,3 +878,6 @@ async def test_cat_wallet_conversion(
await time_out_assert_not_none(
15, check_length, 0, wallet_node_0.wallet_state_manager.get_all_wallet_info_entries, WalletType.CAT
)
client_0.close()
await client_0.await_closed()
+2 -1
View File
@@ -1431,7 +1431,8 @@ def launch_plotter(
plotter_path.unlink()
else:
plotter_path.parent.mkdir(parents=True, exist_ok=True)
outfile = open(plotter_path.resolve(), "w")
with open(plotter_path.resolve(), "w") as outfile:
log.info(f"Service array: {service_array}") # lgtm [py/clear-text-logging-sensitive-data]
process = subprocess.Popen(
service_array,
+1 -2
View File
@@ -1099,8 +1099,7 @@ class FullNode:
tb = traceback.format_exc()
self.log.error(f"Error with syncing: {type(e)}{tb}")
finally:
if self._shut_down:
return None
if not self._shut_down:
await self._finish_sync(fork_point)
async def request_validate_wp(
+2 -2
View File
@@ -464,9 +464,9 @@ class DNSServer:
try:
validated_peer = ip_address(peer)
if validated_peer.version == 4:
self.reliable_peers_v4.append(validated_peer)
self.reliable_peers_v4.append(IPv4Address(validated_peer))
elif validated_peer.version == 6:
self.reliable_peers_v6.append(validated_peer)
self.reliable_peers_v6.append(IPv6Address(validated_peer))
except ValueError:
log.error(f"Invalid peer: {peer}")
continue
+14
View File
@@ -348,6 +348,20 @@ if sys.platform == "win32":
self._proactor.disable_connections()
if sys.version_info >= (3, 14):
# DefaultEventLoopPolicy is deprecated in 3.14 and will be removed in 3.16
# Need to inherit from _BaseDefaultEventLoopPolicy instead
class ChiaPolicy(asyncio.events._BaseDefaultEventLoopPolicy):
def new_event_loop(self) -> asyncio.AbstractEventLoop:
# overriding https://github.com/python/cpython/blob/v3.14.0/Lib/asyncio/events.py#L726-L732
if sys.platform == "win32":
loop_factory = ChiaProactorEventLoop
else:
loop_factory = ChiaSelectorEventLoop
return loop_factory()
else:
class ChiaPolicy(asyncio.DefaultEventLoopPolicy):
def new_event_loop(self) -> asyncio.AbstractEventLoop:
# overriding https://github.com/python/cpython/blob/v3.11.0/Lib/asyncio/events.py#L689-L695
+3
View File
@@ -104,6 +104,9 @@ class FullNodeDiscovery:
cancel_task_safe(self.connect_peers_task, self.log)
cancel_task_safe(self.serialize_task, self.log)
cancel_task_safe(self.cleanup_task, self.log)
tasks_to_await = {t for t in (self.connect_peers_task, self.serialize_task, self.cleanup_task) if t is not None}
if len(tasks_to_await) > 0:
await asyncio.wait(tasks_to_await)
for t in self.pending_tasks:
cancel_task_safe(t, self.log)
if len(self.pending_tasks) > 0:
+12 -1
View File
@@ -190,10 +190,21 @@ class Timelord:
self.main_loop.cancel()
if self.bluebox_pool is not None:
self.bluebox_pool.shutdown()
for _, _, writer in self.free_clients:
await self._shutdown_vdf_clients()
if self.vdf_server is not None:
self.vdf_server.close()
async def _shutdown_vdf_clients(self) -> None:
"""Send stop signal and close all VDF client writers, suppressing errors."""
for _, _, writer in [*self.free_clients, *self.chain_type_to_stream.values()]:
with contextlib.suppress(Exception):
writer.write(b"010")
await writer.drain()
with contextlib.suppress(Exception):
writer.close()
await writer.wait_closed()
self.free_clients.clear()
self.chain_type_to_stream.clear()
def get_connections(self, request_node_type: NodeType | None) -> list[dict[str, Any]]:
return default_get_connections(server=self.server, request_node_type=request_node_type)
+3 -3
View File
@@ -71,7 +71,7 @@ OPENSSL_VERSION_INT=
find_python() {
set +e
unset BEST_VERSION
for V in 313 3.13 312 3.12 311 3.11 310 3.10 3; do
for V in 314 3.14 313 3.13 312 3.12 311 3.11 310 3.10 3; do
if command -v python$V >/dev/null; then
if [ "$BEST_VERSION" = "" ]; then
BEST_VERSION=$V
@@ -134,8 +134,8 @@ if ! command -v "$INSTALL_PYTHON_PATH" >/dev/null; then
exit 1
fi
if [ "$PYTHON_MAJOR_VER" -ne "3" ] || [ "$PYTHON_MINOR_VER" -lt "10" ] || [ "$PYTHON_MINOR_VER" -ge "14" ]; then
echo "Chia requires Python version >= 3.10 and < 3.14.0" >&2
if [ "$PYTHON_MAJOR_VER" -ne "3" ] || [ "$PYTHON_MINOR_VER" -lt "10" ] || [ "$PYTHON_MINOR_VER" -ge "15" ]; then
echo "Chia requires Python version >= 3.10 and < 3.15.0" >&2
echo "Current Python version = $INSTALL_PYTHON_VERSION" >&2
# If Arch, direct to Arch Wiki
if type pacman >/dev/null 2>&1 && [ -f "/etc/arch-release" ]; then
+1169 -892
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -95,7 +95,7 @@ pytest-mock = { version = ">=3.14.0", optional = true }
pytest-monitor = { version = ">=1.6.6", platform = "linux", optional = true }
pytest-xdist = { version = ">=3.6.1", optional = true }
pytest-rerunfailures = { version = ">=16.1, <17.0", optional = true }
tach = { version = ">=0.29.0", optional = true }
tach = { version = ">=0.32.0", optional = true }
types-aiofiles = { version = ">=24.1.0.20240626", optional = true }
types-pyyaml = { version = ">=6.0.12.20240917", optional = true }
types-setuptools = { version = ">=75.5.0.20241122", optional = true }
+6
View File
@@ -17,8 +17,14 @@ markers =
testpaths = chia/_tests/
filterwarnings =
error
ignore:unclosed database:ResourceWarning
ignore:Exception ignored in:pytest.PytestUnraisableExceptionWarning
ignore:cannot collect test class:pytest.PytestCollectionWarning
ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning
ignore:record_property is incompatible with junit_family:pytest.PytestWarning
ignore:Implicit None on return values is deprecated and will raise
ignore:'asyncio.iscoroutinefunction' is deprecated and slated for removal in Python 3.16:DeprecationWarning
ignore:Implicitly cleaning up <TemporaryDirectory:ResourceWarning
ignore:'asyncio.set_event_loop_policy' is deprecated and slated for removal in Python 3.16:DeprecationWarning
ignore:'asyncio.WindowsProactorEventLoopPolicy' is deprecated and slated for removal in Python 3.16:DeprecationWarning
ignore:unclosed <socket.socket:ResourceWarning