diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/acceptance-test-config.yml b/airbyte-integrations/connectors/source-amazon-seller-partner/acceptance-test-config.yml index d9e1b1d6b320..c4278cb6df5e 100644 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/acceptance-test-config.yml +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/acceptance-test-config.yml @@ -130,6 +130,8 @@ acceptance_tests: bypass_reason: "Data cannot be seeded in the test account, integration tests added for the stream instead" - name: GET_VENDOR_TRAFFIC_REPORT bypass_reason: "Data cannot be seeded in the test account, integration tests added for the stream instead" + - name: VendorOrders + bypass_reason: "Data cannot be seeded in the test account, integration tests added for the stream instead" incremental: tests: - config_path: "secrets/config.json" diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/metadata.yaml b/airbyte-integrations/connectors/source-amazon-seller-partner/metadata.yaml index 552bbaa1b92c..c1232b46e330 100644 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/metadata.yaml +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/metadata.yaml @@ -15,7 +15,7 @@ data: connectorSubtype: api connectorType: source definitionId: e55879a8-0ef8-4557-abcf-ab34c53ec460 - dockerImageTag: 3.3.2 + dockerImageTag: 3.4.0 dockerRepository: airbyte/source-amazon-seller-partner documentationUrl: https://docs.airbyte.com/integrations/sources/amazon-seller-partner githubIssueLabel: source-amazon-seller-partner diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/schemas/VendorOrders.json b/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/schemas/VendorOrders.json new file mode 100644 index 000000000000..c05d713e5e9e --- /dev/null +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/schemas/VendorOrders.json @@ -0,0 +1,354 @@ +{ + "title": "Vendor Orders", + "description": "All vendor purchase orders that were updated after a specified date", + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "purchaseOrderNumber": { + "type": ["null", "string"] + }, + "purchaseOrderState": { + "type": ["null", "string"] + }, + "orderDetails": { + "type": ["null", "object"], + "properties": { + "purchaseOrderDate": { + "type": ["null", "string"], + "format": "date-time" + }, + "purchaseOrderChangedDate": { + "type": ["null", "string"], + "format": "date-time" + }, + "purchaseOrderStateChangedDate": { + "type": ["null", "string"], + "format": "date-time" + }, + "purchaseOrderType": { + "type": ["null", "string"] + }, + "importDetails": { + "type": ["null", "object"], + "properties": { + "methodOfPayment": { + "type": ["null", "string"] + }, + "internationalCommercialTerms": { + "type": ["null", "string"] + }, + "portOfDelivery": { + "type": ["null", "string"] + }, + "importContainers": { + "type": ["null", "string"] + }, + "shippingInstructions": { + "type": ["null", "string"] + } + } + }, + "dealCode": { + "type": ["null", "string"] + }, + "paymentMethod": { + "type": ["null", "string"] + }, + "buyingParty": { + "type": ["null", "object"], + "properties": { + "partyId": { + "type": ["null", "string"] + }, + "address": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "addressLine1": { + "type": ["null", "string"] + }, + "addressLine2": { + "type": ["null", "string"] + }, + "addressLine3": { + "type": ["null", "string"] + }, + "city": { + "type": ["null", "string"] + }, + "county": { + "type": ["null", "string"] + }, + "district": { + "type": ["null", "string"] + }, + "stateOrRegion": { + "type": ["null", "string"] + }, + "postalCode": { + "type": ["null", "string"] + }, + "countryCode": { + "type": ["null", "string"] + }, + "phone": { + "type": ["null", "string"] + } + } + }, + "taxInfo": { + "type": ["null", "object"], + "properties": { + "taxType": { + "type": ["null", "string"] + }, + "taxRegistrationNumber": { + "type": ["null", "string"] + } + } + } + } + }, + "sellingParty": { + "type": ["null", "object"], + "properties": { + "partyId": { + "type": ["null", "string"] + }, + "address": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "addressLine1": { + "type": ["null", "string"] + }, + "addressLine2": { + "type": ["null", "string"] + }, + "addressLine3": { + "type": ["null", "string"] + }, + "city": { + "type": ["null", "string"] + }, + "county": { + "type": ["null", "string"] + }, + "district": { + "type": ["null", "string"] + }, + "stateOrRegion": { + "type": ["null", "string"] + }, + "postalCode": { + "type": ["null", "string"] + }, + "countryCode": { + "type": ["null", "string"] + }, + "phone": { + "type": ["null", "string"] + } + } + }, + "taxInfo": { + "type": ["null", "object"], + "properties": { + "taxType": { + "type": ["null", "string"] + }, + "taxRegistrationNumber": { + "type": ["null", "string"] + } + } + } + } + }, + "shipToParty": { + "type": ["null", "object"], + "properties": { + "partyId": { + "type": ["null", "string"] + }, + "address": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "addressLine1": { + "type": ["null", "string"] + }, + "addressLine2": { + "type": ["null", "string"] + }, + "addressLine3": { + "type": ["null", "string"] + }, + "city": { + "type": ["null", "string"] + }, + "county": { + "type": ["null", "string"] + }, + "district": { + "type": ["null", "string"] + }, + "stateOrRegion": { + "type": ["null", "string"] + }, + "postalCode": { + "type": ["null", "string"] + }, + "countryCode": { + "type": ["null", "string"] + }, + "phone": { + "type": ["null", "string"] + } + } + }, + "taxInfo": { + "type": ["null", "object"], + "properties": { + "taxType": { + "type": ["null", "string"] + }, + "taxRegistrationNumber": { + "type": ["null", "string"] + } + } + } + } + }, + "billToParty": { + "type": ["null", "object"], + "properties": { + "partyId": { + "type": ["null", "string"] + }, + "address": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "addressLine1": { + "type": ["null", "string"] + }, + "addressLine2": { + "type": ["null", "string"] + }, + "addressLine3": { + "type": ["null", "string"] + }, + "city": { + "type": ["null", "string"] + }, + "county": { + "type": ["null", "string"] + }, + "district": { + "type": ["null", "string"] + }, + "stateOrRegion": { + "type": ["null", "string"] + }, + "postalCode": { + "type": ["null", "string"] + }, + "countryCode": { + "type": ["null", "string"] + }, + "phone": { + "type": ["null", "string"] + } + } + }, + "taxInfo": { + "type": ["null", "object"], + "properties": { + "taxType": { + "type": ["null", "string"] + }, + "taxRegistrationNumber": { + "type": ["null", "string"] + } + } + } + } + }, + "shipWindow": { + "type": ["null", "string"] + }, + "deliveryWindow": { + "type": ["null", "string"] + }, + "items": { + "type": ["null", "array"], + "items": { + "type": ["null", "object"], + "properties": { + "itemSequenceNumber": { + "type": ["null", "string"] + }, + "amazonProductIdentifier": { + "type": ["null", "string"] + }, + "vendorProductIdentifier": { + "type": ["null", "string"] + }, + "orderedQuantity": { + "type": ["null", "object"], + "properties": { + "amount": { + "type": ["null", "integer"] + }, + "unitOfMeasure": { + "type": ["null", "string"] + }, + "unitSize": { + "type": ["null", "integer"] + } + } + }, + "isBackOrderAllowed": { + "type": ["null", "boolean"] + }, + "netCost": { + "type": ["null", "object"], + "properties": { + "amount": { + "type": ["null", "string"] + }, + "currencyCode": { + "type": ["null", "string"] + } + } + }, + "listPrice": { + "type": ["null", "object"], + "properties": { + "amount": { + "type": ["null", "string"] + }, + "currencyCode": { + "type": ["null", "string"] + } + } + } + } + } + } + } + }, + "changedBefore": { + "type": ["null", "string"], + "format": "date-time" + } + } +} diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/source.py b/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/source.py index ba191b115178..b6547b208146 100644 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/source.py +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/source.py @@ -3,7 +3,6 @@ # -import traceback from os import getenv from typing import Any, List, Mapping, Optional, Tuple @@ -64,6 +63,7 @@ StrandedInventoryUiReport, VendorDirectFulfillmentShipping, VendorInventoryReports, + VendorOrders, VendorSalesReports, VendorTrafficReport, XmlAllOrdersDataByOrderDataGeneral, @@ -181,6 +181,7 @@ def streams(self, config: Mapping[str, Any]) -> List[Stream]: FbaInventoryPlaningReport, LedgerSummaryViewReport, FbaReimbursementsReports, + VendorOrders, ] # TODO: Remove after Brand Analytics will be enabled in CLOUD: https://github.com/airbytehq/airbyte/issues/32353 diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/streams.py b/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/streams.py index f59eee2663c9..a8a644709ffa 100644 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/streams.py +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/streams.py @@ -29,6 +29,7 @@ ORDERS_API_VERSION = "v0" VENDORS_API_VERSION = "v1" FINANCES_API_VERSION = "v0" +VENDOR_ORDERS_API_VERSION = "v1" DATE_TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" DATE_FORMAT = "%Y-%m-%d" @@ -128,7 +129,11 @@ def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, return {self.next_page_token_field: next_page_token} def parse_response( - self, response: requests.Response, stream_state: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None, **kwargs + self, + response: requests.Response, + stream_state: Mapping[str, Any] = None, + stream_slice: Mapping[str, Any] = None, + **kwargs: Any, ) -> Iterable[Mapping]: """ Return an iterable containing each record in the response @@ -137,8 +142,8 @@ def parse_response( def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]) -> Mapping[str, Any]: """ - Return the latest state by comparing the cursor value in the latest record with the stream's most recent state object - and returning an updated state object. + Return the latest state by comparing the cursor value in the latest record with the stream's + most recent state object and returning an updated state object. """ latest_record_state = latest_record[self.cursor_field] if stream_state := current_stream_state.get(self.cursor_field): @@ -176,7 +181,8 @@ class ReportsAmazonSPStream(HttpStream, ABC): data_field = "payload" result_key = None - # see data availability sla at https://developer-docs.amazon.com/sp-api/docs/report-type-values#vendor-retail-analytics-reports + # see data availability sla at + # https://developer-docs.amazon.com/sp-api/docs/report-type-values#vendor-retail-analytics-reports availability_sla_days = 1 availability_strategy = None @@ -196,7 +202,7 @@ def __init__( self._replication_start_date = replication_start_date self._replication_end_date = replication_end_date self.marketplace_id = marketplace_id - self.period_in_days = max(period_in_days, self.replication_start_date_limit_in_days) # ensure old configs work as well + self.period_in_days = max(period_in_days, self.replication_start_date_limit_in_days) # ensure old configs work self._report_options = report_options self._http_method = "GET" @@ -214,7 +220,8 @@ def http_method(self, value: str): @property def retry_factor(self) -> float: """ - Set this 60.0 due to https://developer-docs.amazon.com/sp-api/docs/reports-api-v2021-06-30-reference#post-reports2021-06-30reports + Set this 60.0 due to + https://developer-docs.amazon.com/sp-api/docs/reports-api-v2021-06-30-reference#post-reports2021-06-30reports Override to 0 for integration testing purposes """ return 0 if IS_TESTING else 60.0 @@ -288,7 +295,11 @@ def download_and_decompress_report_document(self, payload: dict) -> str: return report.content.decode("iso-8859-1") def parse_response( - self, response: requests.Response, stream_state: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None, **kwargs + self, + response: requests.Response, + stream_state: Mapping[str, Any] = None, + stream_slice: Mapping[str, Any] = None, + **kwargs: Any, ) -> Iterable[Mapping]: payload = response.json() @@ -384,7 +395,8 @@ def read_records( elif processing_status == ReportProcessingStatus.FATAL: raise AirbyteTracedException( internal_message=( - f"Failed to retrieve the report '{self.name}' for period {stream_slice['dataStartTime']}-{stream_slice['dataEndTime']} " + f"Failed to retrieve the report '{self.name}' for period " + f"{stream_slice['dataStartTime']}-{stream_slice['dataEndTime']} " "due to Amazon Seller Partner platform issues. This will be read during the next sync." ) ) @@ -411,8 +423,8 @@ def _transform_report_record_cursor_value(self, date_string: str) -> str: def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]) -> Mapping[str, Any]: """ - Return the latest state by comparing the cursor value in the latest record with the stream's most recent state object - and returning an updated state object. + Return the latest state by comparing the cursor value in the latest record with the stream's + most recent state object and returning an updated state object. """ latest_record_state = self._transform_report_record_cursor_value(latest_record[self.cursor_field]) if stream_state := current_stream_state.get(self.cursor_field): @@ -489,7 +501,8 @@ class FbaOrdersReports(IncrementalReportsAmazonSPStream): class FlatFileActionableOrderDataShipping(IncrementalReportsAmazonSPStream): """ - Field definitions: https://developer-docs.amazon.com/sp-api/docs/order-reports-attributes#get_flat_file_actionable_order_data_shipping + Field definitions: + https://developer-docs.amazon.com/sp-api/docs/order-reports-attributes#get_flat_file_actionable_order_data_shipping """ name = "GET_FLAT_FILE_ACTIONABLE_ORDER_DATA_SHIPPING" @@ -497,7 +510,8 @@ class FlatFileActionableOrderDataShipping(IncrementalReportsAmazonSPStream): class OrderReportDataShipping(IncrementalReportsAmazonSPStream): """ - Field definitions: https://developer-docs.amazon.com/sp-api/docs/order-reports-attributes#get_order_report_data_shipping + Field definitions: + https://developer-docs.amazon.com/sp-api/docs/order-reports-attributes#get_order_report_data_shipping """ name = "GET_ORDER_REPORT_DATA_SHIPPING" @@ -545,7 +559,11 @@ class GetXmlBrowseTreeData(IncrementalReportsAmazonSPStream): def parse_document(self, document): try: parsed = xmltodict.parse( - document, dict_constructor=dict, attr_prefix="", cdata_key="text", force_list={"attribute", "id", "refinementField"} + document, + dict_constructor=dict, + attr_prefix="", + cdata_key="text", + force_list={"attribute", "id", "refinementField"}, ) except Exception as e: self.logger.warning(f"Unable to parse the report for the stream {self.name}, error: {str(e)}") @@ -737,7 +755,11 @@ def _report_data( return data def parse_response( - self, response: requests.Response, stream_state: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None, **kwargs + self, + response: requests.Response, + stream_state: Mapping[str, Any] = None, + stream_slice: Mapping[str, Any] = None, + **kwargs: Any, ) -> Iterable[Mapping]: payload = response.json() @@ -752,8 +774,8 @@ def parse_response( def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]) -> Mapping[str, Any]: """ - Return the latest state by comparing the cursor value in the latest record with the stream's most recent state object - and returning an updated state object. + Return the latest state by comparing the cursor value in the latest record with the stream's + most recent state object and returning an updated state object. """ latest_record_state = latest_record[self.cursor_field] if stream_state := current_stream_state.get(self.cursor_field): @@ -858,7 +880,8 @@ class SellerFeedbackReports(IncrementalReportsAmazonSPStream): Field definitions: https://sellercentral.amazon.com/help/hub/reference/G202125660 """ - # The list of MarketplaceIds can be found here https://docs.developer.amazonservices.com/en_UK/dev_guide/DG_Endpoints.html + # The list of MarketplaceIds can be found here: + # https://docs.developer.amazonservices.com/en_UK/dev_guide/DG_Endpoints.html MARKETPLACE_DATE_FORMAT_MAP = dict( # eu A2VIGQ35RCS4UG="D/M/YY", # AE @@ -928,7 +951,8 @@ def parse_document(document): class FbaAfnInventoryReports(IncrementalReportsAmazonSPStream): """ Field definitions: https://developer-docs.amazon.com/sp-api/docs/report-type-values#inventory-reports - Report has a long-running issue (fails when requested frequently): https://github.com/amzn/selling-partner-api-docs/issues/2231 + Report has a long-running issue (fails when requested frequently): + https://github.com/amzn/selling-partner-api-docs/issues/2231 """ name = "GET_AFN_INVENTORY_DATA" @@ -937,7 +961,8 @@ class FbaAfnInventoryReports(IncrementalReportsAmazonSPStream): class FbaAfnInventoryByCountryReports(IncrementalReportsAmazonSPStream): """ Field definitions: https://developer-docs.amazon.com/sp-api/docs/report-type-values#inventory-reports - Report has a long-running issue (fails when requested frequently): https://github.com/amzn/selling-partner-api-docs/issues/2231 + Report has a long-running issue (fails when requested frequently): + https://github.com/amzn/selling-partner-api-docs/issues/2231 """ name = "GET_AFN_INVENTORY_DATA_BY_COUNTRY" @@ -981,7 +1006,11 @@ def request_params( return params def parse_response( - self, response: requests.Response, stream_state: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None, **kwargs + self, + response: requests.Response, + stream_state: Mapping[str, Any] = None, + stream_slice: Mapping[str, Any] = None, + **kwargs: Any, ) -> Iterable[Mapping]: yield from response.json().get(self.data_field, {}).get(self.name, []) @@ -1050,7 +1079,11 @@ def backoff_time(self, response: requests.Response) -> Optional[float]: return self.default_backoff_time def parse_response( - self, response: requests.Response, stream_state: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None, **kwargs + self, + response: requests.Response, + stream_state: Mapping[str, Any] = None, + stream_slice: Mapping[str, Any] = None, + **kwargs: Any, ) -> Iterable[Mapping]: order_items_list = response.json().get(self.data_field, {}) if order_items_list.get(self.next_page_token_field) is None: @@ -1093,61 +1126,108 @@ class LedgerSummaryViewReport(LedgerDetailedViewReports): name = "GET_LEDGER_SUMMARY_VIEW_DATA" -class VendorDirectFulfillmentShipping(IncrementalAmazonSPStream): - """ - API docs: https://developer-docs.amazon.com/sp-api/docs/vendor-direct-fulfillment-shipping-api-v1-reference - API model: https://github.com/amzn/selling-partner-api-models/blob/main/models/vendor-direct-fulfillment-shipping-api-model/vendorDirectFulfillmentShippingV1.json - - Returns a list of shipping labels created during the time frame that you specify. - Both createdAfter and createdBefore parameters required to select the time frame. - The date range to search must not be more than 7 days. - """ - - name = "VendorDirectFulfillmentShipping" +class VendorFulfillment(IncrementalAmazonSPStream, ABC): primary_key = "purchaseOrderNumber" - replication_start_date_field = "createdAfter" - replication_end_date_field = "createdBefore" next_page_token_field = "nextToken" page_size_field = "limit" - time_format = "%Y-%m-%dT%H:%M:%SZ" - cursor_field = "createdBefore" - def path(self, **kwargs) -> str: - return f"vendor/directFulfillment/shipping/{VENDORS_API_VERSION}/shippingLabels" + @property + @abstractmethod + def records_path(self) -> str: + pass def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: stream_data = response.json() - next_page_token = stream_data.get("payload", {}).get("pagination", {}).get(self.next_page_token_field) + next_page_token = stream_data.get(self.data_field, {}).get("pagination", {}).get(self.next_page_token_field) if next_page_token: return {self.next_page_token_field: next_page_token} + def stream_slices( + self, + sync_mode: SyncMode, + cursor_field: Optional[List[str]] = None, + stream_state: Optional[Mapping[str, Any]] = None, + ) -> Iterable[Optional[Mapping[str, Any]]]: + start_date = pendulum.parse(self._replication_start_date) + end_date = pendulum.parse(self._replication_end_date) if self._replication_end_date else pendulum.now("utc") + + stream_state = stream_state or {} + if state_value := stream_state.get(self.cursor_field): + start_date = max(start_date, pendulum.parse(state_value)) + + start_date = min(start_date, end_date) + while start_date < end_date: + end_date_slice = start_date.add(days=7) + yield { + self.replication_start_date_field: start_date.strftime(DATE_TIME_FORMAT), + self.replication_end_date_field: min(end_date_slice, end_date).strftime(DATE_TIME_FORMAT), + } + start_date = end_date_slice + def request_params( - self, stream_state: Mapping[str, Any], next_page_token: Mapping[str, Any] = None, **kwargs + self, + stream_state: Optional[Mapping[str, Any]], + stream_slice: Optional[Mapping[str, Any]] = None, + next_page_token: Mapping[str, Any] = None, ) -> MutableMapping[str, Any]: + stream_slice = stream_slice or {} if next_page_token: - return dict(next_page_token) + stream_slice.update(next_page_token) - end_date = pendulum.now("utc").strftime(self.time_format) - if self._replication_end_date: - end_date = self._replication_end_date - # The date range to search must not be more than 7 days - see docs - # https://developer-docs.amazon.com/sp-api/docs/vendor-direct-fulfillment-shipping-api-v1-reference - start_date = max(pendulum.parse(self._replication_start_date), pendulum.parse(end_date).subtract(days=7, hours=1)).strftime( - self.time_format - ) - if stream_state_value := stream_state.get(self.cursor_field): - start_date = max(stream_state_value, start_date) - return {self.replication_start_date_field: start_date, self.replication_end_date_field: end_date} + return stream_slice def parse_response( - self, response: requests.Response, stream_state: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None, **kwargs + self, + response: requests.Response, + stream_state: Optional[Mapping[str, Any]] = None, + stream_slice: Optional[Mapping[str, Any]] = None, + next_page_token: Optional[Mapping[str, Any]] = None, ) -> Iterable[Mapping]: - params = self.request_params(stream_state) - for record in response.json().get(self.data_field, {}).get("shippingLabels", []): + params = self.request_params(stream_state=stream_state, stream_slice=stream_slice, next_page_token=next_page_token) + for record in response.json().get(self.data_field, {}).get(self.records_path, []): record[self.replication_end_date_field] = params.get(self.replication_end_date_field) yield record +class VendorDirectFulfillmentShipping(VendorFulfillment): + """ + API docs: https://developer-docs.amazon.com/sp-api/docs/vendor-direct-fulfillment-shipping-api-v1-reference + API model: https://github.com/amzn/selling-partner-api-models/blob/main/models/vendor-direct-fulfillment-shipping-api-model/vendorDirectFulfillmentShippingV1.json + + Returns a list of shipping labels created during the time frame that you specify. + Both createdAfter and createdBefore parameters required to select the time frame. + The date range to search must not be more than 7 days. + """ + + name = "VendorDirectFulfillmentShipping" + records_path = "shippingLabels" + replication_start_date_field = "createdAfter" + replication_end_date_field = "createdBefore" + cursor_field = "createdBefore" + + def path(self, **kwargs: Any) -> str: + return f"vendor/directFulfillment/shipping/{VENDORS_API_VERSION}/shippingLabels" + + +class VendorOrders(VendorFulfillment): + """ + API docs: + https://developer-docs.amazon.com/sp-api/docs/vendor-orders-api-v1-reference#get-vendorordersv1purchaseorders + + API model: + https://github.com/amzn/selling-partner-api-models/blob/main/models/vendor-orders-api-model/vendorOrders.json + """ + + name = "VendorOrders" + records_path = "orders" + replication_start_date_field = "changedAfter" + replication_end_date_field = "changedBefore" + cursor_field = "changedBefore" + + def path(self, **kwargs: Any) -> str: + return f"vendor/orders/{VENDOR_ORDERS_API_VERSION}/purchaseOrders" + + class FinanceStream(IncrementalAmazonSPStream, ABC): next_page_token_field = "NextToken" page_size_field = "MaxResultsPerPage" @@ -1225,7 +1305,11 @@ def path(self, **kwargs) -> str: return f"finances/{FINANCES_API_VERSION}/financialEventGroups" def parse_response( - self, response: requests.Response, stream_state: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None, **kwargs + self, + response: requests.Response, + stream_state: Mapping[str, Any] = None, + stream_slice: Mapping[str, Any] = None, + **kwargs: Any, ) -> Iterable[Mapping]: yield from response.json().get(self.data_field, {}).get("FinancialEventGroupList", []) @@ -1245,7 +1329,11 @@ def path(self, **kwargs) -> str: return f"finances/{FINANCES_API_VERSION}/financialEvents" def parse_response( - self, response: requests.Response, stream_state: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None, **kwargs + self, + response: requests.Response, + stream_state: Mapping[str, Any] = None, + stream_slice: Mapping[str, Any] = None, + **kwargs: Any, ) -> Iterable[Mapping]: params = self.request_params(stream_state) events = response.json().get(self.data_field, {}).get("FinancialEvents", {}) diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/config.py b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/config.py index 8b79eafe8e94..d1decff994b2 100644 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/config.py +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/config.py @@ -19,6 +19,7 @@ CONFIG_START_DATE = "2023-01-01T00:00:00Z" CONFIG_END_DATE = "2023-01-30T00:00:00Z" NOW = pendulum.now(tz="utc") +TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" class ConfigBuilder: @@ -35,11 +36,11 @@ def __init__(self) -> None: } def with_start_date(self, start_date: datetime) -> ConfigBuilder: - self._config["replication_start_date"] = start_date.isoformat()[:-13] + "Z" + self._config["replication_start_date"] = start_date.strftime(TIME_FORMAT) return self def with_end_date(self, end_date: datetime) -> ConfigBuilder: - self._config["replication_end_date"] = end_date.isoformat()[:-13] + "Z" + self._config["replication_end_date"] = end_date.strftime(TIME_FORMAT) return self def build(self) -> Dict[str, str]: diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/pagination.py b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/pagination.py index f47a39e6655e..79b1528ed038 100644 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/pagination.py +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/pagination.py @@ -10,7 +10,7 @@ NEXT_TOKEN_STRING = "MDAwMDAwMDAwMQ==" -class VendorDirectFulfillmentShippingPaginationStrategy(PaginationStrategy): +class VendorFulfillmentPaginationStrategy(PaginationStrategy): def update(self, response: Dict[str, Any]) -> None: response["payload"]["pagination"] = {} response["payload"]["pagination"]["nextToken"] = NEXT_TOKEN_STRING diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/request_builder.py b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/request_builder.py index d0433259a582..81f4f922f8f2 100644 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/request_builder.py +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/request_builder.py @@ -52,6 +52,10 @@ def download_document_endpoint(cls, url: str) -> RequestBuilder: def vendor_direct_fulfillment_shipping_endpoint(cls) -> RequestBuilder: return cls("vendor/directFulfillment/shipping/v1/shippingLabels") + @classmethod + def vendor_orders_endpoint(cls) -> RequestBuilder: + return cls("vendor/orders/v1/purchaseOrders") + def __init__(self, resource: str) -> None: self._resource = resource self._base_url = "https://sellingpartnerapi-na.amazon.com" diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/test_report_based_streams.py b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/test_report_based_streams.py index 499f79330ae1..796d699d33d2 100644 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/test_report_based_streams.py +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/test_report_based_streams.py @@ -376,11 +376,11 @@ def test_given_report_status_fatal_when_read_then_exception_raised( ) output = self._read(stream_name, config(), expecting_exception=True) - assert output.errors[-1].trace.error.failure_type == FailureType.system_error - assert output.errors[-1].trace.error.internal_message == ( + assert output.errors[-1].trace.error.failure_type == FailureType.config_error + assert ( f"Failed to retrieve the report '{stream_name}' for period {CONFIG_START_DATE}-{CONFIG_END_DATE} " "due to Amazon Seller Partner platform issues. This will be read during the next sync." - ) + ) in output.errors[-1].trace.error.message @pytest.mark.parametrize( ("stream_name", "date_field", "expected_date_value"), diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/test_vendor_direct_fulfillment_shipping.py b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/test_vendor_direct_fulfillment_shipping.py index f267a106ddfc..f210a920bfeb 100644 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/test_vendor_direct_fulfillment_shipping.py +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/test_vendor_direct_fulfillment_shipping.py @@ -7,6 +7,7 @@ from typing import List, Optional import freezegun +import pendulum from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput from airbyte_cdk.test.mock_http import HttpMocker from airbyte_cdk.test.mock_http.response_builder import ( @@ -21,29 +22,38 @@ from airbyte_cdk.test.state_builder import StateBuilder from airbyte_protocol.models import AirbyteStateMessage, FailureType, SyncMode -from .config import NOW, ConfigBuilder -from .pagination import NEXT_TOKEN_STRING, VendorDirectFulfillmentShippingPaginationStrategy +from .config import NOW, TIME_FORMAT, ConfigBuilder +from .pagination import NEXT_TOKEN_STRING, VendorFulfillmentPaginationStrategy from .request_builder import RequestBuilder from .response_builder import response_with_status from .utils import config, mock_auth, read_output +_START_DATE = pendulum.datetime(year=2023, month=1, day=1) +_END_DATE = pendulum.datetime(year=2023, month=1, day=5) +_REPLICATION_START_FIELD = "createdAfter" +_REPLICATION_END_FIELD = "createdBefore" +_CURSOR_FIELD = "createdBefore" _STREAM_NAME = "VendorDirectFulfillmentShipping" -_TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" def _vendor_direct_fulfillment_shipping_request() -> RequestBuilder: - return RequestBuilder.vendor_direct_fulfillment_shipping_endpoint() + return RequestBuilder.vendor_direct_fulfillment_shipping_endpoint().with_query_params( + { + _REPLICATION_START_FIELD: _START_DATE.strftime(TIME_FORMAT), + _REPLICATION_END_FIELD: _END_DATE.strftime(TIME_FORMAT), + } + ) def _vendor_direct_fulfillment_shipping_response() -> HttpResponseBuilder: return create_response_builder( - find_template(_STREAM_NAME, __file__), - NestedPath(["payload", "shippingLabels"]), - pagination_strategy=VendorDirectFulfillmentShippingPaginationStrategy(), + response_template=find_template(_STREAM_NAME, __file__), + records_path=NestedPath(["payload", "shippingLabels"]), + pagination_strategy=VendorFulfillmentPaginationStrategy(), ) -def _a_shipping_label_record() -> RecordBuilder: +def _shipping_label_record() -> RecordBuilder: return create_record_builder( response_template=find_template(_STREAM_NAME, __file__), records_path=NestedPath(["payload", "shippingLabels"]), @@ -68,9 +78,10 @@ def test_given_one_page_when_read_then_return_records(self, http_mocker: HttpMoc mock_auth(http_mocker) http_mocker.get( _vendor_direct_fulfillment_shipping_request().build(), - _vendor_direct_fulfillment_shipping_response().with_record(_a_shipping_label_record()).build(), + _vendor_direct_fulfillment_shipping_response().with_record(_shipping_label_record()).build(), ) - output = self._read(config()) + + output = self._read(config().with_start_date(_START_DATE).with_end_date(_END_DATE)) assert len(output.records) == 1 @HttpMocker() @@ -79,19 +90,50 @@ def test_given_two_pages_when_read_then_return_records(self, http_mocker: HttpMo http_mocker.get( _vendor_direct_fulfillment_shipping_request().build(), _vendor_direct_fulfillment_shipping_response().with_pagination().with_record( - _a_shipping_label_record() + _shipping_label_record() ).build(), ) - query_params_with_next_page_token = {"nextToken": NEXT_TOKEN_STRING} + query_params_with_next_page_token = { + _REPLICATION_START_FIELD: _START_DATE.strftime(TIME_FORMAT), + _REPLICATION_END_FIELD: _END_DATE.strftime(TIME_FORMAT), + "nextToken": NEXT_TOKEN_STRING, + } http_mocker.get( _vendor_direct_fulfillment_shipping_request().with_query_params(query_params_with_next_page_token).build(), - _vendor_direct_fulfillment_shipping_response().with_record(_a_shipping_label_record()).with_record( - _a_shipping_label_record() + _vendor_direct_fulfillment_shipping_response().with_record(_shipping_label_record()).with_record( + _shipping_label_record() ).build(), ) - output = self._read(config()) + + output = self._read(config().with_start_date(_START_DATE).with_end_date(_END_DATE)) assert len(output.records) == 3 + @HttpMocker() + def test_given_two_slices_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + end_date = _START_DATE.add(days=8) + mock_auth(http_mocker) + + query_params_first_slice = { + _REPLICATION_START_FIELD: _START_DATE.strftime(TIME_FORMAT), + _REPLICATION_END_FIELD: _START_DATE.add(days=7).strftime(TIME_FORMAT), + } + http_mocker.get( + _vendor_direct_fulfillment_shipping_request().with_query_params(query_params_first_slice).build(), + _vendor_direct_fulfillment_shipping_response().with_record(_shipping_label_record()).build(), + ) + + query_params_second_slice = { + _REPLICATION_START_FIELD: query_params_first_slice[_REPLICATION_END_FIELD], + _REPLICATION_END_FIELD: end_date.strftime(TIME_FORMAT), + } + http_mocker.get( + _vendor_direct_fulfillment_shipping_request().with_query_params(query_params_second_slice).build(), + _vendor_direct_fulfillment_shipping_response().with_record(_shipping_label_record()).build(), + ) + + output = self._read(config().with_start_date(_START_DATE).with_end_date(end_date)) + assert len(output.records) == 2 + @HttpMocker() def test_given_http_status_500_then_200_when_read_then_retry_and_return_records( self, http_mocker: HttpMocker @@ -101,10 +143,11 @@ def test_given_http_status_500_then_200_when_read_then_retry_and_return_records( _vendor_direct_fulfillment_shipping_request().build(), [ response_with_status(status_code=HTTPStatus.INTERNAL_SERVER_ERROR), - _vendor_direct_fulfillment_shipping_response().with_record(_a_shipping_label_record()).build(), + _vendor_direct_fulfillment_shipping_response().with_record(_shipping_label_record()).build(), ], ) - output = self._read(config()) + + output = self._read(config().with_start_date(_START_DATE).with_end_date(_END_DATE)) assert len(output.records) == 1 @HttpMocker() @@ -116,15 +159,13 @@ def test_given_http_status_500_on_availability_when_read_then_raise_system_error _vendor_direct_fulfillment_shipping_request().build(), response_with_status(status_code=HTTPStatus.INTERNAL_SERVER_ERROR), ) - output = self._read(config(), expecting_exception=True) - assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + output = self._read(config().with_start_date(_START_DATE).with_end_date(_END_DATE), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.config_error @freezegun.freeze_time(NOW.isoformat()) class TestIncremental: - cursor_field = "createdBefore" - replication_start_date = NOW.subtract(days=3) - replication_end_date = NOW @staticmethod def _read( @@ -143,64 +184,58 @@ def test_when_read_then_add_cursor_field(self, http_mocker: HttpMocker) -> None: mock_auth(http_mocker) http_mocker.get( _vendor_direct_fulfillment_shipping_request().build(), - _vendor_direct_fulfillment_shipping_response().with_record(_a_shipping_label_record()).build(), + _vendor_direct_fulfillment_shipping_response().with_record(_shipping_label_record()).build(), ) - output = self._read( - config().with_start_date(self.replication_start_date).with_end_date(self.replication_end_date) - ) - - expected_cursor_value = self.replication_end_date.strftime(_TIME_FORMAT) - assert output.records[0].record.data[self.cursor_field] == expected_cursor_value + output = self._read(config().with_start_date(_START_DATE).with_end_date(_END_DATE)) + expected_cursor_value = _END_DATE.strftime(TIME_FORMAT) + assert output.records[0].record.data[_CURSOR_FIELD] == expected_cursor_value @HttpMocker() def test_when_read_then_state_message_produced_and_state_match_latest_record(self, http_mocker: HttpMocker) -> None: mock_auth(http_mocker) http_mocker.get( _vendor_direct_fulfillment_shipping_request().build(), - _vendor_direct_fulfillment_shipping_response().with_record(_a_shipping_label_record()).with_record( - _a_shipping_label_record() + _vendor_direct_fulfillment_shipping_response().with_record(_shipping_label_record()).with_record( + _shipping_label_record() ).build(), ) - output = self._read( - config().with_start_date(self.replication_start_date).with_end_date(self.replication_end_date) - ) + output = self._read(config().with_start_date(_START_DATE).with_end_date(_END_DATE)) assert len(output.state_messages) == 1 - cursor_value_from_state_message = output.most_recent_state.get(_STREAM_NAME, {}).get(self.cursor_field) - cursor_value_from_latest_record = output.records[-1].record.data.get(self.cursor_field) + cursor_value_from_state_message = output.most_recent_state.get(_STREAM_NAME, {}).get(_CURSOR_FIELD) + cursor_value_from_latest_record = output.records[-1].record.data.get(_CURSOR_FIELD) assert cursor_value_from_state_message == cursor_value_from_latest_record @HttpMocker() def test_given_state_when_read_then_state_value_is_created_after_query_param(self, http_mocker: HttpMocker) -> None: mock_auth(http_mocker) - state_value = self.replication_start_date.add(days=1).strftime(_TIME_FORMAT) + state_value = _START_DATE.add(days=1).strftime(TIME_FORMAT) query_params_first_read = { - "createdAfter": self.replication_start_date.strftime(_TIME_FORMAT), - self.cursor_field: self.replication_end_date.strftime(_TIME_FORMAT), + _REPLICATION_START_FIELD: _START_DATE.strftime(TIME_FORMAT), + _REPLICATION_END_FIELD: _END_DATE.strftime(TIME_FORMAT), } query_params_incremental_read = { - "createdAfter": state_value, self.cursor_field: self.replication_end_date.strftime(_TIME_FORMAT) + _REPLICATION_START_FIELD: state_value, _REPLICATION_END_FIELD: _END_DATE.strftime(TIME_FORMAT) } http_mocker.get( _vendor_direct_fulfillment_shipping_request().with_query_params(query_params_first_read).build(), - _vendor_direct_fulfillment_shipping_response().with_record(_a_shipping_label_record()).with_record( - _a_shipping_label_record() + _vendor_direct_fulfillment_shipping_response().with_record(_shipping_label_record()).with_record( + _shipping_label_record() ).build(), ) http_mocker.get( _vendor_direct_fulfillment_shipping_request().with_query_params(query_params_incremental_read).build(), - _vendor_direct_fulfillment_shipping_response().with_record(_a_shipping_label_record()).with_record( - _a_shipping_label_record() + _vendor_direct_fulfillment_shipping_response().with_record(_shipping_label_record()).with_record( + _shipping_label_record() ).build(), ) + output = self._read( - config_=config().with_start_date(self.replication_start_date).with_end_date(self.replication_end_date), - state=StateBuilder().with_stream_state(_STREAM_NAME, {self.cursor_field: state_value}).build(), + config_=config().with_start_date(_START_DATE).with_end_date(_END_DATE), + state=StateBuilder().with_stream_state(_STREAM_NAME, {_CURSOR_FIELD: state_value}).build(), ) - assert output.most_recent_state == { - _STREAM_NAME: {self.cursor_field: self.replication_end_date.strftime(_TIME_FORMAT)} - } + assert output.most_recent_state == {_STREAM_NAME: {_CURSOR_FIELD: _END_DATE.strftime(TIME_FORMAT)}} diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/test_vendor_orders.py b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/test_vendor_orders.py new file mode 100644 index 000000000000..4c2327cdffea --- /dev/null +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/integration/test_vendor_orders.py @@ -0,0 +1,228 @@ +# +# Copyright (c) 2024 Airbyte, Inc., all rights reserved. +# + + +from http import HTTPStatus +from typing import List, Optional + +import freezegun +import pendulum +from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput +from airbyte_cdk.test.mock_http import HttpMocker +from airbyte_cdk.test.mock_http.response_builder import ( + FieldPath, + HttpResponseBuilder, + NestedPath, + RecordBuilder, + create_record_builder, + create_response_builder, + find_template, +) +from airbyte_cdk.test.state_builder import StateBuilder +from airbyte_protocol.models import AirbyteStateMessage, FailureType, SyncMode + +from .config import NOW, TIME_FORMAT, ConfigBuilder +from .pagination import NEXT_TOKEN_STRING, VendorFulfillmentPaginationStrategy +from .request_builder import RequestBuilder +from .response_builder import response_with_status +from .utils import config, mock_auth, read_output + +_START_DATE = pendulum.datetime(year=2023, month=1, day=1) +_END_DATE = pendulum.datetime(year=2023, month=1, day=5) +_REPLICATION_START_FIELD = "changedAfter" +_REPLICATION_END_FIELD = "changedBefore" +_CURSOR_FIELD = "changedBefore" +_STREAM_NAME = "VendorOrders" + + +def _vendor_orders_request() -> RequestBuilder: + return RequestBuilder.vendor_orders_endpoint().with_query_params( + { + _REPLICATION_START_FIELD: _START_DATE.strftime(TIME_FORMAT), + _REPLICATION_END_FIELD: _END_DATE.strftime(TIME_FORMAT), + } + ) + + +def _vendor_orders_response() -> HttpResponseBuilder: + return create_response_builder( + response_template=find_template(_STREAM_NAME, __file__), + records_path=NestedPath(["payload", "orders"]), + pagination_strategy=VendorFulfillmentPaginationStrategy(), + ) + + +def _order_record() -> RecordBuilder: + return create_record_builder( + response_template=find_template(_STREAM_NAME, __file__), + records_path=NestedPath(["payload", "orders"]), + record_id_path=FieldPath("purchaseOrderNumber"), + ) + + +@freezegun.freeze_time(NOW.isoformat()) +class TestFullRefresh: + + @staticmethod + def _read(config_: ConfigBuilder, expecting_exception: bool = False) -> EntrypointOutput: + return read_output( + config_builder=config_, + stream_name=_STREAM_NAME, + sync_mode=SyncMode.full_refresh, + expecting_exception=expecting_exception, + ) + + @HttpMocker() + def test_given_one_page_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + mock_auth(http_mocker) + http_mocker.get( + _vendor_orders_request().build(), _vendor_orders_response().with_record(_order_record()).build() + ) + + output = self._read(config().with_start_date(_START_DATE).with_end_date(_END_DATE)) + assert len(output.records) == 1 + + @HttpMocker() + def test_given_two_pages_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + mock_auth(http_mocker) + http_mocker.get( + _vendor_orders_request().build(), + _vendor_orders_response().with_pagination().with_record(_order_record()).build(), + ) + query_params_with_next_page_token = { + _REPLICATION_START_FIELD: _START_DATE.strftime(TIME_FORMAT), + _REPLICATION_END_FIELD: _END_DATE.strftime(TIME_FORMAT), + "nextToken": NEXT_TOKEN_STRING, + } + http_mocker.get( + _vendor_orders_request().with_query_params(query_params_with_next_page_token).build(), + _vendor_orders_response().with_record(_order_record()).with_record(_order_record()).build(), + ) + + output = self._read(config().with_start_date(_START_DATE).with_end_date(_END_DATE)) + assert len(output.records) == 3 + + @HttpMocker() + def test_given_two_slices_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + end_date = _START_DATE.add(days=8) + mock_auth(http_mocker) + + query_params_first_slice = { + _REPLICATION_START_FIELD: _START_DATE.strftime(TIME_FORMAT), + _REPLICATION_END_FIELD: _START_DATE.add(days=7).strftime(TIME_FORMAT), + } + http_mocker.get( + _vendor_orders_request().with_query_params(query_params_first_slice).build(), + _vendor_orders_response().with_record(_order_record()).build(), + ) + + query_params_second_slice = { + _REPLICATION_START_FIELD: query_params_first_slice[_REPLICATION_END_FIELD], + _REPLICATION_END_FIELD: end_date.strftime(TIME_FORMAT), + } + http_mocker.get( + _vendor_orders_request().with_query_params(query_params_second_slice).build(), + _vendor_orders_response().with_record(_order_record()).build(), + ) + + output = self._read(config().with_start_date(_START_DATE).with_end_date(end_date)) + assert len(output.records) == 2 + + @HttpMocker() + def test_given_http_status_500_then_200_when_read_then_retry_and_return_records( + self, http_mocker: HttpMocker + ) -> None: + mock_auth(http_mocker) + http_mocker.get( + _vendor_orders_request().build(), + [ + response_with_status(status_code=HTTPStatus.INTERNAL_SERVER_ERROR), + _vendor_orders_response().with_record(_order_record()).build(), + ], + ) + + output = self._read(config().with_start_date(_START_DATE).with_end_date(_END_DATE)) + assert len(output.records) == 1 + + @HttpMocker() + def test_given_http_status_500_on_availability_when_read_then_raise_system_error( + self, http_mocker: HttpMocker + ) -> None: + mock_auth(http_mocker) + http_mocker.get( + _vendor_orders_request().build(), response_with_status(status_code=HTTPStatus.INTERNAL_SERVER_ERROR) + ) + + output = self._read(config().with_start_date(_START_DATE).with_end_date(_END_DATE), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.config_error + + +@freezegun.freeze_time(NOW.isoformat()) +class TestIncremental: + + @staticmethod + def _read( + config_: ConfigBuilder, state: Optional[List[AirbyteStateMessage]] = None, expecting_exception: bool = False + ) -> EntrypointOutput: + return read_output( + config_builder=config_, + stream_name=_STREAM_NAME, + sync_mode=SyncMode.incremental, + state=state, + expecting_exception=expecting_exception, + ) + + @HttpMocker() + def test_when_read_then_add_cursor_field(self, http_mocker: HttpMocker) -> None: + mock_auth(http_mocker) + http_mocker.get( + _vendor_orders_request().build(), _vendor_orders_response().with_record(_order_record()).build() + ) + + output = self._read(config().with_start_date(_START_DATE).with_end_date(_END_DATE)) + expected_cursor_value = _END_DATE.strftime(TIME_FORMAT) + assert output.records[0].record.data[_CURSOR_FIELD] == expected_cursor_value + + @HttpMocker() + def test_when_read_then_state_message_produced_and_state_match_latest_record(self, http_mocker: HttpMocker) -> None: + mock_auth(http_mocker) + http_mocker.get( + _vendor_orders_request().build(), + _vendor_orders_response().with_record(_order_record()).with_record(_order_record()).build(), + ) + + output = self._read(config().with_start_date(_START_DATE).with_end_date(_END_DATE)) + assert len(output.state_messages) == 1 + + cursor_value_from_state_message = output.most_recent_state.get(_STREAM_NAME, {}).get(_CURSOR_FIELD) + cursor_value_from_latest_record = output.records[-1].record.data.get(_CURSOR_FIELD) + assert cursor_value_from_state_message == cursor_value_from_latest_record + + @HttpMocker() + def test_given_state_when_read_then_state_value_is_created_after_query_param(self, http_mocker: HttpMocker) -> None: + mock_auth(http_mocker) + state_value = _START_DATE.add(days=1).strftime(TIME_FORMAT) + + query_params_first_read = { + _REPLICATION_START_FIELD: _START_DATE.strftime(TIME_FORMAT), + _REPLICATION_END_FIELD: _END_DATE.strftime(TIME_FORMAT), + } + query_params_incremental_read = { + _REPLICATION_START_FIELD: state_value, _REPLICATION_END_FIELD: _END_DATE.strftime(TIME_FORMAT) + } + + http_mocker.get( + _vendor_orders_request().with_query_params(query_params_first_read).build(), + _vendor_orders_response().with_record(_order_record()).with_record(_order_record()).build(), + ) + http_mocker.get( + _vendor_orders_request().with_query_params(query_params_incremental_read).build(), + _vendor_orders_response().with_record(_order_record()).with_record(_order_record()).build(), + ) + + output = self._read( + config_=config().with_start_date(_START_DATE).with_end_date(_END_DATE), + state=StateBuilder().with_stream_state(_STREAM_NAME, {_CURSOR_FIELD: state_value}).build(), + ) + assert output.most_recent_state == {_STREAM_NAME: {_CURSOR_FIELD: _END_DATE.strftime(TIME_FORMAT)}} diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/resource/http/response/VendorOrders.json b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/resource/http/response/VendorOrders.json new file mode 100644 index 000000000000..b3426ef45d04 --- /dev/null +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/resource/http/response/VendorOrders.json @@ -0,0 +1,90 @@ +{ + "payload": { + "orders": [ + { + "purchaseOrderNumber": "L8266355", + "purchaseOrderState": "New", + "orderDetails": { + "purchaseOrderDate": "2019-05-23T10:00:00Z", + "purchaseOrderChangedDate": "2019-05-24T16:05:00Z", + "purchaseOrderStateChangedDate": "2019-05-23T10:00:00Z", + "purchaseOrderType": "RegularOrder", + "importDetails": { + "methodOfPayment": "PaidByBuyer", + "internationalCommercialTerms": "ExWorks", + "portOfDelivery": "YANTIAN, CHINA", + "importContainers": "1-40'HC, 1-20'", + "shippingInstructions": "PREFERENCE IS PALLET-LOAD, BUT IF CONTAINERS ARE FLOOR-LOADED, THEN PLEASE DO CLAMP-LOAD OR STRAIGHT FLOOR-LOAD. DO NOT USE SLIP SHEET FOR THIS FC DESTINATION. PAYMENT TERMS ARE PER CONTAINER." + }, + "dealCode": "BTS", + "paymentMethod": "Invoice", + "buyingParty": { + "partyId": "ABCD", + "address": { + "name": "APPARIO RETAIL PVT.LTD.", + "addressLine1": "3APPARIO RETAIL PVT.LTD.- C/O. AMAZON SELLER SERVIC", + "city": "Siddhapudur", + "stateOrRegion": "Tamil Nadu", + "postalCode": "641044", + "countryCode": "IN", + "phone": "206-266-8000" + } + }, + "sellingParty": { + "partyId": "TEST1" + }, + "shipToParty": { + "partyId": "ABCD", + "address": { + "name": "APPARIO RETAIL PVT.LTD.", + "addressLine1": "3APPARIO RETAIL PVT.LTD.- C/O. AMAZON SELLER SERVIC", + "city": "Siddhapudur", + "stateOrRegion": "Tamil Nadu", + "postalCode": "641044", + "countryCode": "IN", + "phone": "206-266-8000" + } + }, + "billToParty": { + "partyId": "ABCD", + "address": { + "name": "APPARIO RETAIL PVT.LTD.", + "addressLine1": "3APPARIO RETAIL PVT.LTD.- C/O. AMAZON SELLER SERVIC", + "city": "Siddhapudur", + "stateOrRegion": "Tamil Nadu", + "postalCode": "641044", + "countryCode": "IN", + "phone": "206-266-8000" + } + }, + "taxInfo": { + "taxType": "GST", + "taxRegistrationNumber": "098522PCA6346DTEDD" + } + }, + "deliveryWindow": "2019-05-23T10:00:00Z--2019-05-30T10:00:00Z", + "items": [ + { + "itemSequenceNumber": "1", + "amazonProductIdentifier": "ABC123434", + "vendorProductIdentifier": "028877454078", + "orderedQuantity": { + "amount": 2, + "unitOfMeasure": "Cases", + "unitSize": 10 + }, + "isBackOrderAllowed": true, + "netCost": { + "amount": "1800", + "currencyCode": "INR" + }, + "listPrice": { + "amount": "2000", + "currencyCode": "INR" + } + } + ] + } + ] + } +} diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/test_streams.py b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/test_streams.py index 982324f3282c..5a3a86188f53 100644 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/test_streams.py +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/unit_tests/test_streams.py @@ -131,7 +131,8 @@ def test_read_records_retrieve_fatal(self, report_init_kwargs, mocker, requests_ with pytest.raises(AirbyteTracedException) as e: list( stream.read_records( - sync_mode=SyncMode.full_refresh, stream_slice={"dataStartTime": stream_start, "dataEndTime": stream_end} + sync_mode=SyncMode.full_refresh, + stream_slice={"dataStartTime": stream_start, "dataEndTime": stream_end}, ) ) assert e.value.internal_message == ( @@ -164,7 +165,7 @@ def test_read_records_retrieve_cancelled(self, report_init_kwargs, mocker, reque stream = SomeReportStream(**report_init_kwargs) list(stream.read_records(sync_mode=SyncMode.full_refresh)) - assert "The report for stream 'GET_TEST_REPORT' was cancelled or there is no data to return" in caplog.messages[-1] + assert "The report for stream 'GET_TEST_REPORT' was cancelled or there is no data" in caplog.messages[-1] def test_read_records_retrieve_done(self, report_init_kwargs, mocker, requests_mock): mocker.patch("time.sleep", lambda x: None) @@ -187,7 +188,11 @@ def test_read_records_retrieve_done(self, report_init_kwargs, mocker, requests_m "GET", f"https://test.url/reports/2021-06-30/reports/{report_id}", status_code=200, - json={"processingStatus": ReportProcessingStatus.DONE, "dataEndTime": "2022-10-03T00:00:00Z", "reportDocumentId": document_id}, + json={ + "processingStatus": ReportProcessingStatus.DONE, + "dataEndTime": "2022-10-03T00:00:00Z", + "reportDocumentId": document_id, + }, ) requests_mock.register_uri( "GET", @@ -212,7 +217,11 @@ def test_read_records_retrieve_forbidden(self, report_init_kwargs, mocker, reque report_id = "some_report_id" requests_mock.register_uri( - "POST", "https://test.url/reports/2021-06-30/reports", status_code=403, json={"reportId": report_id}, reason="Forbidden" + "POST", + "https://test.url/reports/2021-06-30/reports", + status_code=403, + json={"reportId": report_id}, + reason="Forbidden", ) stream = SomeReportStream(**report_init_kwargs) @@ -224,23 +233,74 @@ def test_read_records_retrieve_forbidden(self, report_init_kwargs, mocker, reque ) in caplog.messages[-1] -class TestVendorDirectFulfillmentShipping: +class TestVendorFulfillment: @pytest.mark.parametrize( - ("start_date", "end_date", "expected_params"), + ("start_date", "end_date", "stream_state", "expected_slices"), ( - ("2022-09-01T00:00:00Z", None, {"createdAfter": "2022-09-01T00:00:00Z", "createdBefore": "2022-09-05T00:00:00Z"}), - ("2022-08-01T00:00:00Z", None, {"createdAfter": "2022-08-28T23:00:00Z", "createdBefore": "2022-09-05T00:00:00Z"}), ( "2022-09-01T00:00:00Z", - "2022-09-05T00:00:00Z", - {"createdAfter": "2022-09-01T00:00:00Z", "createdBefore": "2022-09-05T00:00:00Z"}, + None, + None, + [{"createdAfter": "2022-09-01T00:00:00Z", "createdBefore": "2022-09-05T00:00:00Z"}], + ), + ( + "2022-08-01T00:00:00Z", + "2022-08-16T00:00:00Z", + None, + [ + {"createdAfter": "2022-08-01T00:00:00Z", "createdBefore": "2022-08-08T00:00:00Z"}, + {"createdAfter": "2022-08-08T00:00:00Z", "createdBefore": "2022-08-15T00:00:00Z"}, + {"createdAfter": "2022-08-15T00:00:00Z", "createdBefore": "2022-08-16T00:00:00Z"}, + ], + ), + ( + "2022-08-01T00:00:00Z", + "2022-08-05T00:00:00Z", + None, + [{"createdAfter": "2022-08-01T00:00:00Z", "createdBefore": "2022-08-05T00:00:00Z"}], ), + ( + "2022-08-01T00:00:00Z", + "2022-08-11T00:00:00Z", + {"createdBefore": "2022-08-05T00:00:00Z"}, + [{"createdAfter": "2022-08-05T00:00:00Z", "createdBefore": "2022-08-11T00:00:00Z"}], + ), + ("2022-08-01T00:00:00Z", "2022-08-05T00:00:00Z", {"createdBefore": "2022-08-06T00:00:00Z"}, []), ), ) - def test_request_params(self, report_init_kwargs, start_date, end_date, expected_params): + def test_stream_slices(self, report_init_kwargs, start_date, end_date, stream_state, expected_slices): report_init_kwargs["replication_start_date"] = start_date report_init_kwargs["replication_end_date"] = end_date stream = VendorDirectFulfillmentShipping(**report_init_kwargs) with patch("pendulum.now", return_value=pendulum.parse("2022-09-05T00:00:00Z")): - assert stream.request_params(stream_state={}) == expected_params + assert list( + stream.stream_slices(sync_mode=SyncMode.full_refresh, stream_state=stream_state) + ) == expected_slices + + @pytest.mark.parametrize( + ("stream_slice", "next_page_token", "expected_params"), + ( + ( + {"createdAfter": "2022-08-05T00:00:00Z", "createdBefore": "2022-08-11T00:00:00Z"}, + None, + {"createdAfter": "2022-08-05T00:00:00Z", "createdBefore": "2022-08-11T00:00:00Z"}, + ), + ( + {"createdAfter": "2022-08-05T00:00:00Z", "createdBefore": "2022-08-11T00:00:00Z"}, + {"nextToken": "123123123"}, + { + "createdAfter": "2022-08-05T00:00:00Z", + "createdBefore": "2022-08-11T00:00:00Z", + "nextToken": "123123123", + }, + ), + (None, {"nextToken": "123123123"}, {"nextToken": "123123123"}), + (None, None, {}), + ) + ) + def test_request_params(self, report_init_kwargs, stream_slice, next_page_token, expected_params): + stream = VendorDirectFulfillmentShipping(**report_init_kwargs) + assert stream.request_params( + stream_state={}, stream_slice=stream_slice, next_page_token=next_page_token + ) == expected_params diff --git a/docs/integrations/sources/amazon-seller-partner.md b/docs/integrations/sources/amazon-seller-partner.md index 961a90292450..1ceaa780c971 100644 --- a/docs/integrations/sources/amazon-seller-partner.md +++ b/docs/integrations/sources/amazon-seller-partner.md @@ -136,6 +136,7 @@ The Amazon Seller Partner source connector supports the following [sync modes](h - [Vendor Sales Report](https://developer-docs.amazon.com/sp-api/docs/report-type-values-analytics#vendor-retail-analytics-reports) \(incremental\) - [Vendor Traffic Report](https://developer-docs.amazon.com/sp-api/docs/report-type-values-analytics#vendor-retail-analytics-reports) \(incremental\) - [XML Orders By Order Date Report](https://developer-docs.amazon.com/sp-api/docs/report-type-values-order#order-tracking-reports) \(incremental\) +- [Vendor Orders](https://developer-docs.amazon.com/sp-api/docs/vendor-orders-api-v1-reference#get-vendorordersv1purchaseorders) \(incremental\) ## Report options @@ -163,70 +164,71 @@ Information about rate limits you may find [here](https://developer-docs.amazon. ## Changelog -| Version | Date | Pull Request | Subject | -|:---------|:-----------|:------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `3.3.2` | 2024-02-13 | [\#33996](https://github.com/airbytehq/airbyte/pull/33996) | Add integration tests | -| `3.3.1` | 2024-02-09 | [\#35106](https://github.com/airbytehq/airbyte/pull/35106) | Add logs for the failed check command | -| `3.3.0` | 2024-02-09 | [\#35062](https://github.com/airbytehq/airbyte/pull/35062) | Fix the check command for the `Vendor` account type | -| `3.2.2` | 2024-02-07 | [\#34914](https://github.com/airbytehq/airbyte/pull/34914) | Fix date formatting for ledger reports with aggregation by month | -| `3.2.1` | 2024-01-30 | [\#34654](https://github.com/airbytehq/airbyte/pull/34654) | Fix date format in state message for streams with custom dates formatting | -| `3.2.0` | 2024-01-26 | [\#34549](https://github.com/airbytehq/airbyte/pull/34549) | Update schemas for vendor analytics streams | -| `3.1.0` | 2024-01-17 | [\#34283](https://github.com/airbytehq/airbyte/pull/34283) | Delete deprecated streams | -| `3.0.1` | 2023-12-22 | [\#33741](https://github.com/airbytehq/airbyte/pull/33741) | Improve report streams performance | -| `3.0.0` | 2023-12-12 | [\#32977](https://github.com/airbytehq/airbyte/pull/32977) | Make all streams incremental | -| `2.5.0` | 2023-11-27 | [\#32505](https://github.com/airbytehq/airbyte/pull/32505) | Make report options configurable via UI | -| `2.4.0` | 2023-11-23 | [\#32738](https://github.com/airbytehq/airbyte/pull/32738) | Add `GET_VENDOR_NET_PURE_PRODUCT_MARGIN_REPORT`, `GET_VENDOR_REAL_TIME_INVENTORY_REPORT`, and `GET_VENDOR_TRAFFIC_REPORT` streams | -| `2.3.0` | 2023-11-22 | [\#32541](https://github.com/airbytehq/airbyte/pull/32541) | Make `GET_AFN_INVENTORY_DATA`, `GET_AFN_INVENTORY_DATA_BY_COUNTRY`, and `GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE` streams incremental | -| `2.2.0` | 2023-11-21 | [\#32639](https://github.com/airbytehq/airbyte/pull/32639) | Make start date optional, if start date is not provided, date 2 years ago from today will be used | -| `2.1.1` | 2023-11-21 | [\#32560](https://github.com/airbytehq/airbyte/pull/32560) | Silently exit sync if the retry attempts were unsuccessful | -| `2.1.0` | 2023-11-21 | [\#32591](https://github.com/airbytehq/airbyte/pull/32591) | Add new fields to GET_LEDGER_DETAIL_VIEW_DATA, GET_FBA_INVENTORY_PLANNING_DATA and Orders schemas | -| `2.0.2` | 2023-11-17 | [\#32462](https://github.com/airbytehq/airbyte/pull/32462) | Remove Max time option from specification; set default waiting time for reports to 1 hour | -| `2.0.1` | 2023-11-16 | [\#32550](https://github.com/airbytehq/airbyte/pull/32550) | Fix the OAuth flow | -| `2.0.0` | 2023-11-23 | [\#32355](https://github.com/airbytehq/airbyte/pull/32355) | Remove Brand Analytics from Airbyte Cloud, permanently remove deprecated FBA reports | -| `1.6.2` | 2023-11-14 | [\#32508](https://github.com/airbytehq/airbyte/pull/32508) | Do not use AWS signature as it is no longer required by the Amazon API | -| `1.6.1` | 2023-11-13 | [\#32457](https://github.com/airbytehq/airbyte/pull/32457) | Fix report decompression | -| `1.6.0` | 2023-11-09 | [\#32259](https://github.com/airbytehq/airbyte/pull/32259) | mark "aws_secret_key" and "aws_access_key" as required in specification; update schema for stream `Orders` | -| `1.5.1` | 2023-08-18 | [\#29255](https://github.com/airbytehq/airbyte/pull/29255) | role_arn is optional on UI but not really on the backend blocking connector set up using oauth | -| `1.5.0` | 2023-08-08 | [\#29054](https://github.com/airbytehq/airbyte/pull/29054) | Add new stream `OrderItems` | -| `1.4.1` | 2023-07-25 | [\#27050](https://github.com/airbytehq/airbyte/pull/27050) | Fix - non vendor accounts connector create/check issue | -| `1.4.0` | 2023-07-21 | [\#27110](https://github.com/airbytehq/airbyte/pull/27110) | Add `GET_FLAT_FILE_ACTIONABLE_ORDER_DATA_SHIPPING` and `GET_ORDER_REPORT_DATA_SHIPPING` streams | -| `1.3.0` | 2023-06-09 | [\#27110](https://github.com/airbytehq/airbyte/pull/27110) | Removed `app_id` from `InputConfiguration`, refactored `spec` | -| `1.2.0` | 2023-05-23 | [\#22503](https://github.com/airbytehq/airbyte/pull/22503) | Enabled stream attribute customization from Source configuration | -| `1.1.0` | 2023-04-21 | [\#23605](https://github.com/airbytehq/airbyte/pull/23605) | Add FBA Reimbursement Report stream | -| `1.0.1` | 2023-03-15 | [\#24098](https://github.com/airbytehq/airbyte/pull/24098) | Add Belgium Marketplace | -| `1.0.0` | 2023-03-13 | [\#23980](https://github.com/airbytehq/airbyte/pull/23980) | Make `app_id` required. Increase `end_date` gap up to 5 minutes from now for Finance streams. Fix connection check failure when trying to connect to Amazon Vendor Central accounts | -| `0.2.33` | 2023-03-01 | [\#23606](https://github.com/airbytehq/airbyte/pull/23606) | Implement reportOptions for all missing reports and refactor | -| `0.2.32` | 2022-02-21 | [\#23300](https://github.com/airbytehq/airbyte/pull/23300) | Make AWS Access Key, AWS Secret Access and Role ARN optional | -| `0.2.31` | 2022-01-10 | [\#16430](https://github.com/airbytehq/airbyte/pull/16430) | Implement slicing for report streams | -| `0.2.30` | 2022-12-28 | [\#20896](https://github.com/airbytehq/airbyte/pull/20896) | Validate connections without orders data | -| `0.2.29` | 2022-11-18 | [\#19581](https://github.com/airbytehq/airbyte/pull/19581) | Use user provided end date for GET_SALES_AND_TRAFFIC_REPORT | -| `0.2.28` | 2022-10-20 | [\#18283](https://github.com/airbytehq/airbyte/pull/18283) | Added multiple (22) report types | -| `0.2.26` | 2022-09-24 | [\#16629](https://github.com/airbytehq/airbyte/pull/16629) | Report API version to 2021-06-30, added multiple (5) report types | -| `0.2.25` | 2022-07-27 | [\#15063](https://github.com/airbytehq/airbyte/pull/15063) | Add Restock Inventory Report | -| `0.2.24` | 2022-07-12 | [\#14625](https://github.com/airbytehq/airbyte/pull/14625) | Add FBA Storage Fees Report | -| `0.2.23` | 2022-06-08 | [\#13604](https://github.com/airbytehq/airbyte/pull/13604) | Add new streams: Fullfiments returns and Settlement reports | -| `0.2.22` | 2022-06-15 | [\#13633](https://github.com/airbytehq/airbyte/pull/13633) | Fix - handle start date for financial stream | -| `0.2.21` | 2022-06-01 | [\#13364](https://github.com/airbytehq/airbyte/pull/13364) | Add financial streams | -| `0.2.20` | 2022-05-30 | [\#13059](https://github.com/airbytehq/airbyte/pull/13059) | Add replication end date to config | -| `0.2.19` | 2022-05-24 | [\#13119](https://github.com/airbytehq/airbyte/pull/13119) | Add OAuth2.0 support | -| `0.2.18` | 2022-05-06 | [\#12663](https://github.com/airbytehq/airbyte/pull/12663) | Add GET_XML_BROWSE_TREE_DATA report | -| `0.2.17` | 2022-05-19 | [\#12946](https://github.com/airbytehq/airbyte/pull/12946) | Add throttling exception managing in Orders streams | -| `0.2.16` | 2022-05-04 | [\#12523](https://github.com/airbytehq/airbyte/pull/12523) | allow to use IAM user arn or IAM role | -| `0.2.15` | 2022-01-25 | [\#9789](https://github.com/airbytehq/airbyte/pull/9789) | Add stream FbaReplacementsReports | -| `0.2.14` | 2022-01-19 | [\#9621](https://github.com/airbytehq/airbyte/pull/9621) | Add GET_FLAT_FILE_ALL_ORDERS_DATA_BY_LAST_UPDATE_GENERAL report | -| `0.2.13` | 2022-01-18 | [\#9581](https://github.com/airbytehq/airbyte/pull/9581) | Change createdSince parameter to dataStartTime | -| `0.2.12` | 2022-01-05 | [\#9312](https://github.com/airbytehq/airbyte/pull/9312) | Add all remaining brand analytics report streams | -| `0.2.11` | 2022-01-05 | [\#9115](https://github.com/airbytehq/airbyte/pull/9115) | Fix reading only 100 orders | -| `0.2.10` | 2021-12-31 | [\#9236](https://github.com/airbytehq/airbyte/pull/9236) | Fix NoAuth deprecation warning | -| `0.2.9` | 2021-12-30 | [\#9212](https://github.com/airbytehq/airbyte/pull/9212) | Normalize GET_SELLER_FEEDBACK_DATA header field names | -| `0.2.8` | 2021-12-22 | [\#8810](https://github.com/airbytehq/airbyte/pull/8810) | Fix GET_SELLER_FEEDBACK_DATA Date cursor field format | -| `0.2.7` | 2021-12-21 | [\#9002](https://github.com/airbytehq/airbyte/pull/9002) | Extract REPORTS_MAX_WAIT_SECONDS to configurable parameter | -| `0.2.6` | 2021-12-10 | [\#8179](https://github.com/airbytehq/airbyte/pull/8179) | Add GET_BRAND_ANALYTICS_SEARCH_TERMS_REPORT report | -| `0.2.5` | 2021-12-06 | [\#8425](https://github.com/airbytehq/airbyte/pull/8425) | Update title, description fields in spec | -| `0.2.4` | 2021-11-08 | [\#8021](https://github.com/airbytehq/airbyte/pull/8021) | Added GET_SELLER_FEEDBACK_DATA report with incremental sync capability | -| `0.2.3` | 2021-11-08 | [\#7828](https://github.com/airbytehq/airbyte/pull/7828) | Remove datetime format from all streams | -| `0.2.2` | 2021-11-08 | [\#7752](https://github.com/airbytehq/airbyte/pull/7752) | Change `check_connection` function to use stream Orders | -| `0.2.1` | 2021-09-17 | [\#5248](https://github.com/airbytehq/airbyte/pull/5248) | Added `extra stream` support. Updated `reports streams` logics | -| `0.2.0` | 2021-08-06 | [\#4863](https://github.com/airbytehq/airbyte/pull/4863) | Rebuild source with `airbyte-cdk` | -| `0.1.3` | 2021-06-23 | [\#4288](https://github.com/airbytehq/airbyte/pull/4288) | Bugfix failing `connection check` | -| `0.1.2` | 2021-06-15 | [\#4108](https://github.com/airbytehq/airbyte/pull/4108) | Fixed: Sync fails with timeout when create report is CANCELLED` | +| Version | Date | Pull Request | Subject | +|:---------|:-----------|:------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `3.4.0` | 2024-02-15 | [\#35273](https://github.com/airbytehq/airbyte/pull/35273) | Add `VendorOrders` stream | +| `3.3.2` | 2024-02-13 | [\#33996](https://github.com/airbytehq/airbyte/pull/33996) | Add integration tests | +| `3.3.1` | 2024-02-09 | [\#35106](https://github.com/airbytehq/airbyte/pull/35106) | Add logs for the failed check command | +| `3.3.0` | 2024-02-09 | [\#35062](https://github.com/airbytehq/airbyte/pull/35062) | Fix the check command for the `Vendor` account type | +| `3.2.2` | 2024-02-07 | [\#34914](https://github.com/airbytehq/airbyte/pull/34914) | Fix date formatting for ledger reports with aggregation by month | +| `3.2.1` | 2024-01-30 | [\#34654](https://github.com/airbytehq/airbyte/pull/34654) | Fix date format in state message for streams with custom dates formatting | +| `3.2.0` | 2024-01-26 | [\#34549](https://github.com/airbytehq/airbyte/pull/34549) | Update schemas for vendor analytics streams | +| `3.1.0` | 2024-01-17 | [\#34283](https://github.com/airbytehq/airbyte/pull/34283) | Delete deprecated streams | +| `3.0.1` | 2023-12-22 | [\#33741](https://github.com/airbytehq/airbyte/pull/33741) | Improve report streams performance | +| `3.0.0` | 2023-12-12 | [\#32977](https://github.com/airbytehq/airbyte/pull/32977) | Make all streams incremental | +| `2.5.0` | 2023-11-27 | [\#32505](https://github.com/airbytehq/airbyte/pull/32505) | Make report options configurable via UI | +| `2.4.0` | 2023-11-23 | [\#32738](https://github.com/airbytehq/airbyte/pull/32738) | Add `GET_VENDOR_NET_PURE_PRODUCT_MARGIN_REPORT`, `GET_VENDOR_REAL_TIME_INVENTORY_REPORT`, and `GET_VENDOR_TRAFFIC_REPORT` streams | +| `2.3.0` | 2023-11-22 | [\#32541](https://github.com/airbytehq/airbyte/pull/32541) | Make `GET_AFN_INVENTORY_DATA`, `GET_AFN_INVENTORY_DATA_BY_COUNTRY`, and `GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE` streams incremental | +| `2.2.0` | 2023-11-21 | [\#32639](https://github.com/airbytehq/airbyte/pull/32639) | Make start date optional, if start date is not provided, date 2 years ago from today will be used | +| `2.1.1` | 2023-11-21 | [\#32560](https://github.com/airbytehq/airbyte/pull/32560) | Silently exit sync if the retry attempts were unsuccessful | +| `2.1.0` | 2023-11-21 | [\#32591](https://github.com/airbytehq/airbyte/pull/32591) | Add new fields to GET_LEDGER_DETAIL_VIEW_DATA, GET_FBA_INVENTORY_PLANNING_DATA and Orders schemas | +| `2.0.2` | 2023-11-17 | [\#32462](https://github.com/airbytehq/airbyte/pull/32462) | Remove Max time option from specification; set default waiting time for reports to 1 hour | +| `2.0.1` | 2023-11-16 | [\#32550](https://github.com/airbytehq/airbyte/pull/32550) | Fix the OAuth flow | +| `2.0.0` | 2023-11-23 | [\#32355](https://github.com/airbytehq/airbyte/pull/32355) | Remove Brand Analytics from Airbyte Cloud, permanently remove deprecated FBA reports | +| `1.6.2` | 2023-11-14 | [\#32508](https://github.com/airbytehq/airbyte/pull/32508) | Do not use AWS signature as it is no longer required by the Amazon API | +| `1.6.1` | 2023-11-13 | [\#32457](https://github.com/airbytehq/airbyte/pull/32457) | Fix report decompression | +| `1.6.0` | 2023-11-09 | [\#32259](https://github.com/airbytehq/airbyte/pull/32259) | mark "aws_secret_key" and "aws_access_key" as required in specification; update schema for stream `Orders` | +| `1.5.1` | 2023-08-18 | [\#29255](https://github.com/airbytehq/airbyte/pull/29255) | role_arn is optional on UI but not really on the backend blocking connector set up using oauth | +| `1.5.0` | 2023-08-08 | [\#29054](https://github.com/airbytehq/airbyte/pull/29054) | Add new stream `OrderItems` | +| `1.4.1` | 2023-07-25 | [\#27050](https://github.com/airbytehq/airbyte/pull/27050) | Fix - non vendor accounts connector create/check issue | +| `1.4.0` | 2023-07-21 | [\#27110](https://github.com/airbytehq/airbyte/pull/27110) | Add `GET_FLAT_FILE_ACTIONABLE_ORDER_DATA_SHIPPING` and `GET_ORDER_REPORT_DATA_SHIPPING` streams | +| `1.3.0` | 2023-06-09 | [\#27110](https://github.com/airbytehq/airbyte/pull/27110) | Removed `app_id` from `InputConfiguration`, refactored `spec` | +| `1.2.0` | 2023-05-23 | [\#22503](https://github.com/airbytehq/airbyte/pull/22503) | Enabled stream attribute customization from Source configuration | +| `1.1.0` | 2023-04-21 | [\#23605](https://github.com/airbytehq/airbyte/pull/23605) | Add FBA Reimbursement Report stream | +| `1.0.1` | 2023-03-15 | [\#24098](https://github.com/airbytehq/airbyte/pull/24098) | Add Belgium Marketplace | +| `1.0.0` | 2023-03-13 | [\#23980](https://github.com/airbytehq/airbyte/pull/23980) | Make `app_id` required. Increase `end_date` gap up to 5 minutes from now for Finance streams. Fix connection check failure when trying to connect to Amazon Vendor Central accounts | +| `0.2.33` | 2023-03-01 | [\#23606](https://github.com/airbytehq/airbyte/pull/23606) | Implement reportOptions for all missing reports and refactor | +| `0.2.32` | 2022-02-21 | [\#23300](https://github.com/airbytehq/airbyte/pull/23300) | Make AWS Access Key, AWS Secret Access and Role ARN optional | +| `0.2.31` | 2022-01-10 | [\#16430](https://github.com/airbytehq/airbyte/pull/16430) | Implement slicing for report streams | +| `0.2.30` | 2022-12-28 | [\#20896](https://github.com/airbytehq/airbyte/pull/20896) | Validate connections without orders data | +| `0.2.29` | 2022-11-18 | [\#19581](https://github.com/airbytehq/airbyte/pull/19581) | Use user provided end date for GET_SALES_AND_TRAFFIC_REPORT | +| `0.2.28` | 2022-10-20 | [\#18283](https://github.com/airbytehq/airbyte/pull/18283) | Added multiple (22) report types | +| `0.2.26` | 2022-09-24 | [\#16629](https://github.com/airbytehq/airbyte/pull/16629) | Report API version to 2021-06-30, added multiple (5) report types | +| `0.2.25` | 2022-07-27 | [\#15063](https://github.com/airbytehq/airbyte/pull/15063) | Add Restock Inventory Report | +| `0.2.24` | 2022-07-12 | [\#14625](https://github.com/airbytehq/airbyte/pull/14625) | Add FBA Storage Fees Report | +| `0.2.23` | 2022-06-08 | [\#13604](https://github.com/airbytehq/airbyte/pull/13604) | Add new streams: Fullfiments returns and Settlement reports | +| `0.2.22` | 2022-06-15 | [\#13633](https://github.com/airbytehq/airbyte/pull/13633) | Fix - handle start date for financial stream | +| `0.2.21` | 2022-06-01 | [\#13364](https://github.com/airbytehq/airbyte/pull/13364) | Add financial streams | +| `0.2.20` | 2022-05-30 | [\#13059](https://github.com/airbytehq/airbyte/pull/13059) | Add replication end date to config | +| `0.2.19` | 2022-05-24 | [\#13119](https://github.com/airbytehq/airbyte/pull/13119) | Add OAuth2.0 support | +| `0.2.18` | 2022-05-06 | [\#12663](https://github.com/airbytehq/airbyte/pull/12663) | Add GET_XML_BROWSE_TREE_DATA report | +| `0.2.17` | 2022-05-19 | [\#12946](https://github.com/airbytehq/airbyte/pull/12946) | Add throttling exception managing in Orders streams | +| `0.2.16` | 2022-05-04 | [\#12523](https://github.com/airbytehq/airbyte/pull/12523) | allow to use IAM user arn or IAM role | +| `0.2.15` | 2022-01-25 | [\#9789](https://github.com/airbytehq/airbyte/pull/9789) | Add stream FbaReplacementsReports | +| `0.2.14` | 2022-01-19 | [\#9621](https://github.com/airbytehq/airbyte/pull/9621) | Add GET_FLAT_FILE_ALL_ORDERS_DATA_BY_LAST_UPDATE_GENERAL report | +| `0.2.13` | 2022-01-18 | [\#9581](https://github.com/airbytehq/airbyte/pull/9581) | Change createdSince parameter to dataStartTime | +| `0.2.12` | 2022-01-05 | [\#9312](https://github.com/airbytehq/airbyte/pull/9312) | Add all remaining brand analytics report streams | +| `0.2.11` | 2022-01-05 | [\#9115](https://github.com/airbytehq/airbyte/pull/9115) | Fix reading only 100 orders | +| `0.2.10` | 2021-12-31 | [\#9236](https://github.com/airbytehq/airbyte/pull/9236) | Fix NoAuth deprecation warning | +| `0.2.9` | 2021-12-30 | [\#9212](https://github.com/airbytehq/airbyte/pull/9212) | Normalize GET_SELLER_FEEDBACK_DATA header field names | +| `0.2.8` | 2021-12-22 | [\#8810](https://github.com/airbytehq/airbyte/pull/8810) | Fix GET_SELLER_FEEDBACK_DATA Date cursor field format | +| `0.2.7` | 2021-12-21 | [\#9002](https://github.com/airbytehq/airbyte/pull/9002) | Extract REPORTS_MAX_WAIT_SECONDS to configurable parameter | +| `0.2.6` | 2021-12-10 | [\#8179](https://github.com/airbytehq/airbyte/pull/8179) | Add GET_BRAND_ANALYTICS_SEARCH_TERMS_REPORT report | +| `0.2.5` | 2021-12-06 | [\#8425](https://github.com/airbytehq/airbyte/pull/8425) | Update title, description fields in spec | +| `0.2.4` | 2021-11-08 | [\#8021](https://github.com/airbytehq/airbyte/pull/8021) | Added GET_SELLER_FEEDBACK_DATA report with incremental sync capability | +| `0.2.3` | 2021-11-08 | [\#7828](https://github.com/airbytehq/airbyte/pull/7828) | Remove datetime format from all streams | +| `0.2.2` | 2021-11-08 | [\#7752](https://github.com/airbytehq/airbyte/pull/7752) | Change `check_connection` function to use stream Orders | +| `0.2.1` | 2021-09-17 | [\#5248](https://github.com/airbytehq/airbyte/pull/5248) | Added `extra stream` support. Updated `reports streams` logics | +| `0.2.0` | 2021-08-06 | [\#4863](https://github.com/airbytehq/airbyte/pull/4863) | Rebuild source with `airbyte-cdk` | +| `0.1.3` | 2021-06-23 | [\#4288](https://github.com/airbytehq/airbyte/pull/4288) | Bugfix failing `connection check` | +| `0.1.2` | 2021-06-15 | [\#4108](https://github.com/airbytehq/airbyte/pull/4108) | Fixed: Sync fails with timeout when create report is CANCELLED` |