Skip to content

Commit f5bc498

Browse files
committed
Add type hint for utilities
1 parent f311c89 commit f5bc498

File tree

3 files changed

+101
-30
lines changed

3 files changed

+101
-30
lines changed

tdclient/errors.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,66 +2,96 @@
22

33

44
class ParameterValidationError(Exception):
5+
"""Exception raised when parameter validation fails."""
6+
57
pass
68

79

810
# Generic API error
911
class APIError(Exception):
12+
"""Base exception for API-related errors."""
13+
1014
pass
1115

1216

1317
# 401 API errors
1418
class AuthError(APIError):
19+
"""Exception raised for authentication errors (HTTP 401)."""
20+
1521
pass
1622

1723

1824
# 403 API errors, used for database permissions
1925
class ForbiddenError(APIError):
26+
"""Exception raised for forbidden access errors (HTTP 403)."""
27+
2028
pass
2129

2230

2331
# 409 API errors
2432
class AlreadyExistsError(APIError):
33+
"""Exception raised when a resource already exists (HTTP 409)."""
34+
2535
pass
2636

2737

2838
# 404 API errors
2939
class NotFoundError(APIError):
40+
"""Exception raised when a resource is not found (HTTP 404)."""
41+
3042
pass
3143

3244

3345
# PEP 0249 errors
3446
class Error(Exception):
47+
"""Base class for database-related errors (PEP 249)."""
48+
3549
pass
3650

3751

3852
class InterfaceError(Error):
53+
"""Exception for errors related to the database interface (PEP 249)."""
54+
3955
pass
4056

4157

4258
class DatabaseError(Error):
59+
"""Exception for errors related to the database (PEP 249)."""
60+
4361
pass
4462

4563

4664
class DataError(DatabaseError):
65+
"""Exception for errors due to problems with the processed data (PEP 249)."""
66+
4767
pass
4868

4969

5070
class OperationalError(DatabaseError):
71+
"""Exception for errors related to database operation (PEP 249)."""
72+
5173
pass
5274

5375

5476
class IntegrityError(DatabaseError):
77+
"""Exception for errors related to relational integrity (PEP 249)."""
78+
5579
pass
5680

5781

5882
class InternalError(DatabaseError):
83+
"""Exception for internal database errors (PEP 249)."""
84+
5985
pass
6086

6187

6288
class ProgrammingError(DatabaseError):
89+
"""Exception for programming errors (PEP 249)."""
90+
6391
pass
6492

6593

6694
class NotSupportedError(DatabaseError):
95+
"""Exception for unsupported operations (PEP 249)."""
96+
6797
pass

tdclient/types.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
from __future__ import annotations
44

55
from array import array
6-
from typing import IO
6+
from typing import IO, TYPE_CHECKING
77

88
from typing_extensions import Literal, TypeAlias, TypedDict
99

10+
if TYPE_CHECKING:
11+
from collections.abc import Callable
12+
from typing import Any
13+
1014
# File-like types
1115
FileLike: TypeAlias = "str | bytes | IO[bytes]"
1216
"""Type for file inputs: file path, bytes, or file-like object."""
@@ -39,6 +43,16 @@
3943
ResultFormat: TypeAlias = 'Literal["msgpack", "json", "csv", "tsv"]'
4044
"""Type for query result formats."""
4145

46+
# Utility types for CSV parsing and data processing
47+
CSVValue: TypeAlias = "int | float | str | bool | None"
48+
"""Type for values parsed from CSV files."""
49+
50+
Converter: TypeAlias = "Callable[[str], Any]"
51+
"""Type for converter functions that parse string values."""
52+
53+
Record: TypeAlias = "dict[str, Any]"
54+
"""Type for data records (dictionaries with string keys and any values)."""
55+
4256

4357
# TypedDict classes for structured parameters
4458
class ScheduleParams(TypedDict, total=False):

tdclient/util.py

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1+
from __future__ import annotations
2+
13
import csv
24
import io
35
import logging
46
import warnings
7+
from collections.abc import Iterator
8+
from datetime import datetime
9+
from typing import Any, BinaryIO
510
from urllib.parse import quote as urlquote
611

712
import dateutil.parser
813
import msgpack
914

