diff --git a/.github/workflows/pythontest.yml b/.github/workflows/pythontest.yml index 02d917a..fae4be8 100644 --- a/.github/workflows/pythontest.yml +++ b/.github/workflows/pythontest.yml @@ -10,7 +10,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.12" + python-version: "3.13" - name: Install dependencies run: | python -m pip install --upgrade pip @@ -29,7 +29,7 @@ jobs: max-parallel: 4 matrix: os: [ubuntu-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 diff --git a/README.rst b/README.rst index 50a4389..2259f14 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ Requirements ``td-client`` supports the following versions of Python. -* Python 3.5+ +* Python 3.10+ * PyPy Install diff --git a/pyproject.toml b/pyproject.toml index e151c67..b439d6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "td-client" version = "1.5.0" description = "Treasure Data API library for Python" readme = {file = "README.rst", content-type = "text/x-rst; charset=UTF-8"} -requires-python = ">=3.8" +requires-python = ">=3.10" license = {text = "Apache Software License"} authors = [{name = "Treasure Data, Inc.", email = "support@treasure-data.com"}] urls = {homepage = "http://treasuredata.com/"} @@ -18,11 +18,11 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Topic :: Internet", - "Programming Language :: Python :: 3.8", - "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", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] @@ -31,7 +31,6 @@ dependencies = [ "python-dateutil", "msgpack>=0.6.2", "urllib3", - "typing-extensions>=4.0.0", ] [project.optional-dependencies] @@ -46,6 +45,7 @@ tdclient = ["py.typed"] [tool.ruff] line-length = 88 +target-version = "py310" [tool.ruff.lint] select = [ @@ -66,7 +66,7 @@ known-third-party = ["dateutil","msgpack","pkg_resources","pytest","setuptools", include = ["tdclient"] exclude = ["**/__pycache__", "tdclient/test", "docs"] typeCheckingMode = "basic" -pythonVersion = "3.9" +pythonVersion = "3.10" pythonPlatform = "All" reportMissingTypeStubs = false reportUnknownMemberType = false diff --git a/tdclient/__init__.py b/tdclient/__init__.py index f569a41..1765ef1 100644 --- a/tdclient/__init__.py +++ b/tdclient/__init__.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import datetime import time from typing import Any diff --git a/tdclient/api.py b/tdclient/api.py index a0d1011..952eba8 100644 --- a/tdclient/api.py +++ b/tdclient/api.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import annotations - import contextlib import csv import email.utils @@ -33,7 +31,7 @@ from tdclient.schedule_api import ScheduleAPI from tdclient.server_status_api import ServerStatusAPI from tdclient.table_api import TableAPI -from tdclient.types import BytesOrStream +from tdclient.types import BytesOrStream, StreamBody from tdclient.user_api import UserAPI from tdclient.util import ( csv_dict_record_reader, @@ -534,7 +532,7 @@ def send_request( method: str, url: str, fields: dict[str, Any] | None = None, - body: bytes | bytearray | memoryview | array[int] | IO[bytes] | None = None, + body: StreamBody = None, headers: dict[str, str] | None = None, **kwargs: Any, ) -> urllib3.BaseHTTPResponse: diff --git a/tdclient/bulk_import_api.py b/tdclient/bulk_import_api.py index 12c3c27..1b7413d 100644 --- a/tdclient/bulk_import_api.py +++ b/tdclient/bulk_import_api.py @@ -1,22 +1,16 @@ #!/usr/bin/env python -from __future__ import annotations - import collections import contextlib import gzip import io import os from collections.abc import Iterator -from typing import TYPE_CHECKING, Any +from contextlib import AbstractContextManager +from typing import IO, Any import msgpack - -if TYPE_CHECKING: - from contextlib import AbstractContextManager - from typing import IO - - import urllib3 +import urllib3 from tdclient.types import BulkImportParams, BytesOrStream, DataFormat, FileLike from tdclient.util import create_url diff --git a/tdclient/bulk_import_model.py b/tdclient/bulk_import_model.py index b6092c1..8ea061d 100644 --- a/tdclient/bulk_import_model.py +++ b/tdclient/bulk_import_model.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import annotations - import time from collections.abc import Callable, Iterator from typing import TYPE_CHECKING, Any @@ -23,7 +21,7 @@ class BulkImport(Model): STATUS_COMMITTING = "committing" STATUS_COMMITTED = "committed" - def __init__(self, client: Client, **kwargs: Any) -> None: + def __init__(self, client: "Client", **kwargs: Any) -> None: super().__init__(client) self._feed(kwargs) @@ -116,7 +114,7 @@ def perform( wait_interval: int = 5, wait_callback: Callable[[], None] | None = None, timeout: float | None = None, - ) -> Job: + ) -> "Job": """Perform bulk import Args: diff --git a/tdclient/client.py b/tdclient/client.py index e34fe0e..3081102 100644 --- a/tdclient/client.py +++ b/tdclient/client.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import annotations - import datetime import json from collections.abc import Iterator @@ -27,7 +25,7 @@ class Client: def __init__(self, *args: Any, **kwargs: Any) -> None: self._api = api.API(*args, **kwargs) - def __enter__(self) -> Client: + def __enter__(self) -> "Client": return self def __exit__( diff --git a/tdclient/connection.py b/tdclient/connection.py index 7639668..3ff539a 100644 --- a/tdclient/connection.py +++ b/tdclient/connection.py @@ -1,15 +1,13 @@ #!/usr/bin/env python -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Callable +from collections.abc import Callable +from types import TracebackType +from typing import TYPE_CHECKING, Any from tdclient import api, cursor, errors from tdclient.types import Priority if TYPE_CHECKING: - from types import TracebackType - from tdclient.cursor import Cursor @@ -22,7 +20,7 @@ def __init__( priority: Priority | None = None, retry_limit: int | None = None, wait_interval: int | None = None, - wait_callback: Callable[[Cursor], None] | None = None, + wait_callback: Callable[["Cursor"], None] | None = None, **kwargs: Any, ) -> None: cursor_kwargs = dict() @@ -43,7 +41,7 @@ def __init__( self._api = api.API(**kwargs) self._cursor_kwargs = cursor_kwargs - def __enter__(self) -> Connection: + def __enter__(self) -> "Connection": return self def __exit__( @@ -67,5 +65,5 @@ def commit(self) -> None: def rollback(self) -> None: raise errors.NotSupportedError - def cursor(self) -> Cursor: + def cursor(self) -> "Cursor": return cursor.Cursor(self._api, **self._cursor_kwargs) diff --git a/tdclient/connector_api.py b/tdclient/connector_api.py index 71b93af..6b1dd25 100644 --- a/tdclient/connector_api.py +++ b/tdclient/connector_api.py @@ -1,14 +1,10 @@ #!/usr/bin/env python -from __future__ import annotations - import json -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from contextlib import AbstractContextManager +from contextlib import AbstractContextManager +from typing import Any - import urllib3 +import urllib3 from tdclient.util import create_url, normalize_connector_config diff --git a/tdclient/cursor.py b/tdclient/cursor.py index 260d750..d334f6b 100644 --- a/tdclient/cursor.py +++ b/tdclient/cursor.py @@ -1,9 +1,8 @@ #!/usr/bin/env python -from __future__ import annotations - import time -from typing import TYPE_CHECKING, Any, Callable +from collections.abc import Callable +from typing import TYPE_CHECKING, Any from tdclient import errors @@ -14,9 +13,9 @@ class Cursor: def __init__( self, - api: API, + api: "API", wait_interval: int = 5, - wait_callback: Callable[[Cursor], None] | None = None, + wait_callback: Callable[["Cursor"], None] | None = None, **kwargs: Any, ) -> None: self._api = api @@ -30,7 +29,7 @@ def __init__( self.wait_callback = wait_callback @property - def api(self) -> API: + def api(self) -> "API": return self._api @property diff --git a/tdclient/database_api.py b/tdclient/database_api.py index 3ebd9be..91f98b2 100644 --- a/tdclient/database_api.py +++ b/tdclient/database_api.py @@ -1,13 +1,9 @@ #!/usr/bin/env python -from __future__ import annotations +from contextlib import AbstractContextManager +from typing import Any -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from contextlib import AbstractContextManager - - import urllib3 +import urllib3 from tdclient.util import create_url, get_or_else, parse_date diff --git a/tdclient/database_model.py b/tdclient/database_model.py index 91f4d8d..64b3ac1 100644 --- a/tdclient/database_model.py +++ b/tdclient/database_model.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import annotations - import datetime from typing import TYPE_CHECKING, Any @@ -19,7 +17,7 @@ class Database(Model): PERMISSIONS = ["administrator", "full_access", "import_only", "query_only"] PERMISSION_LIST_TABLES = ["administrator", "full_access"] - def __init__(self, client: Client, db_name: str, **kwargs: Any) -> None: + def __init__(self, client: "Client", db_name: str, **kwargs: Any) -> None: super().__init__(client) self._db_name = db_name self._tables: list[Table] | None = kwargs.get("tables") @@ -57,7 +55,7 @@ def name(self) -> str: """ return self._db_name - def tables(self) -> list[Table]: + def tables(self) -> list["Table"]: """ Returns: a list of :class:`tdclient.model.Table` @@ -67,7 +65,7 @@ def tables(self) -> list[Table]: assert self._tables is not None return self._tables - def create_log_table(self, name: str) -> Table: + def create_log_table(self, name: str) -> "Table": """ Args: name (str): name of new log table @@ -77,7 +75,7 @@ def create_log_table(self, name: str) -> Table: """ return self._client.create_log_table(self._db_name, name) - def table(self, table_name: str) -> Table: + def table(self, table_name: str) -> "Table": """ Args: table_name (str): name of a table @@ -95,7 +93,7 @@ def delete(self) -> bool: """ return self._client.delete_database(self._db_name) - def query(self, q: str, **kwargs: Any) -> Job: + def query(self, q: str, **kwargs: Any) -> "Job": """Run a query on the database Args: diff --git a/tdclient/export_api.py b/tdclient/export_api.py index a8f18e6..000d75e 100644 --- a/tdclient/export_api.py +++ b/tdclient/export_api.py @@ -1,13 +1,9 @@ #!/usr/bin/env python -from __future__ import annotations +from contextlib import AbstractContextManager +from typing import Any -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from contextlib import AbstractContextManager - - import urllib3 +import urllib3 from tdclient.types import ExportParams from tdclient.util import create_url diff --git a/tdclient/import_api.py b/tdclient/import_api.py index 146b959..cad23ef 100644 --- a/tdclient/import_api.py +++ b/tdclient/import_api.py @@ -1,16 +1,11 @@ #!/usr/bin/env python -from __future__ import annotations - import contextlib import os -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from contextlib import AbstractContextManager - from typing import IO +from contextlib import AbstractContextManager +from typing import IO, Any - import urllib3 +import urllib3 from tdclient.types import BytesOrStream, DataFormat, FileLike from tdclient.util import create_url diff --git a/tdclient/job_api.py b/tdclient/job_api.py index 337d767..c91c024 100644 --- a/tdclient/job_api.py +++ b/tdclient/job_api.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import annotations - import codecs import gzip import json @@ -10,14 +8,11 @@ import tempfile from collections.abc import Iterator from concurrent.futures import ThreadPoolExecutor -from typing import TYPE_CHECKING, Any, Literal +from contextlib import AbstractContextManager +from typing import Any, Literal import msgpack - -if TYPE_CHECKING: - from contextlib import AbstractContextManager - - import urllib3 +import urllib3 from tdclient.types import Priority from tdclient.util import create_url, get_or_else, parse_date diff --git a/tdclient/job_model.py b/tdclient/job_model.py index c55fb53..fcf27c7 100644 --- a/tdclient/job_model.py +++ b/tdclient/job_model.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import annotations - import time import warnings from collections.abc import Callable, Iterator @@ -35,12 +33,12 @@ def type(self) -> str: """ return self._type - def __init__(self, fields: list[Schema.Field] | None = None) -> None: + def __init__(self, fields: list["Schema.Field"] | None = None) -> None: fields = [] if fields is None else fields self._fields = fields @property - def fields(self) -> list[Schema.Field]: + def fields(self) -> list["Schema.Field"]: """ TODO: add docstring """ @@ -67,7 +65,7 @@ class Job(Model): JOB_PRIORITY = {-2: "VERY LOW", -1: "LOW", 0: "NORMAL", 1: "HIGH", 2: "VERY HIGH"} def __init__( - self, client: Client, job_id: str, type: str, query: str | None, **kwargs: Any + self, client: "Client", job_id: str, type: str, query: str | None, **kwargs: Any ) -> None: super().__init__(client) self._job_id = job_id @@ -205,7 +203,7 @@ def wait( self, timeout: float | None = None, wait_interval: int = 5, - wait_callback: Callable[[Job], None] | None = None, + wait_callback: Callable[["Job"], None] | None = None, ) -> None: """Sleep until the job has been finished diff --git a/tdclient/result_api.py b/tdclient/result_api.py index 4c06e7e..e10558b 100644 --- a/tdclient/result_api.py +++ b/tdclient/result_api.py @@ -1,13 +1,9 @@ #!/usr/bin/env python -from __future__ import annotations +from contextlib import AbstractContextManager +from typing import Any -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from contextlib import AbstractContextManager - - import urllib3 +import urllib3 from tdclient.types import ResultParams from tdclient.util import create_url diff --git a/tdclient/result_model.py b/tdclient/result_model.py index 4fe2c11..34f2c50 100644 --- a/tdclient/result_model.py +++ b/tdclient/result_model.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import annotations - from typing import TYPE_CHECKING from tdclient.model import Model @@ -13,7 +11,7 @@ class Result(Model): """Result on Treasure Data Service""" - def __init__(self, client: Client, name: str, url: str, org_name: str) -> None: + def __init__(self, client: "Client", name: str, url: str, org_name: str) -> None: super().__init__(client) self._name = name self._url = url diff --git a/tdclient/schedule_api.py b/tdclient/schedule_api.py index 0764e1a..5ab4615 100644 --- a/tdclient/schedule_api.py +++ b/tdclient/schedule_api.py @@ -1,18 +1,14 @@ #!/usr/bin/env python -from __future__ import annotations - import datetime -from typing import TYPE_CHECKING, Any +from contextlib import AbstractContextManager +from typing import Any + +import urllib3 from tdclient.types import ScheduleParams from tdclient.util import create_url, get_or_else, parse_date -if TYPE_CHECKING: - from contextlib import AbstractContextManager - - import urllib3 - class ScheduleAPI: """Access to Schedule API diff --git a/tdclient/schedule_model.py b/tdclient/schedule_model.py index a85f82b..4b54ed3 100644 --- a/tdclient/schedule_model.py +++ b/tdclient/schedule_model.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import annotations - import datetime from typing import TYPE_CHECKING, Any @@ -17,7 +15,7 @@ class ScheduledJob(Job): def __init__( self, - client: Client, + client: "Client", scheduled_at: datetime.datetime, job_id: str, type: str, @@ -36,7 +34,7 @@ def scheduled_at(self) -> datetime.datetime: class Schedule(Model): """Schedule on Treasure Data Service""" - def __init__(self, client: Client, *args: Any, **kwargs: Any) -> None: + def __init__(self, client: "Client", *args: Any, **kwargs: Any) -> None: super().__init__(client) if 0 < len(args): self._name: str | None = args[0] diff --git a/tdclient/server_status_api.py b/tdclient/server_status_api.py index 6d975a2..7ce326f 100644 --- a/tdclient/server_status_api.py +++ b/tdclient/server_status_api.py @@ -1,13 +1,9 @@ #!/usr/bin/env python -from __future__ import annotations +from contextlib import AbstractContextManager +from typing import Any -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from contextlib import AbstractContextManager - - import urllib3 +import urllib3 class ServerStatusAPI: diff --git a/tdclient/table_api.py b/tdclient/table_api.py index 2dcd45e..1670f0e 100644 --- a/tdclient/table_api.py +++ b/tdclient/table_api.py @@ -1,16 +1,11 @@ #!/usr/bin/env python -from __future__ import annotations - import json -from typing import TYPE_CHECKING, Any +from contextlib import AbstractContextManager +from typing import Any import msgpack - -if TYPE_CHECKING: - from contextlib import AbstractContextManager - - import urllib3 +import urllib3 from tdclient.util import create_url, get_or_else, parse_date diff --git a/tdclient/table_model.py b/tdclient/table_model.py index 4de608c..2a5248f 100644 --- a/tdclient/table_model.py +++ b/tdclient/table_model.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import annotations - import datetime from typing import TYPE_CHECKING, Any @@ -208,7 +206,7 @@ def import_file( self._db_name, self._table_name, format, file, unique_id=unique_id ) - def export_data(self, storage_type: str, **kwargs: Any) -> Job: + def export_data(self, storage_type: str, **kwargs: Any) -> "Job": """Export data from Treasure Data Service Args: diff --git a/tdclient/types.py b/tdclient/types.py index 1ee0d49..2d035ab 100644 --- a/tdclient/types.py +++ b/tdclient/types.py @@ -1,56 +1,51 @@ """Type definitions for td-client-python.""" -from __future__ import annotations - from array import array -from typing import IO, TYPE_CHECKING - -from typing_extensions import Literal, TypeAlias, TypedDict - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any +from collections.abc import Callable +from typing import IO, Any, Literal, TypeAlias, TypedDict # File-like types -FileLike: TypeAlias = "str | bytes | IO[bytes]" +FileLike: TypeAlias = str | bytes | IO[bytes] """Type for file inputs: file path, bytes, or file-like object.""" -BytesOrStream: TypeAlias = "bytes | bytearray | IO[bytes]" +BytesOrStream: TypeAlias = bytes | bytearray | IO[bytes] """Type for byte data or streams (excluding file paths).""" StreamBody: TypeAlias = "bytes | bytearray | memoryview | array[int] | IO[bytes] | None" """Type for HTTP request body.""" # Query engine types -QueryEngineType: TypeAlias = 'Literal["presto", "hive"]' +QueryEngineType: TypeAlias = Literal["presto", "hive"] """Type for query engine selection.""" -EngineVersion: TypeAlias = 'Literal["stable", "experimental"]' +EngineVersion: TypeAlias = Literal["stable", "experimental"] """Type for engine version selection.""" -Priority: TypeAlias = ( - 'Literal[-2, -1, 0, 1, 2, "VERY LOW", "LOW", "NORMAL", "HIGH", "VERY HIGH"]' -) +Priority: TypeAlias = Literal[ + -2, -1, 0, 1, 2, "VERY LOW", "LOW", "NORMAL", "HIGH", "VERY HIGH" +] """Type for job priority levels (numeric or string).""" # Data format types -ExportFileFormat: TypeAlias = 'Literal["jsonl.gz", "tsv.gz", "json.gz"]' +ExportFileFormat: TypeAlias = Literal["jsonl.gz", "tsv.gz", "json.gz"] """Type for export file formats.""" -DataFormat: TypeAlias = 'Literal["msgpack", "msgpack.gz", "json", "json.gz", "csv", "csv.gz", "tsv", "tsv.gz"]' +DataFormat: TypeAlias = Literal[ + "msgpack", "msgpack.gz", "json", "json.gz", "csv", "csv.gz", "tsv", "tsv.gz" +] """Type for data import/export formats.""" -ResultFormat: TypeAlias = 'Literal["msgpack", "json", "csv", "tsv"]' +ResultFormat: TypeAlias = Literal["msgpack", "json", "csv", "tsv"] """Type for query result formats.""" # Utility types for CSV parsing and data processing -CSVValue: TypeAlias = "int | float | str | bool | None" +CSVValue: TypeAlias = int | float | str | bool | None """Type for values parsed from CSV files.""" -Converter: TypeAlias = "Callable[[str], Any]" +Converter: TypeAlias = Callable[[str], Any] """Type for converter functions that parse string values.""" -Record: TypeAlias = "dict[str, Any]" +Record: TypeAlias = dict[str, Any] """Type for data records (dictionaries with string keys and any values).""" diff --git a/tdclient/user_api.py b/tdclient/user_api.py index 2a6a892..c40b238 100644 --- a/tdclient/user_api.py +++ b/tdclient/user_api.py @@ -1,13 +1,9 @@ #!/usr/bin/env python -from __future__ import annotations +from contextlib import AbstractContextManager +from typing import Any -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from contextlib import AbstractContextManager - - import urllib3 +import urllib3 from tdclient.util import create_url diff --git a/tdclient/user_model.py b/tdclient/user_model.py index c1ac80e..b7011f8 100644 --- a/tdclient/user_model.py +++ b/tdclient/user_model.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import annotations - from typing import TYPE_CHECKING, Any from tdclient.model import Model @@ -15,7 +13,7 @@ class User(Model): def __init__( self, - client: Client, + client: "Client", name: str, org_name: str, role_names: list[str], diff --git a/tdclient/util.py b/tdclient/util.py index 8d5e646..2504312 100644 --- a/tdclient/util.py +++ b/tdclient/util.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import csv import io import logging @@ -233,7 +231,7 @@ def csv_text_record_reader( """ reader = csv.reader(io.TextIOWrapper(file_like, encoding), dialect=dialect) for row in reader: - yield dict(zip(columns, row)) + yield dict(zip(columns, row, strict=False)) def read_csv_records( @@ -300,7 +298,7 @@ def normalized_msgpack(value: Any) -> Any: Returns: Normalized value """ - if isinstance(value, (list, tuple)): + if isinstance(value, list | tuple): return [normalized_msgpack(v) for v in value] elif isinstance(value, dict): return dict( diff --git a/test-requirements.txt b/test-requirements.txt index 009e61f..6703b69 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,3 @@ coveralls>=1.1,<1.2 -mock>=1.3,<1.4 -pytest>=4.0,<=7.2 +pytest>=8.3 tox>=3.0,<4.0