1+ from __future__ import annotations
2+
13import csv
24import io
35import logging
46import warnings
7+ from collections .abc import Iterator
8+ from datetime import datetime
9+ from typing import Any , BinaryIO
510from urllib .parse import quote as urlquote
611
712import dateutil .parser
813import msgpack
914
15+ from tdclient .types import CSVValue , Converter , Record
16+
1017log = 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