15+
from tdclient.types import CSVValue, Converter, Record
16+
1017
log = logging.getLogger(__name__)
1118

1219

13-
def create_url(tmpl, **values):
20+
def create_url(tmpl: str, **values: Any) -> str:
1421
"""Create url with values
1522
1623
Args:
@@ -21,7 +28,7 @@ def create_url(tmpl, **values):
2128
return tmpl.format(**quoted_values)
2229

2330

24-
def validate_record(record):
31+
def validate_record(record: Record) -> bool:
2532
"""Check that `record` contains a key called "time".
2633
2734
Args:
@@ -41,7 +48,7 @@ def validate_record(record):
4148
return True
4249

4350

44-
def guess_csv_value(s):
51+
def guess_csv_value(s: str) -> CSVValue:
4552
"""Determine the most appropriate type for `s` and return it.
4653
4754
Tries to interpret `s` as a more specific datatype, in the following
@@ -75,7 +82,7 @@ def guess_csv_value(s):
7582

7683

7784
# Convert our dtype names to callables that parse a string into that type
78-
DTYPE_TO_CALLABLE = {
85+
DTYPE_TO_CALLABLE: dict[str, Converter] = {
7986
"bool": bool,
8087
"float": float,
8188
"int": int,
@@ -84,7 +91,9 @@ def guess_csv_value(s):
8491
}
8592

8693

87-
def merge_dtypes_and_converters(dtypes=None, converters=None):
94+
def merge_dtypes_and_converters(
95+
dtypes: dict[str, str] | None = None, converters: dict[str, Converter] | None = None
96+
) -> dict[str, Converter]:
8897
"""Generate a merged dictionary from those given.
8998
9099
Args:
@@ -113,23 +122,25 @@ def merge_dtypes_and_converters(dtypes=None, converters=None):
113122
If a column name occurs in both input dictionaries, the callable
114123
specified in `converters` is used.
115124
"""
116-
our_converters = {}
125+
our_converters: dict[str, Converter] = {}
117126
if dtypes is not None:
118-
try:
119-
for column_name, dtype in dtypes.items():
127+
for column_name, dtype in dtypes.items():
128+
try:
120129
our_converters[column_name] = DTYPE_TO_CALLABLE[dtype]
121-
except KeyError:
122-
raise ValueError(
123-
"Unrecognized dtype %r, must be one of %s"
124-
% (dtype, ", ".join(repr(k) for k in sorted(DTYPE_TO_CALLABLE)))
125-
)
130+
except KeyError:
131+
raise ValueError(
132+
"Unrecognized dtype %r, must be one of %s"
133+
% (dtype, ", ".join(repr(k) for k in sorted(DTYPE_TO_CALLABLE)))
134+
)
126135
if converters is not None:
127136
for column_name, parse_fn in converters.items():
128137
our_converters[column_name] = parse_fn
129138
return our_converters
130139

131140

132-
def parse_csv_value(k, s, converters=None):
141+
def parse_csv_value(
142+
k: str, s: str, converters: dict[str, Converter] | None = None
143+
) -> Any:
133144
"""Given a CSV (string) value, work out an actual value.
134145
135146
Args:
@@ -167,7 +178,9 @@ def parse_csv_value(k, s, converters=None):
167178
return parse_fn(s)
168179

169180

