diff --git a/README.md b/README.md index 09b4fc1..e0f8207 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,10 @@ The client exposes four resource groups — all endpoints, parameters, and DTOs | `client.environments` | `list()`, `get()`, `create()`, `delete()`, `create_entry()`, `delete_entry()` | | `client.calculations` | `daily_values()`, `extrapolations()` | +Starting with version 3.0.0, meter-reading CSV exports default to semicolon (`;`). +To preserve the comma-separated output of version 2.x, explicitly set +`delimiter=CsvDelimiter.COMMA` in `ExportMeterReadingsDto`. + ## Configuration ```python diff --git a/contracts/README.md b/contracts/README.md index 963626d..1896390 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -18,6 +18,8 @@ test and release checks. No API token, live backend or sibling checkout is neede header names case-insensitively; additional transport headers are allowed. Query key order and JSON object key order are irrelevant; extra query/body fields and duplicate query keys are not allowed. +- Meter-reading CSV exports default to `semicolon`. Explicit `comma`, `semicolon` + and `tab` selections override the default. - `response` is the local server's synthetic HTTP response. Bodies use exactly one of `json`, UTF-8 `text`, or `base64`; `{}` means no body. A `json: null` body is distinct from no body. Base64 preserves exact CSV bytes, including BOM diff --git a/contracts/cases/errors.json b/contracts/cases/errors.json index 3eaaf8c..898462b 100644 --- a/contracts/cases/errors.json +++ b/contracts/cases/errors.json @@ -194,7 +194,7 @@ "body": { "json": { "includeHeader": true, - "delimiter": "comma", + "delimiter": "semicolon", "dateFormat": "iso", "columns": [ "date", @@ -1285,7 +1285,7 @@ "body": { "json": { "includeHeader": true, - "delimiter": "comma", + "delimiter": "semicolon", "dateFormat": "iso", "columns": [ "date", diff --git a/contracts/cases/meter-readings.json b/contracts/cases/meter-readings.json index 274a4e3..b2d8c7b 100644 --- a/contracts/cases/meter-readings.json +++ b/contracts/cases/meter-readings.json @@ -352,7 +352,7 @@ "body": { "json": { "includeHeader": true, - "delimiter": "comma", + "delimiter": "semicolon", "dateFormat": "iso", "columns": [ "date", @@ -367,12 +367,12 @@ "Content-Type": "text/csv; charset=utf-8" }, "body": { - "base64": "77u/ZGF0ZSx2YWx1ZQ0KMjAyNi0wOS0wMVQxMDozNDo1Ni4xMjNaLDEyMy40NQ0K" + "base64": "77u/ZGF0ZTt2YWx1ZQ0KMjAyNi0wOS0wMVQxMDozNDo1Ni4xMjNaOzEyMy40NQ0K" } }, "expected": { "result": { - "base64": "77u/ZGF0ZSx2YWx1ZQ0KMjAyNi0wOS0wMVQxMDozNDo1Ni4xMjNaLDEyMy40NQ0K" + "base64": "77u/ZGF0ZTt2YWx1ZQ0KMjAyNi0wOS0wMVQxMDozNDo1Ni4xMjNaOzEyMy40NQ0K" } } }, @@ -462,7 +462,7 @@ "body": { "json": { "includeHeader": false, - "delimiter": "comma", + "delimiter": "semicolon", "dateFormat": "iso", "columns": [ "value" @@ -484,6 +484,102 @@ "base64": "MTIz" } } + }, + { + "id": "readings-export-explicit-comma", + "operation": "meterReadings.export", + "input": { + "deviceId": "11111111-1111-4111-8111-111111111111", + "config": { + "columns": [ + "date", + "value" + ], + "delimiter": "comma" + } + }, + "request": { + "method": "POST", + "path": "/v3/devices/standard/11111111-1111-4111-8111-111111111111/meter-readings/export", + "query": {}, + "headers": { + "Authorization": "Bearer contract-test-token", + "Content-Type": "application/json" + }, + "body": { + "json": { + "includeHeader": true, + "delimiter": "comma", + "dateFormat": "iso", + "columns": [ + "date", + "value" + ] + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "text/csv; charset=utf-8" + }, + "body": { + "base64": "77u/ZGF0ZSx2YWx1ZQ0KMjAyNi0wOS0wMVQxMDozNDo1Ni4xMjNaLDEyMy40NQ0K" + } + }, + "expected": { + "result": { + "base64": "77u/ZGF0ZSx2YWx1ZQ0KMjAyNi0wOS0wMVQxMDozNDo1Ni4xMjNaLDEyMy40NQ0K" + } + } + }, + { + "id": "readings-export-explicit-tab", + "operation": "meterReadings.export", + "input": { + "deviceId": "11111111-1111-4111-8111-111111111111", + "config": { + "columns": [ + "date", + "value" + ], + "delimiter": "tab" + } + }, + "request": { + "method": "POST", + "path": "/v3/devices/standard/11111111-1111-4111-8111-111111111111/meter-readings/export", + "query": {}, + "headers": { + "Authorization": "Bearer contract-test-token", + "Content-Type": "application/json" + }, + "body": { + "json": { + "includeHeader": true, + "delimiter": "tab", + "dateFormat": "iso", + "columns": [ + "date", + "value" + ] + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "text/csv; charset=utf-8" + }, + "body": { + "base64": "77u/ZGF0ZQl2YWx1ZQ0KMjAyNi0wOS0wMVQxMDozNDo1Ni4xMjNaCTEyMy40NQ0K" + } + }, + "expected": { + "result": { + "base64": "77u/ZGF0ZQl2YWx1ZQ0KMjAyNi0wOS0wMVQxMDozNDo1Ni4xMjNaCTEyMy40NQ0K" + } + } } ] } diff --git a/energy_tracker_api/models/meter_readings.py b/energy_tracker_api/models/meter_readings.py index 51a7fb8..55d850a 100644 --- a/energy_tracker_api/models/meter_readings.py +++ b/energy_tracker_api/models/meter_readings.py @@ -113,13 +113,13 @@ class ExportMeterReadingsDto: Attributes: columns: Columns and their order in the exported CSV file. include_header: Whether to include the header row (default: True). - delimiter: CSV delimiter (default: comma). + delimiter: CSV delimiter (default: semicolon). date_format: Date format in the export (default: iso). """ columns: list[ExportColumn] include_header: bool = True - delimiter: CsvDelimiter = CsvDelimiter.COMMA + delimiter: CsvDelimiter = CsvDelimiter.SEMICOLON date_format: DateFormat = DateFormat.ISO def _to_dict(self) -> dict: diff --git a/pyproject.toml b/pyproject.toml index d9f8c96..d07f16d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "energy-tracker-api" -version = "2.1.0" +version = "3.0.0" description = "Async Python client for Energy Tracker API" readme = "README.md" requires-python = ">=3.14" diff --git a/tests/test_models.py b/tests/test_models.py index 2104c82..399291b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -226,7 +226,7 @@ def test_to_dict_with_defaults(self): assert result == { "columns": ["date", "value"], "includeHeader": True, - "delimiter": "comma", + "delimiter": "semicolon", "dateFormat": "iso", } @@ -235,7 +235,7 @@ def test_to_dict_with_custom_options(self): dto = ExportMeterReadingsDto( columns=[ExportColumn.DATE, ExportColumn.VALUE, ExportColumn.NOTE], include_header=False, - delimiter=CsvDelimiter.SEMICOLON, + delimiter=CsvDelimiter.COMMA, date_format=DateFormat.UNIX, ) @@ -246,7 +246,7 @@ def test_to_dict_with_custom_options(self): assert result == { "columns": ["date", "value", "note"], "includeHeader": False, - "delimiter": "semicolon", + "delimiter": "comma", "dateFormat": "unix", } diff --git a/tests/test_resources.py b/tests/test_resources.py index b47dc78..cbf5dc5 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -369,7 +369,7 @@ class TestMeterReadingResourceExport: async def test_export_minimal(self): # Arrange client = Mock(spec=EnergyTrackerClient) - client._make_request = AsyncMock(return_value=b"date,value\n2024-01-15,123.45\n") + client._make_request = AsyncMock(return_value=b"date;value\n2024-01-15;123.45\n") resource = MeterReadingResource(client) config = ExportMeterReadingsDto(columns=[ExportColumn.DATE, ExportColumn.VALUE]) @@ -377,7 +377,7 @@ async def test_export_minimal(self): result = await resource.export(device_id="device-123", export_config=config) # Assert - assert result == b"date,value\n2024-01-15,123.45\n" + assert result == b"date;value\n2024-01-15;123.45\n" client._make_request.assert_called_once_with( method="POST", endpoint="/v3/devices/standard/device-123/meter-readings/export", @@ -385,7 +385,7 @@ async def test_export_minimal(self): json={ "columns": ["date", "value"], "includeHeader": True, - "delimiter": "comma", + "delimiter": "semicolon", "dateFormat": "iso", }, params=None, @@ -417,7 +417,7 @@ async def test_export_with_filters(self): json={ "columns": ["date", "value"], "includeHeader": True, - "delimiter": "comma", + "delimiter": "semicolon", "dateFormat": "iso", }, params={