From e8fb9f92e4a64fca817f719ea7c0e43868daeaf3 Mon Sep 17 00:00:00 2001 From: zemion Date: Thu, 21 May 2026 02:51:43 +0200 Subject: [PATCH] initial untested XML commit --- .gitignore | 4 +- LICENSE.md => LICENSE | 0 README.md | 22 +- merge_providers.py | 500 +++++++++++++++++++++++++ providers/atmosfair/atmosfair.xml | 68 ++++ providers/calco2lato/calco2lato.xml | 75 ++-- providers/clevel/clevel.xml | 36 ++ providers/goclimate/goclimate.xml | 51 +++ providers/google_tim/google_tim.xml | 56 +-- providers/klimalink/klimalink.xml | 48 +++ providers/klimapi/klimapi_v1.xml | 147 ++++++++ providers/klimapi/klimapi_v2.xml | 114 ++++++ providers/myclimate/myclimate_bulk.xml | 45 +++ providers/myclimate/myclimate_v1.xml | 38 ++ providers/myclimate/myclimate_v2.xml | 36 ++ 15 files changed, 1175 insertions(+), 65 deletions(-) rename LICENSE.md => LICENSE (100%) create mode 100644 merge_providers.py create mode 100644 providers/atmosfair/atmosfair.xml create mode 100644 providers/clevel/clevel.xml create mode 100644 providers/goclimate/goclimate.xml create mode 100644 providers/klimalink/klimalink.xml create mode 100644 providers/klimapi/klimapi_v1.xml create mode 100644 providers/klimapi/klimapi_v2.xml create mode 100644 providers/myclimate/myclimate_bulk.xml create mode 100644 providers/myclimate/myclimate_v1.xml create mode 100644 providers/myclimate/myclimate_v2.xml diff --git a/.gitignore b/.gitignore index bc08a77..5b704cd 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,4 @@ # Built Visual Studio Code Extensions *.vsix -iso*.csv -domains_operations.txt -merge_providers.py \ No newline at end of file +sources/** diff --git a/LICENSE.md b/LICENSE similarity index 100% rename from LICENSE.md rename to LICENSE diff --git a/README.md b/README.md index 3246f9f..d50053f 100644 --- a/README.md +++ b/README.md @@ -63,17 +63,17 @@ The specification is expected to support provider capability discovery instead o ## Supported providers -| Provider | API key requested | API key received | API tested | Supported | -| :------------------------------------------------ | :---------------: | :--------------: | :--------: | :-------: | -| Atmosfair Webservice 5 | ❌ | | | | -| Google Travel Impact Model (various versions) | ❌ | | | | -| myclimate Flight Calculator V1 & V2 | ❌ | | | | -| myclimate Bulk Flight Calculator | ❌ | | | | -| GoClimate Flight Footprint | ❌ | | | | -| C-Level Carbon Balance API | ❌ | | | | -| KlimaLink API | ❌ | | | | -| KlimAPI Calculation & Compensation API (v1 & v2) | ❌ | | | | -| calco2la.to | ✅ | ✅ | ✅ | ✅ | +| Provider | XML | API key requested | API key received | API tested | Supported | +| :------------------------------------------------ | :------: | :---------------: | :--------------: | :--------: | :-------: | +| Atmosfair Webservice 5 | ✅ | ❌ | ❌ | ❌ | ❌ | +| Google Travel Impact Model (various versions) | ✅ | ❌ | ❌ | ❌ | ❌ | +| myclimate Flight Calculator V1 & V2 | ✅ | ❌ | ❌ | ❌ | ❌ | +| myclimate Bulk Flight Calculator | ✅ | ❌ | ❌ | ❌ | ❌ | +| GoClimate Flight Footprint | ✅ | ✅ | ✅ | ✅ | ❌ | +| C-Level Carbon Balance API | ✅ | ❌ | ❌ | ❌ | ❌ | +| KlimaLink API | ✅ | ❌ | ❌ | ❌ | ❌ | +| KlimAPI Calculation & Compensation API (v1 & v2) | ✅ | ❌ | ❌ | ❌ | ❌ | +| calco2la.to | ✅ | ✅ | ✅ | ✅ | ✅ | ## License diff --git a/merge_providers.py b/merge_providers.py new file mode 100644 index 0000000..877d708 --- /dev/null +++ b/merge_providers.py @@ -0,0 +1,500 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import copy +import sys +import xml.etree.ElementTree as ET +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable + + +@dataclass +class ProviderSource: + provider_id: str + expected_id: str + path: Path + element: ET.Element + + +def local_name(tag: str) -> str: + """Return local XML tag name, ignoring namespace if present.""" + if "}" in tag: + return tag.rsplit("}", 1)[1] + return tag + + +def indent_xml(element: ET.Element, level: int = 0) -> None: + indentation = "\n" + level * "\t" + child_indentation = "\n" + (level + 1) * "\t" + + children = list(element) + + if children: + if not element.text or not element.text.strip(): + element.text = child_indentation + + for child in children: + indent_xml(child, level + 1) + + last_child = children[-1] + if not last_child.tail or not last_child.tail.strip(): + last_child.tail = indentation + + if level and (not element.tail or not element.tail.strip()): + element.tail = indentation + + +def parse_csv(value: str | None) -> set[str] | None: + if not value: + return None + + result = {part.strip() for part in value.split(",") if part.strip()} + return result or None + + +def parse_provider_operations(values: list[str] | None) -> dict[str, set[str]]: + """ + Parse repeated provider-specific operation filters. + + Example: + --provider-operations atmosfair=flight,airports + --provider-operations myclimate=flight + """ + result: dict[str, set[str]] = {} + + if not values: + return result + + for value in values: + if "=" not in value: + raise ValueError( + f"Invalid provider operation filter '{value}'. " + "Expected format: provider_id=operation1,operation2" + ) + + provider_id, operations_raw = value.split("=", 1) + provider_id = provider_id.strip() + + if not provider_id: + raise ValueError(f"Invalid provider operation filter '{value}': missing provider id.") + + operations = parse_csv(operations_raw) + if not operations: + raise ValueError(f"Invalid provider operation filter '{value}': missing operations.") + + result[provider_id] = operations + + return result + + +def find_xml_files(input_dir: Path, output_file: Path | None = None) -> list[Path]: + files = sorted(input_dir.rglob("*.xml")) + + if output_file: + output_file = output_file.resolve() + files = [path for path in files if path.resolve() != output_file] + + return files + + +def expected_provider_id_for_file(input_dir: Path, path: Path) -> str: + """ + If file is directly in input dir: + providers/atmosfair.xml -> atmosfair + + If file is in provider folder: + providers/atmosfair/provider.xml -> atmosfair + """ + relative = path.relative_to(input_dir) + + if len(relative.parts) == 1: + return path.stem + + return relative.parts[0] + + +def find_provider_nodes(root: ET.Element) -> list[ET.Element]: + if local_name(root.tag) == "provider": + return [root] + + return [element for element in root.iter() if local_name(element.tag) == "provider"] + + +def provider_id(provider: ET.Element, fallback: str) -> str: + value = ( + provider.attrib.get("id") + or provider.attrib.get("name") + or provider.attrib.get("provider") + or fallback + ) + + if "id" not in provider.attrib: + provider.attrib["id"] = value + + return value + + +def read_providers(input_dir: Path, output_file: Path | None, strict_ids: bool) -> list[ProviderSource]: + providers: list[ProviderSource] = [] + + for path in find_xml_files(input_dir, output_file): + expected_id = expected_provider_id_for_file(input_dir, path) + + try: + tree = ET.parse(path) + except ET.ParseError as exc: + raise RuntimeError(f"Could not parse XML file '{path}': {exc}") from exc + + root = tree.getroot() + provider_nodes = find_provider_nodes(root) + + if not provider_nodes: + print(f"Warning: no node found in {path}", file=sys.stderr) + continue + + if len(provider_nodes) > 1: + print( + f"Warning: multiple nodes found in {path}; all will be considered.", + file=sys.stderr, + ) + + for provider_node in provider_nodes: + current_id = provider_id(provider_node, expected_id) + + if strict_ids and current_id != expected_id: + raise RuntimeError( + f"Provider id mismatch in '{path}': " + f"expected '{expected_id}', found '{current_id}'." + ) + + if current_id != expected_id: + print( + f"Warning: provider id mismatch in '{path}': " + f"folder/file suggests '{expected_id}', XML says '{current_id}'.", + file=sys.stderr, + ) + + providers.append( + ProviderSource( + provider_id=current_id, + expected_id=expected_id, + path=path, + element=provider_node, + ) + ) + + return providers + + +def operation_id(operation: ET.Element) -> str | None: + return ( + operation.attrib.get("id") + or operation.attrib.get("name") + or operation.attrib.get("operation") + ) + + +def find_operations_container(provider: ET.Element) -> ET.Element | None: + for child in provider: + if local_name(child.tag) == "operations": + return child + return None + + +def list_provider_operations(provider: ET.Element) -> list[str]: + operations_container = find_operations_container(provider) + + if operations_container is None: + return [] + + operation_ids: list[str] = [] + + for child in operations_container: + if local_name(child.tag) != "operation": + continue + + op_id = operation_id(child) + if op_id: + operation_ids.append(op_id) + + return operation_ids + + +def filter_provider_operations(provider: ET.Element, allowed_operations: set[str] | None) -> ET.Element: + provider_copy = copy.deepcopy(provider) + + if allowed_operations is None: + return provider_copy + + operations_container = find_operations_container(provider_copy) + + if operations_container is None: + return provider_copy + + for child in list(operations_container): + if local_name(child.tag) != "operation": + continue + + op_id = operation_id(child) + + if op_id not in allowed_operations: + operations_container.remove(child) + + return provider_copy + + +def ask_selection( + label: str, + options: list[str], + default_all: bool = True, +) -> set[str] | None: + if not options: + return set() + + print() + print(label) + for index, option in enumerate(options, start=1): + print(f" {index}) {option}") + + if default_all: + prompt = "Selection [Enter = all, comma-separated numbers or names]: " + else: + prompt = "Selection [Enter = none, comma-separated numbers or names]: " + + raw = input(prompt).strip() + + if not raw: + return None if default_all else set() + + selected: set[str] = set() + + for part in raw.split(","): + part = part.strip() + + if part.isdigit(): + index = int(part) + if index < 1 or index > len(options): + raise ValueError(f"Invalid selection number: {index}") + selected.add(options[index - 1]) + else: + if part not in options: + raise ValueError(f"Invalid selection value: {part}") + selected.add(part) + + return selected + + +def interactive_selection( + providers: list[ProviderSource], +) -> tuple[set[str] | None, set[str] | None, dict[str, set[str]]]: + provider_ids = sorted({provider.provider_id for provider in providers}) + + selected_providers = ask_selection( + "Available providers:", + provider_ids, + default_all=True, + ) + + effective_providers = selected_providers or set(provider_ids) + + all_operations = sorted( + { + operation + for provider in providers + if provider.provider_id in effective_providers + for operation in list_provider_operations(provider.element) + } + ) + + global_operations = ask_selection( + "Available operations:", + all_operations, + default_all=True, + ) + + provider_operations: dict[str, set[str]] = {} + + print() + custom = input("Define provider-specific operation filters? [y/N]: ").strip().lower() + + if custom in {"y", "yes"}: + for provider_id in sorted(effective_providers): + provider = next(p for p in providers if p.provider_id == provider_id) + ops = sorted(list_provider_operations(provider.element)) + + if not ops: + continue + + selected_ops = ask_selection( + f"Operations for provider '{provider_id}':", + ops, + default_all=True, + ) + + if selected_ops is not None: + provider_operations[provider_id] = selected_ops + + return selected_providers, global_operations, provider_operations + + +def merge_providers( + providers: list[ProviderSource], + selected_providers: set[str] | None, + global_operations: set[str] | None, + provider_operations: dict[str, set[str]], +) -> ET.Element: + root = ET.Element("providers") + + seen_provider_ids: set[str] = set() + + for provider in providers: + if selected_providers is not None and provider.provider_id not in selected_providers: + continue + + if provider.provider_id in seen_provider_ids: + raise RuntimeError( + f"Duplicate provider id '{provider.provider_id}' after selection. " + "Provider ids must be unique in the merged output." + ) + + seen_provider_ids.add(provider.provider_id) + + operations_for_provider = provider_operations.get( + provider.provider_id, + global_operations, + ) + + provider_element = filter_provider_operations( + provider.element, + operations_for_provider, + ) + + root.append(provider_element) + + return root + + +def write_output(root: ET.Element, output_file: Path) -> None: + output_file.parent.mkdir(parents=True, exist_ok=True) + + indent_xml(root) + + ET.register_namespace("", "https://calco2la.to/schema/providers/v1") + tree = ET.ElementTree(root) + tree.write( + output_file, + encoding="utf-8", + xml_declaration=True, + ) + + +def build_arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Merge single-provider XML descriptions into one provider.xml file." + ) + + parser.add_argument( + "input_dir", + type=Path, + help="Directory containing provider XML files or provider folders.", + ) + + parser.add_argument( + "-o", + "--output", + type=Path, + default=Path("provider.xml"), + help="Output file. Default: provider.xml", + ) + + parser.add_argument( + "--providers", + help="Comma-separated provider ids to merge. Default: all providers.", + ) + + parser.add_argument( + "--operations", + help="Comma-separated global operation ids to keep. Default: all operations.", + ) + + parser.add_argument( + "--provider-operations", + action="append", + help=( + "Provider-specific operation filter. " + "Format: provider_id=operation1,operation2. " + "Can be used multiple times." + ), + ) + + parser.add_argument( + "-i", + "--interactive", + action="store_true", + help="Interactively select providers and operations.", + ) + + parser.add_argument( + "--strict-provider-id", + action="store_true", + help="Fail if folder/file name and XML provider id do not match.", + ) + + return parser + + +def main() -> int: + parser = build_arg_parser() + args = parser.parse_args() + + input_dir: Path = args.input_dir + output_file: Path = args.output + + if not input_dir.exists(): + print(f"Input directory does not exist: {input_dir}", file=sys.stderr) + return 1 + + if not input_dir.is_dir(): + print(f"Input path is not a directory: {input_dir}", file=sys.stderr) + return 1 + + try: + providers = read_providers( + input_dir=input_dir, + output_file=output_file, + strict_ids=args.strict_provider_id, + ) + + if not providers: + print("No providers found.", file=sys.stderr) + return 1 + + if args.interactive: + selected_providers, global_operations, provider_operations = interactive_selection(providers) + else: + selected_providers = parse_csv(args.providers) + global_operations = parse_csv(args.operations) + provider_operations = parse_provider_operations(args.provider_operations) + + merged_root = merge_providers( + providers=providers, + selected_providers=selected_providers, + global_operations=global_operations, + provider_operations=provider_operations, + ) + + write_output(merged_root, output_file) + + except Exception as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + + print(f"Created merged provider configuration: {output_file}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/providers/atmosfair/atmosfair.xml b/providers/atmosfair/atmosfair.xml new file mode 100644 index 0000000..2b9babe --- /dev/null +++ b/providers/atmosfair/atmosfair.xml @@ -0,0 +1,68 @@ + + + + Atmosfair Webservice 5 + https://api.atmosfair.de + + + + + + { + "accountId": "${env.ATMOSFAIR_ACCOUNT_ID}", + "password": "${env.ATMOSFAIR_PASSWORD}", + "calculationMethod": "${request.calculation_method}", + "flights": [ + #for leg in request.legs + { + "departure": "${leg.origin_iata}", + "arrival": "${leg.destination_iata}", + "flightNumber": "${leg.flight_number}", + "departureDate": "${leg.departure_date}", + "passengerCount": ${leg.passenger_count}, + "flightCount": ${leg.flight_count}, + "travelClass": "${leg.travel_class}", + "charter": ${leg.charter}, + "aircraftType": "${leg.aircraft_type}" + }#sep, + #end + ] + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/providers/calco2lato/calco2lato.xml b/providers/calco2lato/calco2lato.xml index 8bef19e..2a159e5 100644 --- a/providers/calco2lato/calco2lato.xml +++ b/providers/calco2lato/calco2lato.xml @@ -3,47 +3,72 @@ calco2la.to https://api.calco2la.to -
Authorization
Bearer ${API_KEY} + CALCO2LATO_API_KEY
- - - - + { - "legs": [ + "api_key": "${env.CALCO2LATO_API_KEY}", + "departureDate": "${request.departure_date}", + "rfi": ${request.rfi}, + "pricePerTon": ${request.price_per_ton}, + "flights": [ #for leg in request.legs { - "origin": "${leg.origin_iata}", - "destination": "${leg.destination_iata}", - "departure_time": "${leg.departure_time}" + "departure": "${leg.origin_iata}", + "arrival": "${leg.destination_iata}", + "passengerCount": ${leg.passenger_count}, + "flightCount": ${leg.flight_count}, + "travelClass": "${leg.travel_class}", + "charter": ${leg.charter}, + "aircraftType": "${leg.aircraft_type}", + "departureDate": "${leg.departure_date}", + "calculationMethod": "${request.calculation_method}", + "via": ${leg.via}, + "rfi": ${request.rfi}, + "pricePerTon": ${request.price_per_ton} }#sep, #end - ], - "cabin_class": "${request.cabin_class}", - "passengers": ${request.passengers}, - "include_non_co2": ${request.include_non_co2} + ] } - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/providers/clevel/clevel.xml b/providers/clevel/clevel.xml new file mode 100644 index 0000000..08e6de2 --- /dev/null +++ b/providers/clevel/clevel.xml @@ -0,0 +1,36 @@ + + + GoClimate Flight Footprint + https://api.goclimate.com + + true + GOCLIMATE_API_KEY + + + + + + #for leg in request.legs + + + #end + + #for currency in request.currencies + + #end + + + + + + + + + + + + + + + + diff --git a/providers/goclimate/goclimate.xml b/providers/goclimate/goclimate.xml new file mode 100644 index 0000000..9ac8b44 --- /dev/null +++ b/providers/goclimate/goclimate.xml @@ -0,0 +1,51 @@ + + + C-Level Carbon Balance API + https://api.c-level.earth + +
Authorization
+ Bearer ${API_KEY} + CLEVEL_API_KEY +
+ + + + + { + "IataCodes":[ + #for waypoint in request.iata_path + "${waypoint}"#sep, + #end + ], + "IsReturn":${request.roundtrip}, + "Passengers":${request.passengers}, + "Class":"${request.vendor_options.clevel_class}", + "Type":"${request.vendor_options.clevel_type}", + "AircraftModel":"${request.vendor_options.aircraft_model}", + "Reference":"${request.reference}", + "CurrencyCode":"${request.currency}" + } + + + + + + + + + + + + + + + + + + + + + + +
+ diff --git a/providers/google_tim/google_tim.xml b/providers/google_tim/google_tim.xml index af79387..d915fe1 100644 --- a/providers/google_tim/google_tim.xml +++ b/providers/google_tim/google_tim.xml @@ -3,48 +3,52 @@ Google Travel Impact Model - https://travelimpactmodel.googleapis.com/v1 - + https://travelimpactmodel.googleapis.com
X-Goog-Api-Key
${API_KEY} + GOOGLE_TIM_API_KEY
- - - - + { - "flightSegments": [ + "flights": [ #for leg in request.legs { - "departureAirport": { "code": "${leg.origin_iata}" }, - "arrivalAirport": { "code": "${leg.destination_iata}" } + "origin":"${leg.origin_iata}", + "destination":"${leg.destination_iata}", + "operatingCarrierCode":"${leg.operating_carrier}", + "flightNumber":${leg.flight_number}, + "departureDate":{ + "year":${leg.departure_date.year}, + "month":${leg.departure_date.month}, + "day":${leg.departure_date.day} + } }#sep, #end - ], - "cabinClass": "${request.cabin_class}", - "passengerCount": ${request.passengers} + ] } - - - - - - - - + + + + + + + + + + + + + + + + -
diff --git a/providers/klimalink/klimalink.xml b/providers/klimalink/klimalink.xml new file mode 100644 index 0000000..cdea5cb --- /dev/null +++ b/providers/klimalink/klimalink.xml @@ -0,0 +1,48 @@ + + + KlimaLink API + https://api.dev.klimalink.org + + KLIMALINK_TOKEN + + + + + + { + "flightActivities": [ + #for leg in request.legs + { + "index":${index}, + "departure":"${leg.origin_iata}", + "arrival":"${leg.destination_iata}", + "flightNumber":"${leg.flight_number}", + "flightDate":"${leg.departure_date}", + "aircraftType":"${leg.aircraft_type}", + "airline":"${leg.airline}", + "passengerCount":${leg.passenger_count}, + "flightCount":${leg.flight_count}, + "travelClass":"${leg.travel_class}" + }#sep, + #end + ] + } + + + + + + + + + + + + + + + + + + + diff --git a/providers/klimapi/klimapi_v1.xml b/providers/klimapi/klimapi_v1.xml new file mode 100644 index 0000000..1a96597 --- /dev/null +++ b/providers/klimapi/klimapi_v1.xml @@ -0,0 +1,147 @@ + + + + KlimAPI v1 + https://api.klimapi.com/v1 + +
X-API-KEY
+ ${API_KEY} + KLIMAPI_V1_API_KEY +
+ + + + + { + "calculation_options": [ + #for leg in request.legs + { + "type": "flight", + "departure": "${leg.origin_iata}", + "destination": "${leg.destination_iata}", + "travel_class": "${leg.travel_class}", + "passengers": ${leg.passenger_count}, + "return_trip": ${request.roundtrip} + }#sep, + #end + ], + "fractional_digits": ${request.fractional_digits} + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + "calculation_options": [ + #for leg in request.legs + { + "type": "flight", + "carrier_code": "${leg.operating_carrier}", + "flight_number": ${leg.flight_number}, + "departure_date": "${leg.departure_date}", + "departure": "${leg.origin_iata}", + "destination": "${leg.destination_iata}", + "travel_class": "${leg.travel_class}", + "passengers": ${leg.passenger_count} + }#sep, + #end + ], + "fractional_digits": ${request.fractional_digits} + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + "calculation_options": [ + #for leg in request.legs + { + "type": "flight", + "distance": ${leg.distance_km}, + "unit": "kilometers", + "travel_class": "${leg.travel_class}", + "passengers": ${leg.passenger_count}, + "return_trip": ${request.roundtrip} + }#sep, + #end + ], + "fractional_digits": ${request.fractional_digits} + } + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/providers/klimapi/klimapi_v2.xml b/providers/klimapi/klimapi_v2.xml new file mode 100644 index 0000000..176a9be --- /dev/null +++ b/providers/klimapi/klimapi_v2.xml @@ -0,0 +1,114 @@ + + + + KlimAPI v2 + https://api.klimapi.com/v2 + +
X-API-KEY
+ ${API_KEY} + KLIMAPI_V2_API_KEY +
+ + + + + { + "calculation_options": [ + #for leg in request.legs + { + "type": "travel-air", + "activity": "flights", + "specification": "${request.vendor_options.klimapi_specification}", + "detail": "${request.vendor_options.klimapi_detail}", + "departure": "${leg.origin_iata}", + "destination": "${leg.destination_iata}", + "return_trip": ${request.roundtrip}, + "passengers": ${leg.passenger_count} + }#sep, + #end + ], + "fractional_digits": ${request.fractional_digits} + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + "calculation_options": [ + #for leg in request.legs + { + "type": "travel-air", + "activity": "flights", + "specification": "${request.vendor_options.klimapi_specification}", + "detail": "${request.vendor_options.klimapi_detail}", + "value": ${leg.vendor_options.klimapi_passenger_distance}, + "unit": "${request.vendor_options.klimapi_unit}" + }#sep, + #end + ], + "fractional_digits": ${request.fractional_digits} + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
\ No newline at end of file diff --git a/providers/myclimate/myclimate_bulk.xml b/providers/myclimate/myclimate_bulk.xml new file mode 100644 index 0000000..6f4a394 --- /dev/null +++ b/providers/myclimate/myclimate_bulk.xml @@ -0,0 +1,45 @@ + + + + myclimate Bulk Flight Calculator + https://api.myclimate.org + + MYCLIMATE_USERNAME + MYCLIMATE_PASSWORD + + + + + + { + "flights": [ + #for leg in request.legs + { + "id":"${leg.id}", + "from":"${leg.origin_iata}", + "to":"${leg.destination_iata}", + "aircraft_type":"${leg.aircraft_type}", + "flight_class":"${leg.travel_class}" + }#sep, + #end + ] + } + + + + + + + + + + + + + + + + + + + diff --git a/providers/myclimate/myclimate_v1.xml b/providers/myclimate/myclimate_v1.xml new file mode 100644 index 0000000..1267339 --- /dev/null +++ b/providers/myclimate/myclimate_v1.xml @@ -0,0 +1,38 @@ + + + + myclimate Flight Calculator V1 + https://api.myclimate.org + + MYCLIMATE_USERNAME + MYCLIMATE_PASSWORD + + + + + + { + "from":"${request.legs[0].origin_iata}", + "to":"${request.legs[last].destination_iata}", + "via":"${request.vendor_options.via}", + "passengers":${request.passengers}, + "roundtrip":${request.roundtrip}, + "flight_class":"${request.cabin_class}" + } + + + + + + + + + + + + + + + + + diff --git a/providers/myclimate/myclimate_v2.xml b/providers/myclimate/myclimate_v2.xml new file mode 100644 index 0000000..d1e6ef6 --- /dev/null +++ b/providers/myclimate/myclimate_v2.xml @@ -0,0 +1,36 @@ + + + + myclimate Flight Calculator V2 + https://api.myclimate.org + + MYCLIMATE_USERNAME + MYCLIMATE_PASSWORD + + + + + + { + "from":"${request.legs[0].origin_iata}", + "to":"${request.legs[last].destination_iata}", + "via":"${request.vendor_options.via}", + "aircraft_type_leg_1":"${request.legs[0].aircraft_type}", + "aircraft_type_leg_2":"${request.legs[1].aircraft_type}", + "passengers":${request.passengers}, + "roundtrip":${request.roundtrip}, + "flight_class":"${request.cabin_class}" + } + + + + + + + + + + + + +