170-
def csv_dict_record_reader(file_like, encoding, dialect):
181+
def csv_dict_record_reader(
182+
file_like: BinaryIO, encoding: str, dialect: str | type[csv.Dialect]
183+
) -> Iterator[dict[str, str]]:
171184
"""Yield records from a CSV input using csv.DictReader.
172185
173186
This is a reader suitable for use by `tdclient.util.read_csv_records`_.
@@ -180,7 +193,7 @@ def csv_dict_record_reader(file_like, encoding, dialect):
180193
returns bytes.
181194
encoding (str): the name of the encoding to use when turning those
182195
bytes into strings.
183-
dialect (str): the name of the CSV dialect to use.
196+
dialect (str | type[csv.Dialect]): the name of the CSV dialect to use, or a Dialect class.
184197
185198
Yields:
186199
For each row of CSV data read from `file_like`, yields a dictionary
@@ -192,7 +205,12 @@ def csv_dict_record_reader(file_like, encoding, dialect):
192205
yield row
193206

194207

195-
def csv_text_record_reader(file_like, encoding, dialect, columns):
208+
def csv_text_record_reader(
209+
file_like: BinaryIO,
210+
encoding: str,
211+
dialect: str | type[csv.Dialect],
212+
columns: list[str],
213+
) -> Iterator[dict[str, str]]:
196214
"""Yield records from a CSV input using csv.reader and explicit column names.
197215
198216
This is a reader suitable for use by `tdclient.util.read_csv_records`_.
@@ -205,7 +223,7 @@ def csv_text_record_reader(file_like, encoding, dialect, columns):
205223
returns bytes.
206224
encoding (str): the name of the encoding to use when turning those
207225
bytes into strings.
208-
dialect (str): the name of the CSV dialect to use.
226+
dialect (str | type[csv.Dialect]): the name of the CSV dialect to use, or a Dialect class.
209227
210228
Yields:
211229
For each row of CSV data read from `file_like`, yields a dictionary
@@ -217,7 +235,12 @@ def csv_text_record_reader(file_like, encoding, dialect, columns):
217235
yield dict(zip(columns, row))
218236

219237

220-
def read_csv_records(csv_reader, dtypes=None, converters=None, **kwargs):
238+
def read_csv_records(
239+
csv_reader: Iterator[dict[str, str]],
240+
dtypes: dict[str, str] | None = None,
241+
converters: dict[str, Converter] | None = None,
242+
**kwargs: Any,
243+
) -> Iterator[Record]:
221244
"""Read records using csv_reader and yield the results."""
222245
our_converters = merge_dtypes_and_converters(dtypes, converters)
223246

@@ -227,7 +250,7 @@ def read_csv_records(csv_reader, dtypes=None, converters=None, **kwargs):
227250
yield record
228251

229252

230-
def create_msgpack(items):
253+
def create_msgpack(items: list[dict[str, Any]]) -> bytes:
231254
"""Create msgpack streaming bytes from list
232255
233256
Args:
@@ -256,7 +279,7 @@ def create_msgpack(items):
256279
return stream.getvalue()
257280

258281

259-
def normalized_msgpack(value):
282+
def normalized_msgpack(value: Any) -> Any:
260283
"""Recursively convert int to str if the int "overflows".
261284
262285
Args:
@@ -292,17 +315,19 @@ def normalized_msgpack(value):
292315
return value
293316

294317

295-
def get_or_else(hashmap, key, default_value=None):
318+
def get_or_else(
319+
hashmap: dict[str, str], key: str, default_value: str | None = None
320+
) -> str | None:
296321
"""Get value or default value
297322
298323
It differs from the standard dict ``get`` method in its behaviour when
299324
`key` is present but has a value that is an empty string or a string of
300325
only spaces.
301326
302327
Args:
303-
hashmap (dict): target
304-
key (Any): key
305-
default_value (Any): default value
328+
hashmap (dict): target dictionary with string values
329+
key (str): key to look up
330+
default_value (str | None): default value to return if key is missing or value is empty/whitespace
306331
307332
Example:
308333
@@ -326,27 +351,29 @@ def get_or_else(hashmap, key, default_value=None):
326351
return default_value
327352

328353

329-
def parse_date(s):
354+
def parse_date(s: str | None) -> datetime | None:
330355
"""Parse date from str to datetime
331356
332357
TODO: parse datetime using an optional format string
333358
334359
For now, this does not use a format string since API may return date in ambiguous format :(
335360
336361
Args:
337-
s (str): target str
362+
s (str | None): target str, or None
338363
339364
Returns:
340-
datetime
365+
datetime or None
341366
"""
367+
if s is None:
368+
return None
342369
try:
343370
return dateutil.parser.parse(s)
344371
except ValueError:
345372
log.warning("Failed to parse date string: %s", s)
346373
return None
347374

348375

349-
def normalize_connector_config(config):
376+
def normalize_connector_config(config: dict[str, Any]) -> dict[str, Any]:
350377
"""Normalize connector config
351378
352379
This is porting of TD CLI's ConnectorConfigNormalizer#normalized_config.

0 commit comments

Comments
 (0)