diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4d3516..54f958b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 23bdd07..51d4571 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Python connector for ThingsDB -> This library requires Python 3.9 or higher. +> This library requires Python 3.10 or higher. --------------------------------------- @@ -89,8 +89,8 @@ ThingsDB. ```python thingsdb.client.Client( auto_reconnect: bool = True, - ssl: Optional[Union[bool, ssl.SSLContext]] = None, - loop: Optional[asyncio.AbstractEventLoop] = None + ssl: bool | ssl.SSLContext | None = None, + loop: asyncio.AbstractEventLoop | None = None ) -> Client ``` Initialize a ThingsDB client @@ -119,8 +119,8 @@ Initialize a ThingsDB client ```python async Client().authenticate( - *auth: Union[str, tuple], - timeout: Optional[int] = 5 + *auth: str | tuple, + timeout: int | None = 5 ) -> None ``` @@ -166,7 +166,7 @@ This is equivalent of combining [close()](#close)) and [wait_closed()](#wait_clo Client().connect( host: str, port: int = 9200, - timeout: Optional[int] = 5 + timeout: int | None = 5 ) -> asyncio.Future ``` @@ -204,7 +204,7 @@ set to `None` when successful. ```python Client().connect_pool( pool: list, - *auth: Union[str, tuple] + *auth: str | tuple ) -> asyncio.Future ``` @@ -313,8 +313,8 @@ Can be used to check if the client is using a WebSocket connection. ```python Client().query( code: str, - scope: Optional[str] = None, - timeout: Optional[int] = None, + scope: str | None = None, + timeout: int | None = None, skip_strip_code: bool = False, **kwargs: Any ) -> asyncio.Future @@ -368,7 +368,7 @@ contain the result of the ThingsDB code when successful. ### reconnect ```python -async Client().reconnect() -> Optional[Future] +async Client().reconnect() -> Future | None ``` Re-connect to ThingsDB. @@ -384,9 +384,9 @@ possible but not required. ```python Client().run( procedure: str, - *args: Optional[Any], - scope: Optional[str] = None, - timeout: Optional[int] = None, + *args: Any, + scope: str | None = None, + timeout: int | None = None, **kwargs: Any ) -> asyncio.Future ``` @@ -547,7 +547,7 @@ Property | Description ### join ```python -Room().join(client: Client, wait: Optional[float] = 60.0) -> None +Room().join(client: Client, wait: float | None = 60.0) -> None ``` Joins the room. @@ -572,7 +572,7 @@ Leave the room. If the room is not found, a `LookupError` will be raised. ### emit ```python -Room().emit(event: str, *args: Optional[Any],) -> asyncio.Future +Room().emit(event: str, *args: Any) -> asyncio.Future ``` Emit an event to a room. diff --git a/setup.py b/setup.py index 1d19091..41cfcdd 100644 --- a/setup.py +++ b/setup.py @@ -43,10 +43,10 @@ # Specify the Python versions you support here. In particular, ensure # that you indicate whether you support Python 2, Python 3 or both. - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ], install_requires=[ 'msgpack', diff --git a/thingsdb/client/buildin.py b/thingsdb/client/buildin.py index ef163a7..5750ef4 100644 --- a/thingsdb/client/buildin.py +++ b/thingsdb/client/buildin.py @@ -1,8 +1,6 @@ import asyncio import datetime -from abc import ABC, abstractmethod -from typing import Union as U -from typing import Optional +from abc import abstractmethod from typing import Any @@ -15,13 +13,13 @@ class Buildin: def query( self, code: str, - scope: Optional[str] = None, - timeout: Optional[int] = None, + scope: str | None = None, + timeout: int | None = None, skip_strip_code: bool = False, **kwargs: Any) -> asyncio.Future[Any]: ... - async def collection_info(self, collection: U[int, str]) -> dict: + async def collection_info(self, collection: int | str) -> dict: """Returns information about a specific collection. This function requires QUERY privileges on the requested collection, @@ -39,7 +37,7 @@ async def collections_info(self) -> list: """ return await self.query('collections_info()', scope='@t') - async def del_collection(self, collection: U[int, str]): + async def del_collection(self, collection: int | str): """Delete a collection. This function generates a change. @@ -104,7 +102,7 @@ async def del_user(self, name: str): async def deploy_module( self, name: str, - data: Optional[U[bytes, str]] = None): + data: bytes | str | None = None): """Deploy a module on all nodes. The module must be configured first, using the new_module() function. @@ -127,7 +125,7 @@ async def deploy_module( data=data, scope='@t') - async def grant(self, target: U[int, str], user: str, mask: int): + async def grant(self, target: int | str, user: str, mask: int): """Grant, collection or general, privileges to a user. Access to a user is provided by setting a bit mask to either the @node, @@ -224,7 +222,7 @@ async def new_module( self, name: str, source: str, - configuration: Optional[Any] = None): + configuration: Any = None): """Creates (and configures) a new module for ThingsDB. This function generates a change.""" @@ -239,7 +237,7 @@ async def new_node( self, secret: str, name: str, - port: Optional[int] = 9220) -> int: + port: int | None = 9220) -> int: """Adds a new node to ThingsDB. This function generates a change.""" @@ -253,7 +251,7 @@ async def new_node( async def new_token( self, user: str, - expiration_time: Optional[datetime.datetime] = None, + expiration_time: datetime.datetime | None = None, description: str = ''): ts = None if expiration_time is None \ @@ -285,7 +283,7 @@ async def refresh_module(self, name: str): async def rename_collection( self, - collection: U[int, str], + collection: int | str, new_name: str) -> None: return await self.query( 'rename_collection(collection, new_name)', @@ -310,14 +308,14 @@ async def rename_user(self, name: str, new_name: str) -> None: async def restore( self, filename: str, - options: Optional[dict] = {}): + options: dict[str, Any] | None = {}): return await self.query( 'restore(filename, options)', filename=filename, options=options, scope='@t') - async def revoke(self, target: U[int, str], user: str, mask: int): + async def revoke(self, target: int | str, user: str, mask: int): return await self.query( 'revoke(target, user, mask)', target=target, @@ -328,7 +326,7 @@ async def revoke(self, target: U[int, str], user: str, mask: int): async def set_module_conf( self, name: str, - configuration: Optional[dict] = None): + configuration: dict[str, Any] | None = None): return await self.query( 'set_module_conf(name, configuration)', name=name, @@ -338,7 +336,7 @@ async def set_module_conf( async def set_module_scope( self, name: str, - scope: U[str, None]): + scope: str | None): return await self.query( 'set_module_scope(name, module_scope)', name=name, @@ -346,14 +344,14 @@ async def set_module_scope( scope='@t') async def set_password(self, user: str, - new_password: Optional[str] = None) -> None: + new_password: str | None = None) -> None: return await self.query( 'set_password(user, new_password)', user=user, new_password=new_password, scope='@t') - async def set_time_zone(self, collection: U[int, str], zone: str): + async def set_time_zone(self, collection: int | str, zone: str): """By default each collection will be created with time zone UTC. This function can be used to change the time zone for a collection. If @@ -378,7 +376,7 @@ async def time_zones_info(self) -> list: """ return await self.query('time_zones_info()', scope='@t') - async def user_info(self, user: Optional[str] = None) -> dict: + async def user_info(self, user: str | None = None) -> dict: if user is None: return await self.query('user_info()', scope='@t') return await self.query('user_info(user)', user=user, scope='@t') @@ -419,9 +417,9 @@ async def has_backup(self, backup_id: int, scope='@n'): async def new_backup( self, file_template: str, - start_ts: Optional[datetime.datetime] = None, - repeat: Optional[int] = 0, - max_files: Optional[int] = 7, + start_ts: datetime.datetime | None = None, + repeat: int | None = 0, + max_files: int | None = 7, scope='@n'): ts = None if start_ts is None else int(start_ts.timestamp()) diff --git a/thingsdb/client/client.py b/thingsdb/client/client.py index 80a1aa1..fec26aa 100644 --- a/thingsdb/client/client.py +++ b/thingsdb/client/client.py @@ -5,7 +5,7 @@ import time from collections import defaultdict from ssl import SSLContext, PROTOCOL_TLS -from typing import Optional, Union, Any, List, Tuple +from typing import Any from concurrent.futures import CancelledError from .buildin import Buildin from .protocol import Proto, Protocol, ProtocolWS @@ -21,8 +21,8 @@ class Client(Buildin): def __init__( self, auto_reconnect: bool = True, - ssl: Optional[Union[bool, ssl.SSLContext]] = None, - loop: Optional[asyncio.AbstractEventLoop] = None, + ssl: bool | ssl.SSLContext | None = None, + loop: asyncio.AbstractEventLoop | None = None, ) -> None: """Initialize a ThingsDB client. @@ -145,8 +145,8 @@ def connection_info(self) -> str: def connect_pool( self, - pool: List[Union[str, Tuple[str, int]]], - *auth: Union[str, Tuple[str, str]] + pool: list[str | tuple[str, int]], + *auth: str | tuple[str, str] ) -> asyncio.Future[None]: """Connect using a connection pool. @@ -201,7 +201,7 @@ async def connect( self, host: str, port: int = 9200, - timeout: Optional[int] = 5): + timeout: int | None = 5): """Connect to ThingsDB. This method will *only* create a connection, so the connection is not @@ -237,7 +237,7 @@ async def connect( self._pool_idx = 0 await self._connect(timeout=timeout) - def reconnect(self) -> Optional[asyncio.Future[Any]]: + def reconnect(self) -> asyncio.Future[Any] | None: """Re-connect to ThingsDB. This method can be used, even when a connection still exists. In case @@ -274,8 +274,8 @@ def is_websocket(self) -> bool: async def authenticate( self, - *auth: Union[str, tuple], - timeout: Optional[int] = 5 + *auth: str | tuple[str, str], + timeout: int | None = 5 ) -> None: """Authenticate a ThingsDB connection. @@ -298,8 +298,8 @@ async def authenticate( def query( self, code: str, - scope: Optional[str] = None, - timeout: Optional[int] = None, + scope: str | None = None, + timeout: int | None = None, skip_strip_code: bool = False, **kwargs: Any ) -> asyncio.Future[Any]: @@ -363,7 +363,7 @@ async def _ensure_write( tp: Proto, data: Any = None, is_bin: bool = False, - timeout: Optional[int] = None + timeout: int | None = None ) -> asyncio.Future[Any]: if not self._pool: raise ConnectionError('no connection') @@ -400,7 +400,7 @@ async def _write( tp: Proto, data: Any = None, is_bin: bool = False, - timeout: Optional[int] = None + timeout: int | None = None ) -> asyncio.Future[Any]: if not self.is_connected(): raise ConnectionError('no connection') @@ -410,9 +410,9 @@ async def _write( def run( self, procedure: str, - *args: Optional[Any], - scope: Optional[str] = None, - timeout: Optional[int] = None, + *args: Any, + scope: str | None = None, + timeout: int | None = None, **kwargs: Any, ) -> asyncio.Future[Any]: """Run a procedure. @@ -470,10 +470,10 @@ def run( async def _emit( self, - room_id: Union[int, str], + room_id: int | str, event: str, - *args: Optional[Any], - scope: Optional[str] = None): + *args: Any, + scope: str | None = None): """Emit an event. Use Room(room_id, scope=scope).emit(..) instead of this function to @@ -502,9 +502,9 @@ async def _emit( scope = self._scope await self._write_pkg(Proto.REQ_EMIT, [scope, room_id, event, *args]) - def _join(self, *ids: Union[int, str], - scope: Optional[str] = None - ) -> asyncio.Future[List[Optional[int]]]: + def _join(self, *ids: int | str, + scope: str | None = None + ) -> asyncio.Future[list[int | None]]: """Join one or more rooms. Args: @@ -532,9 +532,9 @@ def _join(self, *ids: Union[int, str], return self._write_pkg(Proto.REQ_JOIN, [scope, *ids]) # type: ignore - def _leave(self, *ids: Union[int, str], - scope: Optional[str] = None - ) -> asyncio.Future[List[Optional[int]]]: + def _leave(self, *ids: int | str, + scope: str | None = None + ) -> asyncio.Future[list[int | None]]: """Leave one or more rooms. Stop receiving events for the rooms given by one or more ids. It is @@ -584,7 +584,7 @@ def _auth_check(auth): def _is_websocket_host(host): return host.startswith('ws://') or host.startswith('wss://') - async def _connect(self, timeout: Optional[int] = 5): + async def _connect(self, timeout: int | None = 5): if not self._pool: return host, port = self._pool[self._pool_idx] diff --git a/thingsdb/client/package.py b/thingsdb/client/package.py index 9580b72..d24ca02 100644 --- a/thingsdb/client/package.py +++ b/thingsdb/client/package.py @@ -1,13 +1,12 @@ import struct import msgpack import logging -from typing import Optional _fail_file = '' -def set_package_fail_file(fn: Optional[str] = ''): +def set_package_fail_file(fn: str | None = ''): """Configure a file name to dump the last failed package. Only the MessagePack data will be dumped in this file, not the package diff --git a/thingsdb/client/protocol.py b/thingsdb/client/protocol.py index a9ac3e7..0f38171 100644 --- a/thingsdb/client/protocol.py +++ b/thingsdb/client/protocol.py @@ -4,7 +4,7 @@ import msgpack from abc import abstractmethod from ssl import SSLContext -from typing import Optional, Any, Callable +from typing import Any, Callable from .package import Package from ..exceptions import AssertionError from ..exceptions import AuthError @@ -202,7 +202,7 @@ def write( tp: Proto, data: Any = None, is_bin: bool = False, - timeout: Optional[int] = None + timeout: int | None = None ) -> asyncio.Future[Any]: """Write data to ThingsDB. This will create a new PID and returns a Future which will be @@ -269,14 +269,14 @@ def __init__( self, on_connection_lost: Callable[[asyncio.Protocol, Exception], None], on_event: Callable[[Package], None], - loop: Optional[asyncio.AbstractEventLoop] = None + loop: asyncio.AbstractEventLoop | None = None ): super().__init__(on_connection_lost, on_event) self._buffered_data = bytearray() self.package = None self.transport = None self.loop = asyncio.get_running_loop() if loop is None else loop - self.close_future: Optional[asyncio.Future[Any]] = None + self.close_future: asyncio.Future[Any] | None = None def connection_made(self, transport): ''' @@ -368,10 +368,10 @@ def __init__( 'missing `websockets` module; ' 'please install the `websockets` module: ' '\n\n pip install websockets\n\n') - self._proto: Optional[WebSocketClientProtocol] = None + self._proto: WebSocketClientProtocol | None = None self._is_closing = False - async def connect(self, uri, ssl: Optional[SSLContext]): + async def connect(self, uri, ssl: SSLContext | None): assert connect, 'websockets required, please install websockets' self._proto = await connect(uri, ssl=ssl, max_size=WEBSOCKET_MAX_SIZE) asyncio.create_task(self._recv_loop()) diff --git a/thingsdb/room/roombase.py b/thingsdb/room/roombase.py index 01954ee..b86c45f 100644 --- a/thingsdb/room/roombase.py +++ b/thingsdb/room/roombase.py @@ -1,7 +1,6 @@ import abc import asyncio import logging -from typing import Union, Optional from ..client import Client from ..client.protocol import Proto from ..util.is_name import is_name @@ -19,8 +18,8 @@ def __init_subclass__(cls): def __init__( self, - room: Union[int, str], - scope: Optional[str] = None): + room: int | str, + scope: str | None = None): """Initializes a room. Args: @@ -33,7 +32,7 @@ def __init__( Collection scope. If no scope is given, the scope will later be set to the default client scope once the room is joined. """ - self._client: Optional[Client] = None + self._client: Client | None = None self._id = room self._scope = scope self._wait_join = False @@ -86,7 +85,7 @@ async def no_join(self, client: Client): raise TypeError(f'Id `{id}` is not a room') self._id = id - async def join(self, client: Client, wait: Optional[float] = 60.0): + async def join(self, client: Client, wait: float | None = 60.0): """Join a room. Args: @@ -186,7 +185,7 @@ async def emit(self, event: str, *args): 'must call join(..) or no_join(..) before using emit') await self._client._emit(self._id, event, *args, scope=self._scope) - def _on_event(self, pkg) -> Optional[asyncio.Task]: + def _on_event(self, pkg) -> asyncio.Task | None: return self.__class__._ROOM_EVENT_MAP[pkg.tp](self, pkg.data) @abc.abstractmethod @@ -219,7 +218,7 @@ async def _on_first_join(self): finally: fut.set_result(None) - def _on_join(self, _data): + def _on_join(self, _data) -> asyncio.Task | None: if self._wait_join: # Future, the first join. Return a task so the room lock is kept # until the on_first_join is finished diff --git a/thingsdb/version.py b/thingsdb/version.py index 86ee767..58d478a 100644 --- a/thingsdb/version.py +++ b/thingsdb/version.py @@ -1 +1 @@ -__version__ = '1.1.9' +__version__ = '1.2.0'