diff --git a/CHANGELOG.md b/CHANGELOG.md index 47e9311e..bcdf0367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- `datacontract test` now supports testing HTTP APIs. + + ## [0.10.33] - 2025-07-29 ### Added diff --git a/README.md b/README.md index 49d07491..abcee558 100644 --- a/README.md +++ b/README.md @@ -376,6 +376,7 @@ Supported server types: - [kafka](#kafka) - [postgres](#postgres) - [trino](#trino) +- [api](#api) - [local](#local) Supported formats: @@ -802,6 +803,38 @@ models: | `DATACONTRACT_TRINO_PASSWORD` | `mysecretpassword` | Password | +#### API + +Data Contract CLI can test APIs that return data in JSON format. +Currently, only GET requests are supported. + +##### Example + +datacontract.yaml +```yaml +servers: + api: + type: "api" + location: "https://api.example.com/path" + delimiter: none # new_line, array, or none (default) + +models: + my_object: # corresponds to the root element of the JSON response + type: object + fields: + field1: + type: string + fields2: + type: number +``` + +##### Environment Variables + +| Environment Variable | Example | Description | +|-----------------------------------------|------------------|---------------------------------------------------| +| `DATACONTRACT_API_HEADER_AUTHORIZATION` | `Bearer ` | The value for the `authorization` header. Optional. | + + #### Local Data Contract CLI can test local files in parquet, json, csv, or delta format. diff --git a/datacontract/engines/data_contract_test.py b/datacontract/engines/data_contract_test.py index ec7b6c30..2b65e8d4 100644 --- a/datacontract/engines/data_contract_test.py +++ b/datacontract/engines/data_contract_test.py @@ -1,5 +1,9 @@ +import atexit +import os +import tempfile import typing +import requests from duckdb.duckdb import DuckDBPyConnection from datacontract.engines.data_contract_checks import create_checks @@ -46,6 +50,9 @@ def execute_data_contract_test( run.outputPortId = server.outputPortId run.server = server_name + if server.type == "api": + server = process_api_response(run, server) + run.checks.extend(create_checks(data_contract_specification, server)) # TODO check server is supported type for nicer error messages @@ -74,3 +81,33 @@ def get_server(data_contract_specification: DataContractSpecification, server_na server_name = list(data_contract_specification.servers.keys())[0] server = data_contract_specification.servers.get(server_name) return server + + +def process_api_response(run, server): + tmp_dir = tempfile.TemporaryDirectory(prefix="datacontract_cli_api_") + atexit.register(tmp_dir.cleanup) + headers = {} + if os.getenv("DATACONTRACT_API_HEADER_AUTHORIZATION") is not None: + headers["Authorization"] = os.getenv("DATACONTRACT_API_HEADER_AUTHORIZATION") + try: + response = requests.get(server.location, headers=headers) + response.raise_for_status() + except requests.exceptions.RequestException as e: + raise DataContractException( + type="connection", + name="API server connection error", + result=ResultEnum.error, + reason=f"Failed to fetch API response from {server.location}: {e}", + engine="datacontract", + ) + with open(f"{tmp_dir.name}/api_response.json", "w") as f: + f.write(response.text) + run.log_info(f"Saved API response to {tmp_dir.name}/api_response.json") + server = Server( + type="local", + format="json", + path=f"{tmp_dir.name}/api_response.json", + dataProductId=server.dataProductId, + outputPortId=server.outputPortId, + ) + return server diff --git a/datacontract/engines/fastjsonschema/check_jsonschema.py b/datacontract/engines/fastjsonschema/check_jsonschema.py index 5ea79caa..17e44d72 100644 --- a/datacontract/engines/fastjsonschema/check_jsonschema.py +++ b/datacontract/engines/fastjsonschema/check_jsonschema.py @@ -159,6 +159,14 @@ def process_json_file(run, schema, model_name, validate, file, delimiter): def process_local_file(run, server, schema, model_name, validate): path = server.path + if not path: + raise DataContractException( + type="schema", + name="Check that JSON has valid schema", + result=ResultEnum.warning, + reason="For server with type 'local', a 'path' must be defined.", + engine="datacontract", + ) if "{model}" in path: path = path.format(model=model_name) diff --git a/datacontract/engines/soda/connections/duckdb_connection.py b/datacontract/engines/soda/connections/duckdb_connection.py index 46c36cfc..d7c6a979 100644 --- a/datacontract/engines/soda/connections/duckdb_connection.py +++ b/datacontract/engines/soda/connections/duckdb_connection.py @@ -71,6 +71,9 @@ def get_duckdb_connection( elif server.format == "delta": con.sql("update extensions;") # Make sure we have the latest delta extension con.sql(f"""CREATE VIEW "{model_name}" AS SELECT * FROM delta_scan('{model_path}');""") + table_info = con.sql(f"PRAGMA table_info('{model_name}');").fetchdf() + if table_info is not None and not table_info.empty: + run.log_info(f"DuckDB Table Info: {table_info.to_string(index=False)}") return con diff --git a/tests/fixtures/api/request.txt b/tests/fixtures/api/request.txt new file mode 100644 index 00000000..46149a96 --- /dev/null +++ b/tests/fixtures/api/request.txt @@ -0,0 +1 @@ +https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t=temperature_2m,wind_speed_10m&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m \ No newline at end of file diff --git a/tests/fixtures/api/response.json b/tests/fixtures/api/response.json new file mode 100644 index 00000000..15e56649 --- /dev/null +++ b/tests/fixtures/api/response.json @@ -0,0 +1,202 @@ +{ + "latitude": 52.52, + "longitude": 13.419998, + "generationtime_ms": 0.141263008117676, + "utc_offset_seconds": 0, + "timezone": "GMT", + "timezone_abbreviation": "GMT", + "elevation": 38, + "current_units": { + "time": "iso8601", + "interval": "seconds", + "temperature_2m": "°C", + "wind_speed_10m": "km/h" + }, + "current": { + "time": "2025-08-01T11:15", + "interval": 900, + "temperature_2m": 19.1, + "wind_speed_10m": 11.1 + }, + "hourly_units": { + "time": "iso8601", + "temperature_2m": "°C", + "relative_humidity_2m": "%", + "wind_speed_10m": "km/h" + }, + "hourly": { + "time": [ + "2025-08-01T00:00", + "2025-08-01T01:00", + "2025-08-01T02:00", + "2025-08-01T03:00", + "2025-08-01T04:00", + "2025-08-01T05:00", + "2025-08-01T06:00", + "2025-08-01T07:00", + "2025-08-01T08:00", + "2025-08-01T09:00", + "2025-08-01T10:00", + "2025-08-01T11:00", + "2025-08-01T12:00", + "2025-08-01T13:00", + "2025-08-01T14:00", + "2025-08-01T15:00", + "2025-08-01T16:00", + "2025-08-01T17:00", + "2025-08-01T18:00", + "2025-08-01T19:00", + "2025-08-01T20:00", + "2025-08-01T21:00", + "2025-08-01T22:00", + "2025-08-01T23:00", + "2025-08-02T00:00", + "2025-08-02T01:00", + "2025-08-02T02:00", + "2025-08-02T03:00", + "2025-08-02T04:00", + "2025-08-02T05:00", + "2025-08-02T06:00", + "2025-08-02T07:00", + "2025-08-02T08:00", + "2025-08-02T09:00", + "2025-08-02T10:00", + "2025-08-02T11:00", + "2025-08-02T12:00", + "2025-08-02T13:00", + "2025-08-02T14:00", + "2025-08-02T15:00", + "2025-08-02T16:00", + "2025-08-02T17:00", + "2025-08-02T18:00", + "2025-08-02T19:00", + "2025-08-02T20:00", + "2025-08-02T21:00", + "2025-08-02T22:00", + "2025-08-02T23:00", + "2025-08-03T00:00", + "2025-08-03T01:00", + "2025-08-03T02:00", + "2025-08-03T03:00", + "2025-08-03T04:00", + "2025-08-03T05:00", + "2025-08-03T06:00", + "2025-08-03T07:00", + "2025-08-03T08:00", + "2025-08-03T09:00", + "2025-08-03T10:00", + "2025-08-03T11:00", + "2025-08-03T12:00", + "2025-08-03T13:00", + "2025-08-03T14:00", + "2025-08-03T15:00", + "2025-08-03T16:00", + "2025-08-03T17:00", + "2025-08-03T18:00", + "2025-08-03T19:00", + "2025-08-03T20:00", + "2025-08-03T21:00", + "2025-08-03T22:00", + "2025-08-03T23:00", + "2025-08-04T00:00", + "2025-08-04T01:00", + "2025-08-04T02:00", + "2025-08-04T03:00", + "2025-08-04T04:00", + "2025-08-04T05:00", + "2025-08-04T06:00", + "2025-08-04T07:00", + "2025-08-04T08:00", + "2025-08-04T09:00", + "2025-08-04T10:00", + "2025-08-04T11:00", + "2025-08-04T12:00", + "2025-08-04T13:00", + "2025-08-04T14:00", + "2025-08-04T15:00", + "2025-08-04T16:00", + "2025-08-04T17:00", + "2025-08-04T18:00", + "2025-08-04T19:00", + "2025-08-04T20:00", + "2025-08-04T21:00", + "2025-08-04T22:00", + "2025-08-04T23:00", + "2025-08-05T00:00", + "2025-08-05T01:00", + "2025-08-05T02:00", + "2025-08-05T03:00", + "2025-08-05T04:00", + "2025-08-05T05:00", + "2025-08-05T06:00", + "2025-08-05T07:00", + "2025-08-05T08:00", + "2025-08-05T09:00", + "2025-08-05T10:00", + "2025-08-05T11:00", + "2025-08-05T12:00", + "2025-08-05T13:00", + "2025-08-05T14:00", + "2025-08-05T15:00", + "2025-08-05T16:00", + "2025-08-05T17:00", + "2025-08-05T18:00", + "2025-08-05T19:00", + "2025-08-05T20:00", + "2025-08-05T21:00", + "2025-08-05T22:00", + "2025-08-05T23:00", + "2025-08-06T00:00", + "2025-08-06T01:00", + "2025-08-06T02:00", + "2025-08-06T03:00", + "2025-08-06T04:00", + "2025-08-06T05:00", + "2025-08-06T06:00", + "2025-08-06T07:00", + "2025-08-06T08:00", + "2025-08-06T09:00", + "2025-08-06T10:00", + "2025-08-06T11:00", + "2025-08-06T12:00", + "2025-08-06T13:00", + "2025-08-06T14:00", + "2025-08-06T15:00", + "2025-08-06T16:00", + "2025-08-06T17:00", + "2025-08-06T18:00", + "2025-08-06T19:00", + "2025-08-06T20:00", + "2025-08-06T21:00", + "2025-08-06T22:00", + "2025-08-06T23:00", + "2025-08-07T00:00", + "2025-08-07T01:00", + "2025-08-07T02:00", + "2025-08-07T03:00", + "2025-08-07T04:00", + "2025-08-07T05:00", + "2025-08-07T06:00", + "2025-08-07T07:00", + "2025-08-07T08:00", + "2025-08-07T09:00", + "2025-08-07T10:00", + "2025-08-07T11:00", + "2025-08-07T12:00", + "2025-08-07T13:00", + "2025-08-07T14:00", + "2025-08-07T15:00", + "2025-08-07T16:00", + "2025-08-07T17:00", + "2025-08-07T18:00", + "2025-08-07T19:00", + "2025-08-07T20:00", + "2025-08-07T21:00", + "2025-08-07T22:00", + "2025-08-07T23:00" + ], + "temperature_2m": [16.3, 16.2, 16.2, 15.8, 15.6, 15.6, 16.2, 16.9, 17.6, 18.2, 18.9, 18.8, 20.1, 19.5, 19.4, 19.6, 19.9, 19.9, 19.8, 19.3, 18.8, 18, 17.6, 16.8, 16.3, 16.1, 15.9, 15.6, 15.4, 15.5, 16, 17.1, 17.7, 18.6, 19.4, 20.3, 20.9, 21.6, 22.2, 22.2, 22, 22, 21.6, 20.9, 20.2, 19.3, 18.8, 18, 17.5, 17.1, 16.4, 15.8, 15.3, 15.2, 16.2, 17, 17.5, 17.6, 18.1, 18.9, 19.8, 20.3, 20.1, 19.9, 19.3, 18.9, 18.5, 17.7, 17.2, 16.6, 16.2, 15.9, 15.7, 15.5, 15.4, 15.2, 15.1, 15.3, 15.6, 15.8, 16.2, 17, 18.6, 20.5, 22, 22.9, 23.4, 23.6, 23.4, 22.9, 22.2, 21.3, 20.3, 19.4, 18.9, 18.6, 18.3, 17.9, 17.6, 17.3, 17, 16.7, 16.8, 17.7, 19.1, 20.1, 20.6, 20.8, 21, 21.5, 22, 22.2, 21.8, 21, 20.1, 19.1, 18, 17, 16.1, 15.4, 14.9, 14.4, 14, 13.8, 13.8, 14, 14.6, 15.6, 17, 18.3, 19.5, 20.7, 21.4, 21.6, 21.4, 21, 20.6, 20.1, 19.5, 18.6, 17.5, 16.6, 15.8, 15.1, 14.5, 13.8, 13.3, 13, 13.2, 13.7, 14.6, 16, 17.8, 19.5, 20.9, 22.2, 23.3, 24, 24.4, 24.6, 24.4, 23.9, 23.2, 22.2, 20.9, 19.8, 19.3, 19.1], + "relative_humidity_2m": [81, 83, 88, 90, 91, 90, 86, 81, 77, 74, 68, 75, 65, 67, 66, 67, 61, 64, 64, 68, 74, 77, 77, 83, 87, 90, 90, 91, 91, 90, 86, 75, 73, 67, 63, 58, 55, 54, 48, 51, 52, 51, 51, 57, 63, 68, 71, 73, 73, 75, 79, 83, 86, 88, 85, 81, 80, 80, 75, 73, 61, 52, 57, 57, 54, 56, 59, 65, 68, 71, 77, 79, 81, 83, 83, 84, 84, 84, 84, 85, 87, 85, 78, 68, 60, 55, 52, 51, 51, 53, 55, 59, 63, 67, 70, 72, 74, 74, 73, 73, 78, 84, 86, 79, 67, 58, 54, 52, 49, 44, 38, 35, 37, 41, 46, 50, 55, 59, 63, 67, 69, 72, 73, 74, 75, 75, 73, 68, 62, 56, 51, 47, 44, 44, 45, 47, 49, 52, 56, 61, 68, 73, 77, 80, 83, 86, 89, 90, 89, 87, 83, 76, 68, 60, 54, 50, 46, 43, 41, 41, 42, 45, 49, 54, 59, 63, 64, 64], + "wind_speed_10m": [6.2, 6.9, 7, 6.9, 6.5, 5.1, 7.2, 8.4, 9.4, 8.9, 8.9, 10.9, 10.2, 11.3, 12.2, 13, 9.4, 9, 7.1, 6.7, 6.2, 6.6, 7.1, 8, 7, 6.8, 7.1, 7.1, 7.7, 9.3, 10.8, 9, 10.8, 10.8, 12.3, 11.9, 12.2, 10.8, 9, 9.2, 10.1, 8.9, 7.6, 4.7, 2.6, 0.5, 1.8, 4.6, 3.6, 5.2, 5.4, 5.4, 4.8, 5.8, 10.4, 7.2, 8.7, 8.8, 10.1, 11.2, 12.6, 12.2, 11.9, 12.8, 12.4, 11.4, 9.1, 7.1, 6.6, 8, 8.3, 8.2, 8, 7.8, 8.1, 7.4, 7.2, 9, 13, 10.9, 8.3, 7.7, 9.4, 11.2, 12.6, 13, 12.6, 11.6, 9.8, 7.6, 5.8, 4.9, 5.2, 5.6, 5.9, 5.6, 5.9, 6.5, 7.2, 8, 8.9, 9.4, 10, 10.9, 13.3, 15.8, 16.9, 17.5, 17.3, 17.3, 16.6, 16.2, 15.5, 15.2, 14.6, 13.8, 13.3, 13, 13, 13.3, 13.7, 13.1, 12.8, 12.9, 13, 13.5, 14.1, 14.9, 16.2, 17.4, 17.8, 17.7, 17.7, 17.8, 17.7, 17.1, 15.7, 13.8, 11.9, 9.8, 7.9, 6.9, 6.6, 6.3, 6.3, 5.9, 5.4, 5, 4.7, 4.8, 5.2, 6.2, 7.1, 7.8, 7.2, 6.3, 5.4, 5.4, 5.8, 6.5, 6.1, 4.9, 4.1, 4.2, 4.9, 5.2, 4.8, 4.1] + } +} \ No newline at end of file diff --git a/tests/fixtures/api/weather-service.odcs.yaml b/tests/fixtures/api/weather-service.odcs.yaml new file mode 100644 index 00000000..724e8431 --- /dev/null +++ b/tests/fixtures/api/weather-service.odcs.yaml @@ -0,0 +1,328 @@ +version: "1.0.0" +kind: "DataContract" +apiVersion: "v3.0.2" +id: "weather-data-berlin-hourly-001" +name: "Berlin Weather Data - Hourly Forecast" +status: "active" +tenant: "weather-services" +domain: "meteorology" +dataProduct: "Weather Analytics Platform" + +description: + purpose: "Provides hourly weather forecast data for Berlin, Germany including temperature, humidity, and wind speed measurements for operational and analytical purposes" + usage: "This dataset is intended for weather analysis, forecasting models, and operational decision making. Data should be refreshed hourly for accurate predictions." + limitations: "Data accuracy depends on meteorological conditions and sensor reliability. Historical data beyond 7 days may have reduced accuracy. Use appropriate error handling for missing data points." + +servers: + - server: "weather-api-prod" + type: "api" + location: "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t=temperature_2m,wind_speed_10m&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m" + description: "Open-Meteo Weather API production server" + environment: "prod" + +tags: ["weather", "forecast", "meteorology", "berlin", "germany", "temperature", "humidity", "wind", "hourly"] + +team: + - username: "weather-team@company.com" + name: "Weather Data Team" + role: "owner" + description: "Responsible for weather data ingestion, quality, and availability" + +support: + - channel: "weather-data-support" + url: "mailto:weather-team@company.com" + tool: "email" + scope: "issues" + description: "Technical support for weather data issues" + +schema: + - name: "weather_forecast" + physicalType: "api" + logicalType: "object" + description: "Complete weather forecast response containing location, current conditions, and hourly predictions" + dataGranularityDescription: "Hourly weather measurements for Berlin, Germany with 7-day forecast horizon" + properties: + - name: "latitude" + logicalType: "number" + description: "Latitude coordinate in decimal degrees" + required: true + logicalTypeOptions: + minimum: -90 + maximum: 90 + examples: [52.52] + + - name: "longitude" + logicalType: "number" + description: "Longitude coordinate in decimal degrees" + required: true + logicalTypeOptions: + minimum: -180 + maximum: 180 + examples: [13.419998] + + - name: "generationtime_ms" + physicalType: "FLOAT" + logicalType: "number" + description: "API response generation time in milliseconds" + required: false + logicalTypeOptions: + minimum: 0 + examples: [0.141263008117676] + + - name: "utc_offset_seconds" + physicalType: "INTEGER" + logicalType: "integer" + description: "UTC offset in seconds" + required: true + examples: [0] + + - name: "timezone" + physicalType: "VARCHAR(50)" + logicalType: "string" + description: "Timezone identifier" + required: true + logicalTypeOptions: + maxLength: 50 + examples: ["GMT"] + + - name: "timezone_abbreviation" + physicalType: "VARCHAR(10)" + logicalType: "string" + description: "Timezone abbreviation" + required: true + logicalTypeOptions: + maxLength: 10 + examples: ["GMT"] + + - name: "elevation" + physicalType: "INTEGER" + logicalType: "integer" + description: "Elevation above sea level in meters" + required: true + logicalTypeOptions: + minimum: -500 + maximum: 9000 + examples: [38] + + - name: "current_units" + physicalType: "OBJECT" + logicalType: "object" + description: "Units of measurement for current weather data" + required: true + properties: + - name: "time" + physicalType: "VARCHAR(20)" + logicalType: "string" + description: "Time format specification" + required: true + examples: ["iso8601"] + + - name: "interval" + physicalType: "VARCHAR(20)" + logicalType: "string" + description: "Time interval unit" + required: true + examples: ["seconds"] + + - name: "temperature_2m" + physicalType: "VARCHAR(5)" + logicalType: "string" + description: "Temperature unit" + required: true + examples: ["°C"] + + - name: "wind_speed_10m" + physicalType: "VARCHAR(10)" + logicalType: "string" + description: "Wind speed unit" + required: true + examples: ["km/h"] + + - name: "current" + physicalType: "OBJECT" + logicalType: "object" + description: "Current weather conditions" + required: true + properties: + - name: "time" + physicalType: "string" + logicalType: "string" + description: "Current observation time in ISO8601 format" + required: true +# logicalTypeOptions: +# format: "yyyy-MM-ddTHH:mm:ss" + examples: ["2025-08-01T11:15"] + + - name: "interval" + physicalType: "INTEGER" + logicalType: "integer" + description: "Measurement interval in seconds" + required: true + examples: [900] + + - name: "temperature_2m" + physicalType: "FLOAT" + logicalType: "number" + description: "Air temperature at 2 meters above ground in Celsius" + required: true + logicalTypeOptions: + minimum: -50 + maximum: 60 + format: "f32" + examples: [19.1] + + - name: "wind_speed_10m" + physicalType: "FLOAT" + logicalType: "number" + description: "Wind speed at 10 meters above ground in km/h" + required: true + logicalTypeOptions: + minimum: 0 + maximum: 200 + format: "f32" + examples: [11.1] + + - name: "hourly_units" + physicalType: "OBJECT" + logicalType: "object" + description: "Units of measurement for hourly weather data" + required: true + properties: + - name: "time" + physicalType: "VARCHAR(20)" + logicalType: "string" + description: "Time format specification" + required: true + examples: ["iso8601"] + + - name: "temperature_2m" + physicalType: "VARCHAR(5)" + logicalType: "string" + description: "Temperature unit" + required: true + examples: ["°C"] + + - name: "relative_humidity_2m" + physicalType: "VARCHAR(5)" + logicalType: "string" + description: "Humidity unit" + required: true + examples: ["%"] + + - name: "wind_speed_10m" + physicalType: "VARCHAR(10)" + logicalType: "string" + description: "Wind speed unit" + required: true + examples: ["km/h"] + + - name: "hourly" + physicalType: "OBJECT" + logicalType: "object" + description: "Hourly weather forecast data" + required: true + properties: + - name: "time" + physicalType: "ARRAY" + logicalType: "array" + description: "Array of hourly timestamps in ISO8601 format" + required: true + logicalTypeOptions: + minItems: 1 + maxItems: 168 + items: + logicalType: "string" + physicalType: "string" + examples: ["2025-08-01T00:00", "2025-08-01T01:00"] + + - name: "temperature_2m" + physicalType: "ARRAY" + logicalType: "array" + description: "Hourly air temperature at 2 meters above ground in Celsius" + required: true + logicalTypeOptions: + minItems: 1 + maxItems: 168 + items: + logicalType: "number" + logicalTypeOptions: + minimum: -50 + maximum: 60 + examples: [16.3, 16.2, 16.2] + + - name: "relative_humidity_2m" + physicalType: "ARRAY" + logicalType: "array" + description: "Hourly relative humidity at 2 meters above ground in percentage" + required: true + logicalTypeOptions: + minItems: 1 + maxItems: 168 + items: + logicalType: "integer" + logicalTypeOptions: + minimum: 0 + maximum: 100 + examples: [81, 83, 88] + + - name: "wind_speed_10m" + physicalType: "ARRAY" + logicalType: "array" + description: "Hourly wind speed at 10 meters above ground in km/h" + required: true + logicalTypeOptions: + minItems: 1 + maxItems: 168 + items: + logicalType: "number" + logicalTypeOptions: + minimum: 0 + maximum: 200 + format: "f32" + examples: [6.2, 6.9, 7.0] + +# quality: +# - name: "array_length_consistency" +# description: "All hourly arrays must have matching lengths" +# dimension: "consistency" +# type: "sql" +# query: | +# SELECT +# CASE +# WHEN +# len(hourly -> 'time') = len(hourly -> 'temperature_2m') +# AND len(hourly -> 'temperature_2m') = len(hourly -> 'relative_humidity_2m') +# AND len(hourly -> 'relative_humidity_2m') = len(hourly -> 'wind_speed_10m') +# THEN 1 +# ELSE 0 +# END AS arrays_equal +# FROM {model} +# mustBe: 1 +# severity: "error" + +slaProperties: + - property: "availability" + value: 99.5 + unit: "percent" + element: "weather_forecast" + driver: "operational" + + - property: "retention" + value: 90 + unit: "days" + element: "weather_forecast" + driver: "operational" + + - property: "freshness" + value: 15 + unit: "minutes" + element: "weather_forecast.current" + driver: "operational" + + - property: "latency" + value: 500 + unit: "ms" + element: "weather_forecast" + driver: "operational" + +contractCreatedTs: "2025-08-01T11:15:00Z" \ No newline at end of file diff --git a/tests/test_test_api.py b/tests/test_test_api.py new file mode 100644 index 00000000..d51e0eb3 --- /dev/null +++ b/tests/test_test_api.py @@ -0,0 +1,55 @@ +import json +import logging +from unittest.mock import MagicMock, patch + +from datacontract.data_contract import DataContract + +logging.basicConfig(level=logging.DEBUG, force=True) + + +datacontract = "fixtures/api/weather-service.odcs.yaml" + + +def mock_get(*args, **kwargs): + mock_response = MagicMock() + mock_response.status_code = 200 + json_data = { + "latitude": 52.52, + "longitude": 13.419998, + "generationtime_ms": 0.141263008117676, + "utc_offset_seconds": 0, + "timezone": "GMT", + "timezone_abbreviation": "GMT", + "elevation": 38, + "current_units": {"time": "iso8601", "interval": "seconds", "temperature_2m": "°C", "wind_speed_10m": "km/h"}, + "current": {"time": "2025-08-01T11:15", "interval": 900, "temperature_2m": 19.1, "wind_speed_10m": 11.1}, + "hourly_units": { + "time": "iso8601", + "temperature_2m": "°C", + "relative_humidity_2m": "%", + "wind_speed_10m": "km/h", + }, + "hourly": { + "time": ["2025-08-01T00:00", "2025-08-01T01:00"], + "temperature_2m": [16.3, 16.2], + "relative_humidity_2m": [81, 83], + "wind_speed_10m": [6.2, 6.9], + }, + } + mock_response.json.return_value = json_data + mock_response.text = json.dumps(json_data) + mock_response.raise_for_status.return_value = None + return mock_response + + +@patch("datacontract.engines.data_contract_test.requests.get", side_effect=mock_get) +def test_test_api(mock_get, monkeypatch): + with open(datacontract) as data_contract_file: + data_contract_str = data_contract_file.read() + data_contract = DataContract(data_contract_str=data_contract_str) + + run = data_contract.test() + + print(run) + assert run.result == "passed" + assert all(check.result == "passed" for check in run.checks)