Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions .github/workflows/pythontest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,33 @@ name: Python testing
on: [push, pull_request]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ".[dev]"
- name: lint with ruff
run: |
ruff format tdclient --diff --exit-non-zero-on-fix
ruff check tdclient
- name: Run pyright
run: |
pyright tdclient

test:
runs-on: ${{ matrix.os }}
strategy:
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
Expand All @@ -23,9 +43,6 @@ jobs:
pip install ".[dev]"
pip install -r requirements.txt -r test-requirements.txt
pip install -U coveralls pyyaml
- name: Run pyright
run: |
pyright tdclient
- name: Run test
run: |
coverage run --source=tdclient -m pytest tdclient/test
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Requirements
``td-client`` supports the following versions of Python.


* Python 3.5+
* Python 3.10+
* PyPy

Install
Expand Down
4 changes: 1 addition & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,7 @@ def linkcode_resolve(domain, info):
except Exception:
linenum = ""

return "https://github.com/{}/{}/blob/{}/{}/{}#L{}".format(
GH_ORGANIZATION, GH_PROJECT, revision, MODULE, relpath, linenum
)
return f"https://github.com/{GH_ORGANIZATION}/{GH_PROJECT}/blob/{revision}/{MODULE}/{relpath}#L{linenum}"


# -- Project information -----------------------------------------------------
Expand Down
14 changes: 9 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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/"}
Expand All @@ -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",
]
Expand All @@ -31,7 +31,6 @@ dependencies = [
"python-dateutil",
"msgpack>=0.6.2",
"urllib3",
"typing-extensions>=4.0.0",
]

[project.optional-dependencies]
Expand All @@ -46,11 +45,16 @@ tdclient = ["py.typed"]

[tool.ruff]
line-length = 88
target-version = "py310"

[tool.ruff.lint]
select = [
"E",
"W",
"F",
"I",
"UP",
"B",
]
exclude = ["tdclient/test/*"]
ignore = ["E203", "E501"]
Expand All @@ -62,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
Expand Down
2 changes: 0 additions & 2 deletions tdclient/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from __future__ import annotations

import datetime
import time
from typing import Any
Expand Down
74 changes: 28 additions & 46 deletions tdclient/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#!/usr/bin/env python

from __future__ import annotations

