From 22f9ed8705523ddb9612cdd768ba11f30d17b0ce Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Fri, 25 Sep 2026 10:50:52 +0200 Subject: [PATCH] Fix limit validation and send limit/format in Tables.export_raw The limit check compared the limit against isinstance(table_id, int), so every non-None limit raised ValueError. On top of that, neither limit nor format were ever added to the request body, so even a valid limit would have been silently ignored and the whole table exported. - validate limit as a positive int (bool excluded) - send limit and format in the export-async request - add mock tests asserting the posted body Fixes #66 Supersedes #78 Co-Authored-By: Claude Fable 5.1 --- kbcstorage/tables.py | 7 ++++-- tests/mocks/test_tables.py | 44 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/kbcstorage/tables.py b/kbcstorage/tables.py index 5285fb1..f1a505a 100644 --- a/kbcstorage/tables.py +++ b/kbcstorage/tables.py @@ -508,10 +508,13 @@ def export_raw(self, table_id, limit=None, file_format='rfc', } if not isinstance(table_id, str) or table_id == '': raise ValueError("Invalid table_id '{}'.".format(table_id)) - if limit is not None and limit is not isinstance(table_id, int): - raise ValueError("Invalid limit '{}'.".format(limit)) + if limit is not None: + if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1: + raise ValueError("Invalid limit '{}'.".format(limit)) + params['limit'] = limit if file_format not in ('rfc', 'escaped', 'raw'): raise ValueError("Invalid format '{}'.".format(file_format)) + params['format'] = file_format if changed_since is not None: if not isinstance(changed_since, str): raise ValueError("Invalid changed_since '{}'.". diff --git a/tests/mocks/test_tables.py b/tests/mocks/test_tables.py index 4b6dbee..86f5751 100644 --- a/tests/mocks/test_tables.py +++ b/tests/mocks/test_tables.py @@ -2,6 +2,7 @@ Test basic functionality of the Tables endpoint """ import unittest +from urllib.parse import parse_qs import responses @@ -33,3 +34,46 @@ def test_list(self): ) tables_list = self.tables.list() assert isinstance(tables_list, list) + + @responses.activate + def test_export_raw_sends_limit_and_format(self): + """ + export_raw passes limit and format to the export-async request + """ + responses.add( + responses.Response( + method='POST', + url='https://connection.keboola.com/v2/storage/tables/in.c-bucket.table/export-async', + json={'id': 1, 'status': 'waiting'} + ) + ) + self.tables.export_raw('in.c-bucket.table', limit=10, file_format='escaped') + request_body = parse_qs(responses.calls[0].request.body, keep_blank_values=True) + assert request_body['limit'] == ['10'] + assert request_body['format'] == ['escaped'] + + @responses.activate + def test_export_raw_omits_limit_when_not_set(self): + """ + export_raw does not send limit when it is None + """ + responses.add( + responses.Response( + method='POST', + url='https://connection.keboola.com/v2/storage/tables/in.c-bucket.table/export-async', + json={'id': 1, 'status': 'waiting'} + ) + ) + self.tables.export_raw('in.c-bucket.table') + request_body = parse_qs(responses.calls[0].request.body, keep_blank_values=True) + assert 'limit' not in request_body + assert request_body['format'] == ['rfc'] + + def test_export_raw_rejects_invalid_limit(self): + """ + export_raw raises ValueError for a limit that is not a positive int + """ + for invalid_limit in ('10', 10.5, True, 0, -1): + with self.subTest(limit=invalid_limit): + with self.assertRaises(ValueError): + self.tables.export_raw('in.c-bucket.table', limit=invalid_limit)