Skip to content

Fix Python 3.12 compatibility: event loop, - #362

Closed
marcfischer wants to merge 1 commit into
ctera:masterfrom
marcfischer:fix/python312-compat-serializer-client
Closed

Fix Python 3.12 compatibility: event loop,#362
marcfischer wants to merge 1 commit into
ctera:masterfrom
marcfischer:fix/python312-compat-serializer-client

Conversation

@marcfischer

Copy link
Copy Markdown

Discovered via: Using the ctera.ctera Ansible collection (v2.3.9)
against a fresh SDK install on Python 3.12+. The collection is no
longer actively maintained and targets an older SDK version where
CTERAException was dict-like — these bugs are therefore unlikely to
surface through the SDK's own test suite.

Reproduction: Install the Ansible collection, run any portal module
(e.g. ctera_portal_storage_node) on Python 3.12, and observe the
errors in sequence:

  1. RuntimeError: There is no current event loop in thread
    'MainThread'
  2. AttributeError: 'AuthenticationError' object has no attribute
    'get'
  3. TypeError: Object of type AuthenticationError is not JSON
    serializable
  4. TypeError: 'NoneType' object is not callable (on delete
    operations)

JSON encoder, and delete serializer

Three bugs surfaced when running against Python 3.12 with the
redesigned
exception hierarchy (CTERAException is now a plain Exception
subclass,
no longer dict-like):

  1. clients.py – execute(): asyncio.get_event_loop() raises RuntimeError in Python 3.12 when no event loop is set for the current thread. The module already creates a dedicated event_loop at import time; use it directly instead of going through asyncio.get_event_loop().

  2. serializers.py – Encoder.default(): called o.get('dict', None), treating the object as a dict. SDK Object instances and Exception subclasses are not dict-like in the current codebase. Replace with getattr(o, 'dict', None) and an explicit Exception branch that returns str(o), so CTERAException subclasses (e.g. AuthenticationError) serialize to their message string instead of raising TypeError or AttributeError.

  3. clients.py – Client.delete(): unconditionally called data_serializer(data) even when data_serializer is None (the default). Callers such as XML.delete() intentionally omit the serializer; guard the call with a None check.

 JSON encoder, and delete serializer
Three bugs surfaced when running against Python 3.12 with the
redesigned
exception hierarchy (CTERAException is now a plain Exception
subclass,
no longer dict-like):
1. clients.py – execute(): asyncio.get_event_loop() raises
RuntimeError in
Python 3.12 when no event loop is set for the current thread. The
module
already creates a dedicated event_loop at import time; use it
directly
instead of going through asyncio.get_event_loop().
2. serializers.py – Encoder.default(): called o.get('__dict__',
None),
treating the object as a dict. SDK Object instances and Exception
subclasses are not dict-like in the current codebase. Replace
with
getattr(o, '__dict__', None) and an explicit Exception branch
that
returns str(o), so CTERAException subclasses (e.g.
AuthenticationError)
serialize to their message string instead of raising TypeError or
AttributeError.
3. clients.py – Client.delete(): unconditionally called
data_serializer(data)
even when data_serializer is None (the default). Callers such as
XML.delete()
intentionally omit the serializer; guard the call with a None
check.
@marcfischer

marcfischer commented Jun 18, 2026

Copy link
Copy Markdown
Author

without this pr it is not possible to remove a StorageNode:

This fails without this PR:

portal = GlobalAdmin('192.168.100.91')
portal.login('admin', 'password1!')
portal.portals.browse_global_admin()
portal.buckets.delete('MainStorage')

Error:

Traceback (most recent call last):
File "/data/test_storage.py", line 12, in <module>
portal.buckets.delete('MainStorage')
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.13/site-packages/cterasdk/core/buckets.py", line 120, in delete
response = self._core.api.delete(f'/locations/{name}')
File "/usr/local/lib/python3.13/site-packages/cterasdk/clients/clients.py", line 329, in delete
response = super().delete(path, data, on_error=XMLHandler(), **kwargs)
File "/usr/local/lib/python3.13/site-packages/cterasdk/clients/decorators.py", line 19, in authenticate_then_execute
return execute_request(self, *args, **kwargs)
File "/usr/local/lib/python3.13/site-packages/cterasdk/clients/clients.py", line 240, in delete
request = async_requests.DeleteRequest(self._builder(path), data=data_serializer(data), **kwargs)
~~~~~~~~~~~~~~~^^^^^^
TypeError: 'NoneType' object is not callable
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7f3d44036e40>
Unclosed connector
connections: ['deque([(<aiohttp.client_proto.ResponseHandler object at 0x7f3d4458d780>, 1168792.570529471)])']
connector: <aiohttp.connector.TCPConnector object at 0x7f3d44036900>
ctera-initial-config-d9cc8789c-d8blw:/data$ python test_storage.py
Traceback (most recent call last):
File "/data/test_storage.py", line 12, in <module>
portal.buckets.delete('MainStorage')
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.13/site-packages/cterasdk/core/buckets.py", line 120, in delete
response = self._core.api.delete(f'/locations/{name}')
File "/usr/local/lib/python3.13/site-packages/cterasdk/clients/clients.py", line 329, in delete
response = super().delete(path, data, on_error=XMLHandler(), **kwargs)
File "/usr/local/lib/python3.13/site-packages/cterasdk/clients/decorators.py", line 19, in authenticate_then_execute
return execute_request(self, *args, **kwargs)
File "/usr/local/lib/python3.13/site-packages/cterasdk/clients/clients.py", line 240, in delete
request = async_requests.DeleteRequest(self._builder(path), data=data_serializer(data), **kwargs)
~~~~~~~~~~~~~~~^^^^^^
TypeError: 'NoneType' object is not callable
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7f5ea9496e40>
Unclosed connector
connections: ['deque([(<aiohttp.client_proto.ResponseHandler object at 0x7f5ea99d16a0>, 1168911.387573209)])']
connector: <aiohttp.connector.TCPConnector object at 0x7f5ea9496900>

@saimonation

Copy link
Copy Markdown
Collaborator

Thank you. Regression introduced as part of:
467fb74#diff-b2dfabac33f98aca353fe206e0125927bec71520d7bdece98c5d5a2b9d89345a

Normally, a delete request doesn't include a body, but it was added to enable deletion of shares via the Portal.
Resolved as part of: #367

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@marcfischer@saimonation