import contextlib
import csv
import email.utils
Expand All @@ -11,7 +9,6 @@
import json
import logging
import os
import socket
import ssl
import tempfile
import time
Expand All @@ -34,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,
Expand Down Expand Up @@ -108,11 +105,11 @@ def __init__(
if user_agent is not None:
self._user_agent = user_agent
else:
self._user_agent = "TD-Client-Python/%s" % (version.__version__)
self._user_agent = f"TD-Client-Python/{version.__version__}"

if endpoint is not None:
if not urlparse.urlparse(endpoint).scheme:
endpoint = "https://{}".format(endpoint)
endpoint = f"https://{endpoint}"
self._endpoint = endpoint
elif os.getenv("TD_API_SERVER"):
self._endpoint = os.getenv("TD_API_SERVER")
Expand Down Expand Up @@ -154,7 +151,7 @@ def _init_http(
if http_proxy.startswith("http://"):
return self._init_http_proxy(http_proxy, **kwargs)
else:
return self._init_http_proxy("http://%s" % (http_proxy,), **kwargs)
return self._init_http_proxy(f"http://{http_proxy}", **kwargs)

def _init_http_proxy(self, http_proxy: str, **kwargs: Any) -> urllib3.ProxyManager:
pool_options = dict(kwargs)
Expand All @@ -164,7 +161,7 @@ def _init_http_proxy(self, http_proxy: str, **kwargs: Any) -> urllib3.ProxyManag
if "@" in netloc:
auth, netloc = netloc.split("@", 2)
pool_options["proxy_headers"] = urllib3.make_headers(proxy_basic_auth=auth)
return urllib3.ProxyManager("%s://%s" % (scheme, netloc), **pool_options)
return urllib3.ProxyManager(f"{scheme}://{netloc}", **pool_options)

def get(
self,
Expand Down Expand Up @@ -214,12 +211,12 @@ def get(
self._max_cumul_retry_delay,
)
except (
OSError,
urllib3.exceptions.TimeoutStateError,
urllib3.exceptions.TimeoutError,
urllib3.exceptions.PoolError,
http.client.IncompleteRead,
TimeoutError,
socket.error,
):
pass

Expand All @@ -235,12 +232,7 @@ def get(
retry_delay *= 2
else:
raise APIError(
"Retrying stopped after %d seconds. (cumulative: %d/%d)"
% (
self._max_cumul_retry_delay,
cumul_retry_delay,
self._max_cumul_retry_delay,
)
f"Retrying stopped after {self._max_cumul_retry_delay} seconds. (cumulative: {cumul_retry_delay}/{self._max_cumul_retry_delay})"
)

log.debug(
Expand All @@ -254,7 +246,7 @@ def get(
def post(
self,
path: str,
params: dict[str, Any] | None = None,
params: dict[str, Any] | bytes | None = None,
headers: dict[str, str] | None = None,
**kwargs: Any,
) -> contextlib.AbstractContextManager[urllib3.BaseHTTPResponse]:
Expand Down Expand Up @@ -314,13 +306,15 @@ def post(
self._max_cumul_retry_delay,
)
except (
OSError,
urllib3.exceptions.TimeoutStateError,
urllib3.exceptions.TimeoutError,
urllib3.exceptions.PoolError,
socket.error,
):
if not self._retry_post_requests:
raise APIError("Retrying stopped by retry_post_requests == False")
raise APIError(
"Retrying stopped by retry_post_requests == False"
) from None

if cumul_retry_delay <= self._max_cumul_retry_delay:
log.warning(
Expand All @@ -334,12 +328,7 @@ def post(
retry_delay *= 2
else:
raise APIError(
"Retrying stopped after %d seconds. (cumulative: %d/%d)"
% (
self._max_cumul_retry_delay,
cumul_retry_delay,
self._max_cumul_retry_delay,
)
f"Retrying stopped after {self._max_cumul_retry_delay} seconds. (cumulative: {cumul_retry_delay}/{self._max_cumul_retry_delay})"
)

log.debug(
Expand Down Expand Up @@ -408,12 +397,12 @@ def put(
else:
raise APIError("Error %d: %s", response.status, response.data)
except (
OSError,
urllib3.exceptions.TimeoutStateError,
urllib3.exceptions.TimeoutError,
urllib3.exceptions.PoolError,
socket.error,
):
raise APIError("Error: %s" % (repr(response)))
raise APIError(f"Error: {repr(response)}") from None

log.debug(
"REST PUT response:\n headers: %s\n status: %d\n body: <omitted>",
Expand Down Expand Up @@ -470,10 +459,10 @@ def delete(
self._max_cumul_retry_delay,
)
except (
OSError,
urllib3.exceptions.TimeoutStateError,
urllib3.exceptions.TimeoutError,
urllib3.exceptions.PoolError,
socket.error,
):
pass

Expand All @@ -489,12 +478,7 @@ def delete(
retry_delay *= 2
else:
raise APIError(
"Retrying stopped after %d seconds. (cumulative: %d/%d)"
% (
self._max_cumul_retry_delay,
cumul_retry_delay,
self._max_cumul_retry_delay,
)
f"Retrying stopped after {self._max_cumul_retry_delay} seconds. (cumulative: {cumul_retry_delay}/{self._max_cumul_retry_delay})"
)

log.debug(
Expand Down Expand Up @@ -536,7 +520,7 @@ def build_request(
# use default headers first
_headers = dict(self._headers)
# add default headers
_headers["authorization"] = "TD1 %s" % (self._apikey,)
_headers["authorization"] = f"TD1 {self._apikey}"
_headers["date"] = email.utils.formatdate(time.time())
_headers["user-agent"] = self._user_agent
# override given headers
Expand All @@ -548,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:
Expand All @@ -571,28 +555,26 @@ def raise_error(
status_code = res.status
s = body if isinstance(body, str) else body.decode("utf-8")
if status_code == 404:
raise errors.NotFoundError("%s: %s" % (msg, s))
raise errors.NotFoundError(f"{msg}: {s}")
elif status_code == 409:
raise errors.AlreadyExistsError("%s: %s" % (msg, s))
raise errors.AlreadyExistsError(f"{msg}: {s}")
elif status_code == 401:
raise errors.AuthError("%s: %s" % (msg, s))
raise errors.AuthError(f"{msg}: {s}")
elif status_code == 403:
raise errors.ForbiddenError("%s: %s" % (msg, s))
raise errors.ForbiddenError(f"{msg}: {s}")
else:
raise errors.APIError("%d: %s: %s" % (status_code, msg, s))
raise errors.APIError(f"{status_code}: {msg}: {s}")

def checked_json(self, body: bytes, required: list[str]) -> dict[str, Any]:
js = None
try:
js = json.loads(body.decode("utf-8"))
except ValueError as error:
raise APIError("Unexpected API response: %s: %s" % (error, repr(body)))
raise APIError(f"Unexpected API response: {error}: {repr(body)}") from error
js = dict(js)
if 0 < [k in js for k in required].count(False):
missing = [k for k in required if k not in js]
raise APIError(
"Unexpected API response: %s: %s" % (repr(missing), repr(body))
)
raise APIError(f"Unexpected API response: {repr(missing)}: {repr(body)}")
return js

def close(self) -> None:
Expand All @@ -619,11 +601,11 @@ def _read_file(self, file_like, fmt, **kwargs):
compressed = fmt.endswith(".gz")
if compressed:
fmt = fmt[0 : len(fmt) - len(".gz")]
reader_name = "_read_%s_file" % (fmt,)
reader_name = f"_read_{fmt}_file"
if hasattr(self, reader_name):
reader = getattr(self, reader_name)
else:
raise TypeError("unknown format: %s" % (fmt,))
raise TypeError(f"unknown format: {fmt}")
if hasattr(file_like, "read"):
if compressed:
file_like = gzip.GzipFile(fileobj=file_like)
Expand Down
Loading