From 7503dc78f178b77e9422d7bb818c1c086406cee3 Mon Sep 17 00:00:00 2001 From: Gireesh Sreepathi Date: Mon, 8 Jan 2024 09:09:17 -0800 Subject: [PATCH 001/574] destination-redshift: Heartbeats for bastion and other minor bugfixes (#33948) Signed-off-by: Gireesh Sreepathi Co-authored-by: Edward Gao --- .../typing_deduping/DefaultTyperDeduper.java | 24 +++++++++++--- .../BaseSqlGeneratorIntegrationTest.java | 20 +++++++++-- .../all_types_v1_inputrecords.jsonl | 1 + .../sqlgenerator/alltypes_inputrecords.jsonl | 1 + .../destination-bigquery/metadata.yaml | 2 +- .../alltypes_expectedrecords_final.jsonl | 1 + .../alltypes_expectedrecords_raw.jsonl | 1 + .../destination-redshift/build.gradle | 2 +- .../destination-redshift/metadata.yaml | 2 +- .../redshift/RedshiftInsertDestination.java | 16 +++++++-- .../RedshiftStagingS3Destination.java | 21 ++++++++++-- .../RedshiftDestinationHandler.java | 33 +++++++++++++++++++ .../alltypes_expectedrecords_final.jsonl | 2 ++ .../alltypes_expectedrecords_raw.jsonl | 1 + .../destination-snowflake/metadata.yaml | 2 +- .../SnowflakeSqlGeneratorIntegrationTest.java | 4 +-- .../alltypes_expectedrecords_final.jsonl | 1 + .../alltypes_expectedrecords_raw.jsonl | 1 + docs/integrations/destinations/bigquery.md | 1 + docs/integrations/destinations/redshift.md | 5 +-- docs/integrations/destinations/snowflake.md | 1 + 21 files changed, 123 insertions(+), 19 deletions(-) diff --git a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/main/java/io/airbyte/integrations/base/destination/typing_deduping/DefaultTyperDeduper.java b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/main/java/io/airbyte/integrations/base/destination/typing_deduping/DefaultTyperDeduper.java index fd60681451699..9fff9fd8e1166 100644 --- a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/main/java/io/airbyte/integrations/base/destination/typing_deduping/DefaultTyperDeduper.java +++ b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/main/java/io/airbyte/integrations/base/destination/typing_deduping/DefaultTyperDeduper.java @@ -205,16 +205,25 @@ public Lock getRawTableInsertLock(final String originalNamespace, final String o return tdLocks.get(streamConfig.id()).readLock(); } + private boolean streamSetupSucceeded(final StreamConfig streamConfig) { + final var originalNamespace = streamConfig.id().originalNamespace(); + final var originalName = streamConfig.id().originalName(); + if (!streamsWithSuccessfulSetup.contains(Pair.of(originalNamespace, originalName))) { + // For example, if T+D setup fails, but the consumer tries to run T+D on all streams during close, + // we should skip it. + LOGGER.warn("Skipping typing and deduping for {}.{} because we could not set up the tables for this stream.", originalNamespace, + originalName); + return false; + } + return true; + } + public CompletableFuture> typeAndDedupeTask(final StreamConfig streamConfig, final boolean mustRun) { return CompletableFuture.supplyAsync(() -> { final var originalNamespace = streamConfig.id().originalNamespace(); final var originalName = streamConfig.id().originalName(); try { - if (!streamsWithSuccessfulSetup.contains(Pair.of(originalNamespace, originalName))) { - // For example, if T+D setup fails, but the consumer tries to run T+D on all streams during close, - // we should skip it. - LOGGER.warn("Skipping typing and deduping for {}.{} because we could not set up the tables for this stream.", originalNamespace, - originalName); + if (!streamSetupSucceeded(streamConfig)) { return Optional.empty(); } @@ -264,6 +273,11 @@ public void typeAndDedupe(final Map streamS final Set>> typeAndDedupeTasks = new HashSet<>(); parsedCatalog.streams().stream() .filter(streamConfig -> { + // Skip if stream setup failed. + if (!streamSetupSucceeded(streamConfig)) { + return false; + } + // Skip if we don't have any records for this stream. final StreamSyncSummary streamSyncSummary = streamSyncSummaries.getOrDefault( streamConfig.id().asStreamDescriptor(), StreamSyncSummary.DEFAULT); diff --git a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/java/io/airbyte/integrations/base/destination/typing_deduping/BaseSqlGeneratorIntegrationTest.java b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/java/io/airbyte/integrations/base/destination/typing_deduping/BaseSqlGeneratorIntegrationTest.java index e7e44a56662a9..e094930853bfd 100644 --- a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/java/io/airbyte/integrations/base/destination/typing_deduping/BaseSqlGeneratorIntegrationTest.java +++ b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/java/io/airbyte/integrations/base/destination/typing_deduping/BaseSqlGeneratorIntegrationTest.java @@ -403,6 +403,10 @@ public void incrementalDedupSameNameNamespace() throws Exception { */ @Test public void allTypes() throws Exception { + // Add case-sensitive columnName to test json path querying + incrementalDedupStream.columns().put( + generator.buildColumnId("IamACaseSensitiveColumnName"), + AirbyteProtocolType.STRING); createRawTable(streamId); createFinalTable(incrementalDedupStream, ""); insertRawTableRecords( @@ -514,6 +518,10 @@ public void minTimestampBehavesCorrectly() throws Exception { */ @Test public void handlePreexistingRecords() throws Exception { + // Add case-sensitive columnName to test json path querying + incrementalDedupStream.columns().put( + generator.buildColumnId("IamACaseSensitiveColumnName"), + AirbyteProtocolType.STRING); createRawTable(streamId); createFinalTable(incrementalDedupStream, ""); insertRawTableRecords( @@ -541,6 +549,10 @@ public void handlePreexistingRecords() throws Exception { */ @Test public void handleNoPreexistingRecords() throws Exception { + // Add case-sensitive columnName to test json path querying + incrementalDedupStream.columns().put( + generator.buildColumnId("IamACaseSensitiveColumnName"), + AirbyteProtocolType.STRING); createRawTable(streamId); final DestinationHandler.InitialRawTableState tableState = destinationHandler.getInitialRawTableState(streamId); assertAll( @@ -1124,6 +1136,10 @@ public void noColumns() throws Exception { public void testV1V2migration() throws Exception { // This is maybe a little hacky, but it avoids having to refactor this entire class and subclasses // for something that is going away + // Add case-sensitive columnName to test json path querying + incrementalDedupStream.columns().put( + generator.buildColumnId("IamACaseSensitiveColumnName"), + AirbyteProtocolType.STRING); final StreamId v1RawTableStreamId = new StreamId(null, null, streamId.finalNamespace(), "v1_" + streamId.rawName(), null, null); createV1RawTable(v1RawTableStreamId); insertV1RawTableRecords(v1RawTableStreamId, BaseTypingDedupingTest.readRecords( @@ -1169,8 +1185,8 @@ protected void migrationAssertions(final List v1RawRecords, final List record -> record.get("_airbyte_raw_id").asText(), Function.identity())); assertAll( - () -> assertEquals(5, v1RawRecords.size()), - () -> assertEquals(5, v2RawRecords.size())); + () -> assertEquals(6, v1RawRecords.size()), + () -> assertEquals(6, v2RawRecords.size())); v1RawRecords.forEach(v1Record -> { final var v1id = v1Record.get("_airbyte_ab_id").asText(); assertAll( diff --git a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/resources/sqlgenerator/all_types_v1_inputrecords.jsonl b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/resources/sqlgenerator/all_types_v1_inputrecords.jsonl index 3938dd7b53d16..e2cde49ad980a 100644 --- a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/resources/sqlgenerator/all_types_v1_inputrecords.jsonl +++ b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/resources/sqlgenerator/all_types_v1_inputrecords.jsonl @@ -4,3 +4,4 @@ // Note that array and struct have invalid values ({} and [] respectively). {"_airbyte_ab_id": "84242b60-3a34-4531-ad75-a26702960a9a", "_airbyte_emitted_at": "2023-01-01T00:00:00Z", "_airbyte_data": {"id1": 4, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": {}, "struct": [], "string": null, "number": "foo", "integer": "bar", "boolean": "fizz", "timestamp_with_timezone": {}, "timestamp_without_timezone": {}, "time_with_timezone": {}, "time_without_timezone": {}, "date": "airbyte", "unknown": null}} {"_airbyte_ab_id": "a4a783b5-7729-4d0b-b659-48ceb08713f1", "_airbyte_emitted_at": "2023-01-01T00:00:00Z", "_airbyte_data": {"id1": 5, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "number": 67.174118, "struct": {"nested_number": 67.174118}, "array": [67.174118], "unknown": 67.174118}} +{"_airbyte_ab_id": "7e1fac0c-017e-4ad6-bc78-334a34d64fce", "_airbyte_emitted_at": "2023-01-01T00:00:00Z", "_airbyte_data": {"id1": 6, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "IamACaseSensitiveColumnName": "Case senstive value"}} \ No newline at end of file diff --git a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/resources/sqlgenerator/alltypes_inputrecords.jsonl b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/resources/sqlgenerator/alltypes_inputrecords.jsonl index 0491c86d495c3..c21fc0bbb6abe 100644 --- a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/resources/sqlgenerator/alltypes_inputrecords.jsonl +++ b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/resources/sqlgenerator/alltypes_inputrecords.jsonl @@ -4,3 +4,4 @@ // Note that array and struct have invalid values ({} and [] respectively). {"_airbyte_raw_id": "84242b60-3a34-4531-ad75-a26702960a9a", "_airbyte_extracted_at": "2023-01-01T00:00:00Z", "_airbyte_data": {"id1": 4, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": {}, "struct": [], "string": null, "number": "foo", "integer": "bar", "boolean": "fizz", "timestamp_with_timezone": {}, "timestamp_without_timezone": {}, "time_with_timezone": {}, "time_without_timezone": {}, "date": "airbyte", "unknown": null}} {"_airbyte_raw_id": "a4a783b5-7729-4d0b-b659-48ceb08713f1", "_airbyte_extracted_at": "2023-01-01T00:00:00Z", "_airbyte_data": {"id1": 5, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "number": 67.174118, "struct": {"nested_number": 67.174118}, "array": [67.174118], "unknown": 67.174118}} +{"_airbyte_raw_id": "7e1fac0c-017e-4ad6-bc78-334a34d64fce", "_airbyte_extracted_at": "2023-01-01T00:00:00Z", "_airbyte_data": {"id1": 6, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "IamACaseSensitiveColumnName": "Case senstive value"}} \ No newline at end of file diff --git a/airbyte-integrations/connectors/destination-bigquery/metadata.yaml b/airbyte-integrations/connectors/destination-bigquery/metadata.yaml index b1ecef34c1254..165f71b8ff3e2 100644 --- a/airbyte-integrations/connectors/destination-bigquery/metadata.yaml +++ b/airbyte-integrations/connectors/destination-bigquery/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: database connectorType: destination definitionId: 22f6c74f-5699-40ff-833c-4a879ea40133 - dockerImageTag: 2.3.26 + dockerImageTag: 2.3.27 dockerRepository: airbyte/destination-bigquery documentationUrl: https://docs.airbyte.com/integrations/destinations/bigquery githubIssueLabel: destination-bigquery diff --git a/airbyte-integrations/connectors/destination-bigquery/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl b/airbyte-integrations/connectors/destination-bigquery/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl index 627521e4d9581..e83d33307523e 100644 --- a/airbyte-integrations/connectors/destination-bigquery/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl +++ b/airbyte-integrations/connectors/destination-bigquery/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl @@ -5,3 +5,4 @@ // Note that for numbers where we parse the value to JSON (struct, array, unknown) we lose precision. // But for numbers where we create a NUMBER column, we do not lose precision (see the `number` column). {"id1": 5, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "number": 67.174118, "struct": {"nested_number": 67.17411800000001}, "array": [67.17411800000001], "unknown": 67.17411800000001, "_airbyte_extracted_at": "2023-01-01T00:00:00Z", "_airbyte_meta": {"errors": []}} +{"id1": 6, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "IamACaseSensitiveColumnName": "Case senstive value", "_airbyte_extracted_at": "2023-01-01T00:00:00Z", "_airbyte_meta": {"errors": []}} \ No newline at end of file diff --git a/airbyte-integrations/connectors/destination-bigquery/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl b/airbyte-integrations/connectors/destination-bigquery/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl index 9f89442b914f9..aad52eb2e5253 100644 --- a/airbyte-integrations/connectors/destination-bigquery/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl +++ b/airbyte-integrations/connectors/destination-bigquery/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl @@ -3,3 +3,4 @@ {"_airbyte_raw_id": "7e1fac0c-017e-4ad6-bc78-334a34d64fbe", "_airbyte_extracted_at": "2023-01-01T00:00:00Z", "_airbyte_data": {"id1": 3, "id2": 100, "updated_at": "2023-01-01T01:00:00Z"}} {"_airbyte_raw_id": "84242b60-3a34-4531-ad75-a26702960a9a", "_airbyte_extracted_at": "2023-01-01T00:00:00Z", "_airbyte_data": {"id1": 4, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": {}, "struct": [], "string": null, "number": "foo", "integer": "bar", "boolean": "fizz", "timestamp_with_timezone": {}, "timestamp_without_timezone": {}, "time_with_timezone": {}, "time_without_timezone": {}, "date": "airbyte", "unknown": null}} {"_airbyte_raw_id": "a4a783b5-7729-4d0b-b659-48ceb08713f1", "_airbyte_extracted_at": "2023-01-01T00:00:00Z", "_airbyte_data": {"id1": 5, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "number": 67.174118, "struct": {"nested_number": 67.174118}, "array": [67.174118], "unknown": 67.174118}} +{"_airbyte_raw_id": "7e1fac0c-017e-4ad6-bc78-334a34d64fce", "_airbyte_extracted_at": "2023-01-01T00:00:00Z", "_airbyte_data": {"id1": 6, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "IamACaseSensitiveColumnName": "Case senstive value"}} \ No newline at end of file diff --git a/airbyte-integrations/connectors/destination-redshift/build.gradle b/airbyte-integrations/connectors/destination-redshift/build.gradle index fef3e8e7a053b..aff9a6f52400f 100644 --- a/airbyte-integrations/connectors/destination-redshift/build.gradle +++ b/airbyte-integrations/connectors/destination-redshift/build.gradle @@ -4,7 +4,7 @@ plugins { } airbyteJavaConnector { - cdkVersionRequired = '0.11.0' + cdkVersionRequired = '0.11.1' features = ['db-destinations', 's3-destinations'] useLocalCdk = false } diff --git a/airbyte-integrations/connectors/destination-redshift/metadata.yaml b/airbyte-integrations/connectors/destination-redshift/metadata.yaml index d339d168748d9..a363329ca957e 100644 --- a/airbyte-integrations/connectors/destination-redshift/metadata.yaml +++ b/airbyte-integrations/connectors/destination-redshift/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: database connectorType: destination definitionId: f7a7d195-377f-cf5b-70a5-be6b819019dc - dockerImageTag: 0.7.12 + dockerImageTag: 0.7.13 dockerRepository: airbyte/destination-redshift documentationUrl: https://docs.airbyte.com/integrations/destinations/redshift githubIssueLabel: destination-redshift diff --git a/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftInsertDestination.java b/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftInsertDestination.java index 5f02e71860542..66e5e544093fd 100644 --- a/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftInsertDestination.java +++ b/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftInsertDestination.java @@ -23,6 +23,7 @@ import io.airbyte.integrations.destination.redshift.typing_deduping.RedshiftDestinationHandler; import io.airbyte.integrations.destination.redshift.typing_deduping.RedshiftSqlGenerator; import java.time.Duration; +import java.util.HashMap; import java.util.Map; import java.util.Optional; import javax.sql.DataSource; @@ -55,7 +56,7 @@ public DataSource getDataSource(final JsonNode config) { jdbcConfig.has(JdbcUtils.PASSWORD_KEY) ? jdbcConfig.get(JdbcUtils.PASSWORD_KEY).asText() : null, RedshiftInsertDestination.DRIVER_CLASS, jdbcConfig.get(JdbcUtils.JDBC_URL_KEY).asText(), - SSL_JDBC_PARAMETERS, + getDefaultConnectionProperties(config), Duration.ofMinutes(2)); } @@ -70,7 +71,18 @@ public JdbcDatabase getDatabase(final DataSource dataSource, final JdbcSourceOpe @Override protected Map getDefaultConnectionProperties(final JsonNode config) { - return SSL_JDBC_PARAMETERS; + // The following properties can be overriden through jdbcUrlParameters in the config. + final Map connectionOptions = new HashMap<>(); + // Redshift properties + // https://docs.aws.amazon.com/redshift/latest/mgmt/jdbc20-configuration-options.html#jdbc20-connecttimeout-option + // connectTimeout is different from Hikari pool's connectionTimout, driver defaults to 10seconds so + // increase it to match hikari's default + connectionOptions.put("connectTimeout", "120"); + // HikariPool properties + // https://github.com/brettwooldridge/HikariCP?tab=readme-ov-file#frequently-used + // TODO: Change data source factory to configure these properties + connectionOptions.putAll(SSL_JDBC_PARAMETERS); + return connectionOptions; } public static JsonNode getJdbcConfig(final JsonNode redshiftConfig) { diff --git a/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftStagingS3Destination.java b/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftStagingS3Destination.java index 3086f716ebaac..d82a22fe2eaa9 100644 --- a/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftStagingS3Destination.java +++ b/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftStagingS3Destination.java @@ -57,6 +57,7 @@ import io.airbyte.protocol.models.v0.ConfiguredAirbyteCatalog; import io.airbyte.protocol.models.v0.ConfiguredAirbyteStream; import java.time.Duration; +import java.util.HashMap; import java.util.Map; import java.util.function.Consumer; import javax.sql.DataSource; @@ -132,7 +133,7 @@ public DataSource getDataSource(final JsonNode config) { jdbcConfig.has(JdbcUtils.PASSWORD_KEY) ? jdbcConfig.get(JdbcUtils.PASSWORD_KEY).asText() : null, RedshiftInsertDestination.DRIVER_CLASS, jdbcConfig.get(JdbcUtils.JDBC_URL_KEY).asText(), - SSL_JDBC_PARAMETERS, + getDefaultConnectionProperties(config), Duration.ofMinutes(2)); } @@ -143,7 +144,23 @@ protected NamingConventionTransformer getNamingResolver() { @Override protected Map getDefaultConnectionProperties(final JsonNode config) { - return SSL_JDBC_PARAMETERS; + // TODO: Pull common code from RedshiftInsertDestination and RedshiftStagingS3Destination into a + // base class. + // The following properties can be overriden through jdbcUrlParameters in the config. + Map connectionOptions = new HashMap<>(); + // Redshift properties + // https://docs.aws.amazon.com/redshift/latest/mgmt/jdbc20-configuration-options.html#jdbc20-connecttimeout-option + // connectTimeout is different from Hikari pool's connectionTimout, driver defaults to 10seconds so + // increase it to match hikari's default + connectionOptions.put("connectTimeout", "120"); + // HikariPool properties + // https://github.com/brettwooldridge/HikariCP?tab=readme-ov-file#frequently-used + // connectionTimeout is set explicitly to 2 minutes when creating data source. + // Do aggressive keepAlive with minimum allowed value, this only applies to connection sitting idle + // in the pool. + connectionOptions.put("keepaliveTime", Long.toString(Duration.ofSeconds(30).toMillis())); + connectionOptions.putAll(SSL_JDBC_PARAMETERS); + return connectionOptions; } // this is a no op since we override getDatabase. diff --git a/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/typing_deduping/RedshiftDestinationHandler.java b/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/typing_deduping/RedshiftDestinationHandler.java index 6535ab5121a83..3fe5e6ecf32d6 100644 --- a/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/typing_deduping/RedshiftDestinationHandler.java +++ b/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/typing_deduping/RedshiftDestinationHandler.java @@ -7,15 +7,48 @@ import com.fasterxml.jackson.databind.JsonNode; import io.airbyte.cdk.db.jdbc.JdbcDatabase; import io.airbyte.cdk.integrations.destination.jdbc.typing_deduping.JdbcDestinationHandler; +import io.airbyte.integrations.base.destination.typing_deduping.Sql; import io.airbyte.integrations.base.destination.typing_deduping.StreamId; +import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +@Slf4j public class RedshiftDestinationHandler extends JdbcDestinationHandler { public RedshiftDestinationHandler(final String databaseName, final JdbcDatabase jdbcDatabase) { super(databaseName, jdbcDatabase); } + @Override + public void execute(final Sql sql) throws Exception { + final List> transactions = sql.transactions(); + final UUID queryId = UUID.randomUUID(); + for (final List transaction : transactions) { + final UUID transactionId = UUID.randomUUID(); + log.info("Executing sql {}-{}: {}", queryId, transactionId, String.join("\n", transaction)); + final long startTime = System.currentTimeMillis(); + + try { + // Original list is immutable, so copying it into a different list. + final List modifiedStatements = new ArrayList<>(); + // This is required for Redshift to retrieve Json path query with upper case characters, even after + // specifying quotes. + // see https://github.com/airbytehq/airbyte/issues/33900 + modifiedStatements.add("SET enable_case_sensitive_identifier to TRUE;\n"); + modifiedStatements.addAll(transaction); + jdbcDatabase.executeWithinTransaction(modifiedStatements); + } catch (final SQLException e) { + log.error("Sql {}-{} failed", queryId, transactionId, e); + throw e; + } + + log.info("Sql {}-{} completed in {} ms", queryId, transactionId, System.currentTimeMillis() - startTime); + } + } + @Override public boolean isFinalTableEmpty(final StreamId id) throws Exception { // Redshift doesn't have an information_schema.tables table, so we have to use SVV_TABLE_INFO. diff --git a/airbyte-integrations/connectors/destination-redshift/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl b/airbyte-integrations/connectors/destination-redshift/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl index f1b6cd3a5e20d..f6441416658b4 100644 --- a/airbyte-integrations/connectors/destination-redshift/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl +++ b/airbyte-integrations/connectors/destination-redshift/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl @@ -5,3 +5,5 @@ // Note that for numbers where we parse the value to JSON (struct, array, unknown) we lose precision. // But for numbers where we create a NUMBER column, we do not lose precision (see the `number` column). {"id1": 5, "id2": 100, "updated_at": "2023-01-01T01:00:00.000000Z", "number": 67.174118, "struct": {"nested_number": 67.174118}, "array": [67.174118], "unknown": 67.174118, "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}} +// Note that redshift downcases IAmACaseSensitiveColumnName to all lowercase +{"id1": 6, "id2": 100, "updated_at": "2023-01-01T01:00:00.000000Z", "iamacasesensitivecolumnname": "Case senstive value", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}} \ No newline at end of file diff --git a/airbyte-integrations/connectors/destination-redshift/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl b/airbyte-integrations/connectors/destination-redshift/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl index a341d911fbbc9..6b99169ececf1 100644 --- a/airbyte-integrations/connectors/destination-redshift/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl +++ b/airbyte-integrations/connectors/destination-redshift/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl @@ -3,3 +3,4 @@ {"_airbyte_raw_id": "7e1fac0c-017e-4ad6-bc78-334a34d64fbe", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 3, "id2": 100, "updated_at": "2023-01-01T01:00:00Z"}} {"_airbyte_raw_id": "84242b60-3a34-4531-ad75-a26702960a9a", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 4, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": {}, "struct": [], "string": null, "number": "foo", "integer": "bar", "boolean": "fizz", "timestamp_with_timezone": {}, "timestamp_without_timezone": {}, "time_with_timezone": {}, "time_without_timezone": {}, "date": "airbyte", "unknown": null}} {"_airbyte_raw_id": "a4a783b5-7729-4d0b-b659-48ceb08713f1", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 5, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "number": 67.174118, "struct": {"nested_number": 67.174118}, "array": [67.174118], "unknown": 67.174118}} +{"_airbyte_raw_id": "7e1fac0c-017e-4ad6-bc78-334a34d64fce", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 6, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "IamACaseSensitiveColumnName": "Case senstive value"}} diff --git a/airbyte-integrations/connectors/destination-snowflake/metadata.yaml b/airbyte-integrations/connectors/destination-snowflake/metadata.yaml index 126f33b376be1..db96739abf1a9 100644 --- a/airbyte-integrations/connectors/destination-snowflake/metadata.yaml +++ b/airbyte-integrations/connectors/destination-snowflake/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: database connectorType: destination definitionId: 424892c4-daac-4491-b35d-c6688ba547ba - dockerImageTag: 3.4.19 + dockerImageTag: 3.4.20 dockerRepository: airbyte/destination-snowflake documentationUrl: https://docs.airbyte.com/integrations/destinations/snowflake githubIssueLabel: destination-snowflake diff --git a/airbyte-integrations/connectors/destination-snowflake/src/test-integration/java/io/airbyte/integrations/destination/snowflake/typing_deduping/SnowflakeSqlGeneratorIntegrationTest.java b/airbyte-integrations/connectors/destination-snowflake/src/test-integration/java/io/airbyte/integrations/destination/snowflake/typing_deduping/SnowflakeSqlGeneratorIntegrationTest.java index f866501516d75..13338a83a03ea 100644 --- a/airbyte-integrations/connectors/destination-snowflake/src/test-integration/java/io/airbyte/integrations/destination/snowflake/typing_deduping/SnowflakeSqlGeneratorIntegrationTest.java +++ b/airbyte-integrations/connectors/destination-snowflake/src/test-integration/java/io/airbyte/integrations/destination/snowflake/typing_deduping/SnowflakeSqlGeneratorIntegrationTest.java @@ -367,8 +367,8 @@ protected void migrationAssertions(final List v1RawRecords, final List record -> record.get(JavaBaseConstants.COLUMN_NAME_AB_RAW_ID).asText(), Function.identity())); assertAll( - () -> assertEquals(5, v1RawRecords.size()), - () -> assertEquals(5, v2RawRecords.size())); + () -> assertEquals(6, v1RawRecords.size()), + () -> assertEquals(6, v2RawRecords.size())); v1RawRecords.forEach(v1Record -> { final var v1id = v1Record.get(JavaBaseConstants.COLUMN_NAME_AB_ID.toUpperCase()).asText(); assertAll( diff --git a/airbyte-integrations/connectors/destination-snowflake/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl b/airbyte-integrations/connectors/destination-snowflake/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl index b38d23d4e8239..f7bffd2581230 100644 --- a/airbyte-integrations/connectors/destination-snowflake/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl +++ b/airbyte-integrations/connectors/destination-snowflake/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl @@ -4,3 +4,4 @@ {"ID1": 4, "ID2": 100, "UPDATED_AT": "2023-01-01T01:00:00.000000000Z", "UNKNOWN": null, "_AIRBYTE_EXTRACTED_AT": "2023-01-01T00:00:00.000000000Z", "_AIRBYTE_META": {"errors": ["Problem with `struct`", "Problem with `array`", "Problem with `number`", "Problem with `integer`", "Problem with `boolean`", "Problem with `timestamp_with_timezone`", "Problem with `timestamp_without_timezone`", "Problem with `time_with_timezone`", "Problem with `time_without_timezone`", "Problem with `date`"]}} // Note: no loss of precision on these numbers. A naive float64 conversion would yield 67.17411800000001. {"ID1": 5, "ID2": 100, "UPDATED_AT": "2023-01-01T01:00:00.000000000Z", "NUMBER": 67.174118, "STRUCT": {"nested_number": 67.174118}, "ARRAY": [67.174118], "UNKNOWN": 67.174118, "_AIRBYTE_EXTRACTED_AT": "2023-01-01T00:00:00.000000000Z", "_AIRBYTE_META": {"errors": []}} +{"ID1": 6, "ID2": 100, "UPDATED_AT": "2023-01-01T01:00:00.000000000Z", "IAMACASESENSITIVECOLUMNNAME": "Case senstive value", "_AIRBYTE_EXTRACTED_AT":"2023-01-01T00:00:00.000000000Z", "_AIRBYTE_META":{"errors":[]}} diff --git a/airbyte-integrations/connectors/destination-snowflake/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl b/airbyte-integrations/connectors/destination-snowflake/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl index 75553fdd99974..e5909080bd837 100644 --- a/airbyte-integrations/connectors/destination-snowflake/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl +++ b/airbyte-integrations/connectors/destination-snowflake/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl @@ -3,3 +3,4 @@ {"_airbyte_raw_id": "7e1fac0c-017e-4ad6-bc78-334a34d64fbe", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000000Z", "_airbyte_data": {"id1": 3, "id2": 100, "updated_at": "2023-01-01T01:00:00Z"}} {"_airbyte_raw_id": "84242b60-3a34-4531-ad75-a26702960a9a", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000000Z", "_airbyte_data": {"id1": 4, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": {}, "struct": [], "string": null, "number": "foo", "integer": "bar", "boolean": "fizz", "timestamp_with_timezone": {}, "timestamp_without_timezone": {}, "time_with_timezone": {}, "time_without_timezone": {}, "date": "airbyte", "unknown": null}} {"_airbyte_raw_id": "a4a783b5-7729-4d0b-b659-48ceb08713f1", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000000Z", "_airbyte_data": {"id1": 5, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "number": 67.174118, "struct": {"nested_number": 67.174118}, "array": [67.174118], "unknown": 67.174118}} +{"_airbyte_raw_id": "7e1fac0c-017e-4ad6-bc78-334a34d64fce", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000000Z", "_airbyte_data": {"id1": 6, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "IamACaseSensitiveColumnName": "Case senstive value"}} \ No newline at end of file diff --git a/docs/integrations/destinations/bigquery.md b/docs/integrations/destinations/bigquery.md index 7b726fa13df3d..c73b50c6cc583 100644 --- a/docs/integrations/destinations/bigquery.md +++ b/docs/integrations/destinations/bigquery.md @@ -142,6 +142,7 @@ Now that you have set up the BigQuery destination connector, check out the follo | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 2.3.27 | 2024-01-05 | [\#33948](https://github.com/airbytehq/airbyte/pull/33948) | Skip retrieving initial table state when setup fails | | 2.3.26 | 2024-01-04 | [\#33730](https://github.com/airbytehq/airbyte/pull/33730) | Internal code structure changes | | 2.3.25 | 2023-12-20 | [\#33704](https://github.com/airbytehq/airbyte/pull/33704) | Update to java CDK 0.10.0 (no changes) | | 2.3.24 | 2023-12-20 | [\#33697](https://github.com/airbytehq/airbyte/pull/33697) | Stop creating unnecessary tmp tables | diff --git a/docs/integrations/destinations/redshift.md b/docs/integrations/destinations/redshift.md index c42bb1a01c2af..e4e484650c957 100644 --- a/docs/integrations/destinations/redshift.md +++ b/docs/integrations/destinations/redshift.md @@ -214,7 +214,8 @@ Each stream will be output into its own raw table in Redshift. Each table will c ## Changelog | Version | Date | Pull Request | Subject | -| :------ | :--------- | :--------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|:--------|:-----------|:-----------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0.7.13 | 2024-01-05 | [\#33948](https://github.com/airbytehq/airbyte/pull/33948) | Fix NPE when prepare tables fail; Add case sensitive session for super; Bastion heartbeats added | | 0.7.12 | 2024-01-03 | [#33924](https://github.com/airbytehq/airbyte/pull/33924) | Add new ap-southeast-3 AWS region | | 0.7.11 | 2024-01-04 | [\#33730](https://github.com/airbytehq/airbyte/pull/33730) | Internal code structure changes | | 0.7.10 | 2024-01-04 | [\#33728](https://github.com/airbytehq/airbyte/pull/33728) | Allow users to disable final table creation | @@ -255,7 +256,7 @@ Each stream will be output into its own raw table in Redshift. Each table will c | 0.3.55 | 2023-01-26 | [\#20631](https://github.com/airbytehq/airbyte/pull/20631) | Added support for destination checkpointing with staging | | 0.3.54 | 2023-01-18 | [\#21087](https://github.com/airbytehq/airbyte/pull/21087) | Wrap Authentication Errors as Config Exceptions | | 0.3.53 | 2023-01-03 | [\#17273](https://github.com/airbytehq/airbyte/pull/17273) | Flatten JSON arrays to fix maximum size check for SUPER field | -| 0.3.52 | 2022-12-30 | [\#20879](https://github.com/airbytehq/airbyte/pull/20879) | Added configurable parameter for number of file buffers (⛔ this version has a bug and will not work; use `0.3.56` instead) | +| 0.3.52 | 2022-12-30 | [\#20879](https://github.com/airbytehq/airbyte/pull/20879) | Added configurable parameter for number of file buffers (⛔ this version has a bug and will not work; use `0.3.56` instead) | | 0.3.51 | 2022-10-26 | [\#18434](https://github.com/airbytehq/airbyte/pull/18434) | Fix empty S3 bucket path handling | | 0.3.50 | 2022-09-14 | [\#15668](https://github.com/airbytehq/airbyte/pull/15668) | Wrap logs in AirbyteLogMessage | | 0.3.49 | 2022-09-01 | [\#16243](https://github.com/airbytehq/airbyte/pull/16243) | Fix Json to Avro conversion when there is field name clash from combined restrictions (`anyOf`, `oneOf`, `allOf` fields) | diff --git a/docs/integrations/destinations/snowflake.md b/docs/integrations/destinations/snowflake.md index 3032a755e098f..504f6f218a300 100644 --- a/docs/integrations/destinations/snowflake.md +++ b/docs/integrations/destinations/snowflake.md @@ -246,6 +246,7 @@ Otherwise, make sure to grant the role the required permissions in the desired n | Version | Date | Pull Request | Subject | |:----------------|:-----------|:-----------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 3.4.20 | 2024-01-05 | [\#33948](https://github.com/airbytehq/airbyte/pull/33948) | Skip retrieving initial table state when setup fails | | 3.4.19 | 2024-01-04 | [\#33730](https://github.com/airbytehq/airbyte/pull/33730) | Internal code structure changes | | 3.4.18 | 2024-01-02 | [\#33728](https://github.com/airbytehq/airbyte/pull/33728) | Add option to only type and dedupe at the end of the sync | | 3.4.17 | 2023-12-20 | [\#33704](https://github.com/airbytehq/airbyte/pull/33704) | Update to java CDK 0.10.0 (no changes) | From c6e10a733a69cfdb0c89f83091c20f9f29c95b1c Mon Sep 17 00:00:00 2001 From: Anton Karpets Date: Mon, 8 Jan 2024 20:35:47 +0200 Subject: [PATCH 002/574] =?UTF-8?q?=F0=9F=90=9BSource=20Stripe:=20update?= =?UTF-8?q?=20endpoint=20for=20bank=5Faccounts=20stream=20(#33926)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../source-stripe/acceptance-test-config.yml | 22 ++++++++++++++----- .../integration_tests/expected_records.jsonl | 6 ++--- .../connectors/source-stripe/metadata.yaml | 2 +- .../source_stripe/schemas/bank_accounts.json | 3 +++ .../source-stripe/source_stripe/source.py | 3 +-- .../source-stripe/source_stripe/streams.py | 2 +- .../source-stripe/unit_tests/test_streams.py | 11 +++------- docs/integrations/sources/stripe.md | 1 + 8 files changed, 29 insertions(+), 21 deletions(-) diff --git a/airbyte-integrations/connectors/source-stripe/acceptance-test-config.yml b/airbyte-integrations/connectors/source-stripe/acceptance-test-config.yml index 84469b4d8ce0a..c3002b6d31f3c 100644 --- a/airbyte-integrations/connectors/source-stripe/acceptance-test-config.yml +++ b/airbyte-integrations/connectors/source-stripe/acceptance-test-config.yml @@ -23,13 +23,13 @@ acceptance_tests: - name: "application_fees" bypass_reason: "This stream can't be seeded in our sandbox account" - name: "application_fees_refunds" - bypass_reason: "this stream can't be seeded in our sandbox account" + bypass_reason: "This stream can't be seeded in our sandbox account" - name: "authorizations" bypass_reason: "This stream can't be seeded in our sandbox account" - name: "bank_accounts" - bypass_reason: "this stream can't be seeded in our sandbox account" + bypass_reason: "This stream can't be seeded in our sandbox account" - name: "cards" - bypass_reason: "this stream can't be seeded in our sandbox account" + bypass_reason: "This stream can't be seeded in our sandbox account" - name: "early_fraud_warnings" bypass_reason: "This stream can't be seeded in our sandbox account" - name: "external_account_bank_accounts" @@ -37,11 +37,11 @@ acceptance_tests: - name: "external_account_cards" bypass_reason: "This stream can't be seeded in our sandbox account" - name: "payment_methods" - bypass_reason: "this stream can't be seeded in our sandbox account" + bypass_reason: "This stream can't be seeded in our sandbox account" - name: "persons" - bypass_reason: "this stream can't be seeded in our sandbox account" + bypass_reason: "This stream can't be seeded in our sandbox account" - name: "reviews" - bypass_reason: "this stream can't be seeded in our sandbox account" + bypass_reason: "This stream can't be seeded in our sandbox account" - name: "transactions" bypass_reason: "This stream can't be seeded in our sandbox account" - name: "events" @@ -84,6 +84,16 @@ acceptance_tests: invoice_line_items: - name: margins bypass_reason: "API randomly returns this field" + subscriptions: + - name: current_period_start + bypass_reason: "Frequently changing data" + - name: current_period_end + bypass_reason: "Frequently changing data" + - name: latest_invoice + bypass_reason: "Frequently changing data" + customers: + - name: next_invoice_sequence + bypass_reason: "Frequently changing data" incremental: tests: - config_path: "secrets/config.json" diff --git a/airbyte-integrations/connectors/source-stripe/integration_tests/expected_records.jsonl b/airbyte-integrations/connectors/source-stripe/integration_tests/expected_records.jsonl index affd99f2e322c..e5df135b15e69 100644 --- a/airbyte-integrations/connectors/source-stripe/integration_tests/expected_records.jsonl +++ b/airbyte-integrations/connectors/source-stripe/integration_tests/expected_records.jsonl @@ -21,7 +21,7 @@ {"stream": "credit_notes", "data": {"id": "cn_1NGPwmEcXtiJtvvhNXwHpgJF", "object": "credit_note", "amount": 8400, "amount_shipping": 0, "created": 1686158100, "currency": "usd", "customer": "cus_Kou8knsO3qQOwU", "customer_balance_transaction": null, "discount_amount": "0", "discount_amounts": [], "effective_at": 1686158100, "invoice": "in_1K9GK0EcXtiJtvvhSo2LvGqT", "lines": {"object": "list", "data": [{"id": "cnli_1NGPwmEcXtiJtvvhcL7yEIBJ", "object": "credit_note_line_item", "amount": 8400, "amount_excluding_tax": 8400, "description": "a box of parsnips", "discount_amount": 0, "discount_amounts": [], "invoice_line_item": "il_1K9GKLEcXtiJtvvhhHaYMebN", "livemode": false, "quantity": 1, "tax_amounts": [], "tax_rates": [], "type": "invoice_line_item", "unit_amount": 8400, "unit_amount_decimal": 8400.0, "unit_amount_excluding_tax": 8400.0}], "has_more": false, "url": "/v1/credit_notes/cn_1NGPwmEcXtiJtvvhNXwHpgJF/lines"}, "livemode": false, "memo": null, "metadata": {}, "number": "CA35DF83-0001-CN-01", "out_of_band_amount": null, "pdf": "https://pay.stripe.com/credit_notes/acct_1JwnoiEcXtiJtvvh/test_YWNjdF8xSndub2lFY1h0aUp0dnZoLF9PMlV3dFlJelh4NHM1R0VIWnhMR3RjWUtlejFlRWtILDg4MTY4MDc20200Sa50llWu/pdf?s=ap", "reason": null, "refund": null, "shipping_cost": null, "status": "issued", "subtotal": 8400, "subtotal_excluding_tax": 8400, "tax_amounts": [], "total": 8400, "total_excluding_tax": 8400, "type": "pre_payment", "voided_at": null, "updated": 1686158100}, "emitted_at": 1697627276386} {"stream": "customers", "data": {"id": "cus_LIiHR6omh14Xdg", "object": "customer", "address": {"city": "san francisco", "country": "US", "line1": "san francisco", "line2": "", "postal_code": "", "state": "CA"}, "balance": 0, "created": 1646998902, "currency": "usd", "default_source": "card_1MSHU1EcXtiJtvvhytSN6V54", "delinquent": false, "description": "test", "discount": null, "email": "test@airbyte_integration_test.com", "invoice_prefix": "09A6A98F", "invoice_settings": {"custom_fields": null, "default_payment_method": null, "footer": null, "rendering_options": null}, "livemode": false, "metadata": {}, "name": "Test", "next_invoice_sequence": 1, "phone": null, "preferred_locales": [], "shipping": {"address": {"city": "", "country": "US", "line1": "", "line2": "", "postal_code": "", "state": ""}, "name": "", "phone": ""}, "tax_exempt": "none", "test_clock": null, "updated": 1646998902}, "emitted_at": 1697627278433} {"stream": "customers", "data": {"id": "cus_Kou8knsO3qQOwU", "object": "customer", "address": null, "balance": 0, "created": 1640123795, "currency": "usd", "default_source": "src_1MSID8EcXtiJtvvhxIT9lXRy", "delinquent": false, "description": null, "discount": null, "email": "edward.gao+stripe-test-customer-1@airbyte.io", "invoice_prefix": "CA35DF83", "invoice_settings": {"custom_fields": null, "default_payment_method": null, "footer": null, "rendering_options": null}, "livemode": false, "metadata": {}, "name": "edgao-test-customer-1", "next_invoice_sequence": 2, "phone": null, "preferred_locales": [], "shipping": null, "tax_exempt": "none", "test_clock": null, "updated": 1640123795}, "emitted_at": 1697627278435} -{"stream": "customers", "data": { "id": "cus_NGoTFiJFVbSsvZ", "object": "customer", "address": { "city": "", "country": "US", "line1": "Street 2, 34567", "line2": "", "postal_code": "94114", "state": "CA" }, "balance": 0, "created": 1675160053, "currency": "usd", "default_source": "src_1MWGs8EcXtiJtvvh4nYdQvEr", "delinquent": false, "description": "Test Customer 2 description", "discount": null, "email": "user1.sample@zohomail.eu", "invoice_prefix": "C09C1837", "invoice_settings": { "custom_fields": null, "default_payment_method": null, "footer": null, "rendering_options": null }, "livemode": false, "metadata": {}, "name": "Test Customer 2", "next_invoice_sequence": 14, "phone": null, "preferred_locales": [ "en-US" ], "shipping": { "address": { "city": "", "country": "US", "line1": "Street 2, 34567", "line2": "", "postal_code": "94114", "state": "CA" }, "name": "Test Customer 2", "phone": "" }, "tax_exempt": "none", "test_clock": null, "updated": 1675160053 }, "emitted_at": 1697627278439} +{"stream": "customers", "data": {"id": "cus_NGoTFiJFVbSsvZ", "object": "customer", "address": {"city": "", "country": "US", "line1": "Street 2, 34567", "line2": "", "postal_code": "94114", "state": "CA"}, "balance": 0, "created": 1675160053, "currency": "usd", "default_source": "src_1MWGs8EcXtiJtvvh4nYdQvEr", "delinquent": false, "description": "Test Customer 2 description", "discount": null, "email": "user1.sample@zohomail.eu", "invoice_prefix": "C09C1837", "invoice_settings": {"custom_fields": null, "default_payment_method": null, "footer": null, "rendering_options": null}, "livemode": false, "metadata": {}, "name": "Test Customer 2", "next_invoice_sequence": 15, "phone": null, "preferred_locales": ["en-US"], "shipping": {"address": {"city": "", "country": "US", "line1": "Street 2, 34567", "line2": "", "postal_code": "94114", "state": "CA"}, "name": "Test Customer 2", "phone": ""}, "tax_exempt": "none", "test_clock": null, "updated": 1675160053}, "emitted_at": 1697627278439} {"stream": "cardholders", "data": {"id": "ich_1KUKBeEcXtiJtvvhCEFgko6h", "object": "issuing.cardholder", "billing": {"address": {"city": "San Francisco", "country": "US", "line1": "1234 Main Street", "line2": null, "postal_code": "94111", "state": "CA"}}, "company": null, "created": 1645143542, "email": "jenny.rosen@example.com", "individual": null, "livemode": false, "metadata": {}, "name": "Jenny Rosen", "phone_number": "+18888675309", "preferred_locales": [], "requirements": {"disabled_reason": null, "past_due": []}, "spending_controls": {"allowed_categories": [], "blocked_categories": [], "spending_limits": [], "spending_limits_currency": null}, "status": "active", "type": "individual", "updated": 1645143542}, "emitted_at": 1697627292209} {"stream": "charges", "data": {"id": "ch_3K9FSOEcXtiJtvvh0zxb7clc", "object": "charge", "amount": 5300, "amount_captured": 5300, "amount_refunded": 0, "amount_updates": [], "application": null, "application_fee": null, "application_fee_amount": null, "balance_transaction": "txn_3K9FSOEcXtiJtvvh0KoS5mx7", "billing_details": {"address": {"city": null, "country": null, "line1": null, "line2": null, "postal_code": null, "state": null}, "email": null, "name": null, "phone": null}, "calculated_statement_descriptor": "AIRBYTE.IO", "captured": true, "created": 1640120473, "currency": "usd", "customer": null, "description": null, "destination": null, "dispute": null, "disputed": false, "failure_balance_transaction": null, "failure_code": null, "failure_message": null, "fraud_details": {}, "invoice": null, "livemode": false, "metadata": {}, "on_behalf_of": null, "order": null, "outcome": {"network_status": "approved_by_network", "reason": null, "risk_level": "normal", "risk_score": 48, "seller_message": "Payment complete.", "type": "authorized"}, "paid": true, "payment_intent": "pi_3K9FSOEcXtiJtvvh0AEIFllC", "payment_method": "src_1K9FSOEcXtiJtvvhHGu1qtOx", "payment_method_details": {"card": {"amount_authorized": 5300, "brand": "visa", "checks": {"address_line1_check": null, "address_postal_code_check": null, "cvc_check": "pass"}, "country": "US", "exp_month": 12, "exp_year": 2034, "extended_authorization": {"status": "disabled"}, "fingerprint": "X7e9fFB0r8MMcdo6", "funding": "credit", "incremental_authorization": {"status": "unavailable"}, "installments": null, "last4": "4242", "mandate": null, "multicapture": {"status": "unavailable"}, "network": "visa", "network_token": {"used": false}, "overcapture": {"maximum_amount_capturable": 5300, "status": "unavailable"}, "three_d_secure": null, "wallet": null}, "type": "card"}, "receipt_email": null, "receipt_number": "1509-9197", "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xSndub2lFY1h0aUp0dnZoKJ35vqkGMgYYlboX7Hs6LBbBoR6yFToo5WeMCCwbkvCz7nl3E1KToovFFZKMJYnrpAHBlWJrVMJK6BWm", "refunded": false, "refunds": {"object": "list", "data": [], "has_more": false, "total_count": 0.0, "url": "/v1/charges/ch_3K9FSOEcXtiJtvvh0zxb7clc/refunds"}, "review": null, "shipping": null, "source": {"id": "src_1K9FSOEcXtiJtvvhHGu1qtOx", "object": "source", "amount": null, "card": {"address_line1_check": null, "address_zip_check": null, "brand": "Visa", "country": "US", "cvc_check": "pass", "dynamic_last4": null, "exp_month": 12, "exp_year": 2034, "fingerprint": "X7e9fFB0r8MMcdo6", "funding": "credit", "last4": "4242", "name": null, "three_d_secure": "optional", "tokenization_method": null}, "client_secret": "src_client_secret_3WszbFGtWT8vmMjqnNztOwhU", "created": 1640120473, "currency": null, "flow": "none", "livemode": false, "metadata": {}, "owner": {"address": null, "email": null, "name": null, "phone": null, "verified_address": null, "verified_email": null, "verified_name": null, "verified_phone": null}, "statement_descriptor": null, "status": "consumed", "type": "card", "usage": "reusable"}, "source_transfer": null, "statement_descriptor": "airbyte.io", "statement_descriptor_suffix": null, "status": "succeeded", "transfer_data": null, "transfer_group": null, "updated": 1640120473}, "emitted_at": 1697627293840} {"stream": "charges", "data": {"id": "ch_3K9F5DEcXtiJtvvh1w2MaTpj", "object": "charge", "amount": 4200, "amount_captured": 4200, "amount_refunded": 0, "amount_updates": [], "application": null, "application_fee": null, "application_fee_amount": null, "balance_transaction": "txn_3K9F5DEcXtiJtvvh1qsqmHcH", "billing_details": {"address": {"city": null, "country": null, "line1": null, "line2": null, "postal_code": null, "state": null}, "email": null, "name": null, "phone": null}, "calculated_statement_descriptor": "AIRBYTE.IO", "captured": true, "created": 1640119035, "currency": "usd", "customer": null, "description": "edgao test", "destination": null, "dispute": null, "disputed": false, "failure_balance_transaction": null, "failure_code": null, "failure_message": null, "fraud_details": {}, "invoice": null, "livemode": false, "metadata": {}, "on_behalf_of": null, "order": null, "outcome": {"network_status": "approved_by_network", "reason": null, "risk_level": "normal", "risk_score": 63, "seller_message": "Payment complete.", "type": "authorized"}, "paid": true, "payment_intent": "pi_3K9F5DEcXtiJtvvh16scJMp6", "payment_method": "src_1K9F5CEcXtiJtvvhrsZdur8Y", "payment_method_details": {"card": {"amount_authorized": 4200, "brand": "visa", "checks": {"address_line1_check": null, "address_postal_code_check": null, "cvc_check": "pass"}, "country": "US", "exp_month": 9, "exp_year": 2028, "extended_authorization": {"status": "disabled"}, "fingerprint": "X7e9fFB0r8MMcdo6", "funding": "credit", "incremental_authorization": {"status": "unavailable"}, "installments": null, "last4": "4242", "mandate": null, "multicapture": {"status": "unavailable"}, "network": "visa", "network_token": {"used": false}, "overcapture": {"maximum_amount_capturable": 4200, "status": "unavailable"}, "three_d_secure": null, "wallet": null}, "type": "card"}, "receipt_email": null, "receipt_number": "1549-5630", "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xSndub2lFY1h0aUp0dnZoKJ35vqkGMgbg2Y1Ao1M6LBYViHyCHYtYZtCIzc8I1Pm_oXAcXtgPDTNCfzyB3XOfFO4N-RK2w9sLuPjq", "refunded": false, "refunds": {"object": "list", "data": [], "has_more": false, "total_count": 0.0, "url": "/v1/charges/ch_3K9F5DEcXtiJtvvh1w2MaTpj/refunds"}, "review": null, "shipping": null, "source": {"id": "src_1K9F5CEcXtiJtvvhrsZdur8Y", "object": "source", "amount": null, "card": {"address_line1_check": null, "address_zip_check": null, "brand": "Visa", "country": "US", "cvc_check": "pass", "dynamic_last4": null, "exp_month": 9, "exp_year": 2028, "fingerprint": "X7e9fFB0r8MMcdo6", "funding": "credit", "last4": "4242", "name": null, "three_d_secure": "optional", "tokenization_method": null}, "client_secret": "src_client_secret_QyH8xuqSyiZh8oxzzIszqQ92", "created": 1640119035, "currency": null, "flow": "none", "livemode": false, "metadata": {}, "owner": {"address": null, "email": null, "name": null, "phone": null, "verified_address": null, "verified_email": null, "verified_name": null, "verified_phone": null}, "statement_descriptor": null, "status": "consumed", "type": "card", "usage": "reusable"}, "source_transfer": null, "statement_descriptor": "airbyte.io", "statement_descriptor_suffix": null, "status": "succeeded", "transfer_data": null, "transfer_group": null, "updated": 1640119035}, "emitted_at": 1697627293843} @@ -46,7 +46,7 @@ {"stream": "products", "data": {"id": "prod_KouQ5ez86yREmB", "object": "product", "active": true, "attributes": [], "created": 1640124902, "default_price": "price_1K9GbqEcXtiJtvvhJ3lZe4i5", "description": null, "features": [], "images": [], "livemode": false, "metadata": {}, "name": "edgao-test-product", "package_dimensions": null, "shippable": null, "statement_descriptor": null, "tax_code": "txcd_10000000", "type": "service", "unit_label": null, "updated": 1696839715, "url": null}, "emitted_at": 1697627307635} {"stream": "products", "data": {"id": "prod_NHcKselSHfKdfc", "object": "product", "active": true, "attributes": [], "created": 1675345504, "default_price": "price_1MX364EcXtiJtvvhE3WgTl4O", "description": "Test Product 1 description", "features": [], "images": ["https://files.stripe.com/links/MDB8YWNjdF8xSndub2lFY1h0aUp0dnZofGZsX3Rlc3RfdjBOT09UaHRiNVl2WmJ6clNYRUlmcFFD00cCBRNHnV"], "livemode": false, "metadata": {}, "name": "Test Product 1", "package_dimensions": null, "shippable": null, "statement_descriptor": null, "tax_code": "txcd_10301000", "type": "service", "unit_label": null, "updated": 1696839789, "url": null}, "emitted_at": 1697627307877} {"stream": "products", "data": {"id": "prod_NCgx1XP2IFQyKF", "object": "product", "active": true, "attributes": [], "created": 1674209524, "default_price": null, "description": null, "features": [], "images": [], "livemode": false, "metadata": {}, "name": "tu", "package_dimensions": null, "shippable": null, "statement_descriptor": null, "tax_code": "txcd_10000000", "type": "service", "unit_label": null, "updated": 1696839225, "url": null}, "emitted_at": 1697627307879} -{"stream": "subscriptions", "data": {"id": "sub_1O2Dg0EcXtiJtvvhz7Q4zS0n", "object": "subscription", "application": null, "application_fee_percent": null, "automatic_tax": {"enabled": true}, "billing_cycle_anchor": 1697550676.0, "billing_thresholds": null, "cancel_at": 1705499476.0, "cancel_at_period_end": false, "canceled_at": 1697550676.0, "cancellation_details": {"comment": null, "feedback": null, "reason": "cancellation_requested"}, "collection_method": "charge_automatically", "created": 1697550676, "currency": "usd", "current_period_end": 1702821076.0, "current_period_start": 1700229076, "customer": "cus_NGoTFiJFVbSsvZ", "days_until_due": null, "default_payment_method": null, "default_source": null, "default_tax_rates": [], "description": null, "discount": null, "ended_at": null, "items": {"object": "list", "data": [{"id": "si_OptSP2o3XZUBpx", "object": "subscription_item", "billing_thresholds": null, "created": 1697550677, "metadata": {}, "plan": {"id": "price_1MSHZoEcXtiJtvvh6O8TYD8T", "object": "plan", "active": true, "aggregate_usage": null, "amount": 600, "amount_decimal": "600", "billing_scheme": "per_unit", "created": 1674209524, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": null, "product": "prod_NCgx1XP2IFQyKF", "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed"}, "price": {"id": "price_1MSHZoEcXtiJtvvh6O8TYD8T", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1674209524, "currency": "usd", "custom_unit_amount": null, "livemode": false, "lookup_key": null, "metadata": {}, "nickname": null, "product": "prod_NCgx1XP2IFQyKF", "recurring": {"aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": null, "usage_type": "licensed"}, "tax_behavior": "exclusive", "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 600, "unit_amount_decimal": "600"}, "quantity": 1, "subscription": "sub_1O2Dg0EcXtiJtvvhz7Q4zS0n", "tax_rates": []}], "has_more": false, "total_count": 1.0, "url": "/v1/subscription_items?subscription=sub_1O2Dg0EcXtiJtvvhz7Q4zS0n"}, "latest_invoice": "in_1ODSSHEcXtiJtvvhW5LllxDH", "livemode": false, "metadata": {}, "next_pending_invoice_item_invoice": null, "on_behalf_of": null, "pause_collection": null, "payment_settings": {"payment_method_options": null, "payment_method_types": null, "save_default_payment_method": null}, "pending_invoice_item_interval": null, "pending_setup_intent": null, "pending_update": null, "plan": {"id": "price_1MSHZoEcXtiJtvvh6O8TYD8T", "object": "plan", "active": true, "aggregate_usage": null, "amount": 600, "amount_decimal": "600", "billing_scheme": "per_unit", "created": 1674209524, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": null, "product": "prod_NCgx1XP2IFQyKF", "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed"}, "quantity": 1, "schedule": "sub_sched_1O2Dg0EcXtiJtvvh7GtbtIhP", "start_date": 1697550676, "status": "active", "test_clock": null, "transfer_data": null, "trial_end": null, "trial_settings": {"end_behavior": {"missing_payment_method": "create_invoice"}}, "trial_start": null, "updated": 1697550676}, "emitted_at": 1700232971060} +{"stream": "subscriptions", "data": {"id": "sub_1O2Dg0EcXtiJtvvhz7Q4zS0n", "object": "subscription", "application": null, "application_fee_percent": null, "automatic_tax": {"enabled": true}, "billing_cycle_anchor": 1697550676.0, "billing_thresholds": null, "cancel_at": 1705499476.0, "cancel_at_period_end": false, "canceled_at": 1697550676.0, "cancellation_details": {"comment": null, "feedback": null, "reason": "cancellation_requested"}, "collection_method": "charge_automatically", "created": 1697550676, "currency": "usd", "current_period_end": 1705499476.0, "current_period_start": 1702821076, "customer": "cus_NGoTFiJFVbSsvZ", "days_until_due": null, "default_payment_method": null, "default_source": null, "default_tax_rates": [], "description": null, "discount": null, "ended_at": null, "items": {"object": "list", "data": [{"id": "si_OptSP2o3XZUBpx", "object": "subscription_item", "billing_thresholds": null, "created": 1697550677, "metadata": {}, "plan": {"id": "price_1MSHZoEcXtiJtvvh6O8TYD8T", "object": "plan", "active": true, "aggregate_usage": null, "amount": 600, "amount_decimal": "600", "billing_scheme": "per_unit", "created": 1674209524, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": null, "product": "prod_NCgx1XP2IFQyKF", "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed"}, "price": {"id": "price_1MSHZoEcXtiJtvvh6O8TYD8T", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1674209524, "currency": "usd", "custom_unit_amount": null, "livemode": false, "lookup_key": null, "metadata": {}, "nickname": null, "product": "prod_NCgx1XP2IFQyKF", "recurring": {"aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": null, "usage_type": "licensed"}, "tax_behavior": "exclusive", "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 600, "unit_amount_decimal": "600"}, "quantity": 1, "subscription": "sub_1O2Dg0EcXtiJtvvhz7Q4zS0n", "tax_rates": []}], "has_more": false, "total_count": 1.0, "url": "/v1/subscription_items?subscription=sub_1O2Dg0EcXtiJtvvhz7Q4zS0n"}, "latest_invoice": "in_1OOKkUEcXtiJtvvheUUavyuB", "livemode": false, "metadata": {}, "next_pending_invoice_item_invoice": null, "on_behalf_of": null, "pause_collection": null, "payment_settings": {"payment_method_options": null, "payment_method_types": null, "save_default_payment_method": null}, "pending_invoice_item_interval": null, "pending_setup_intent": null, "pending_update": null, "plan": {"id": "price_1MSHZoEcXtiJtvvh6O8TYD8T", "object": "plan", "active": true, "aggregate_usage": null, "amount": 600, "amount_decimal": "600", "billing_scheme": "per_unit", "created": 1674209524, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": null, "product": "prod_NCgx1XP2IFQyKF", "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed"}, "quantity": 1, "schedule": "sub_sched_1O2Dg0EcXtiJtvvh7GtbtIhP", "start_date": 1697550676, "status": "active", "test_clock": null, "transfer_data": null, "trial_end": null, "trial_settings": {"end_behavior": {"missing_payment_method": "create_invoice"}}, "trial_start": null, "updated": 1697550676}, "emitted_at": 1700232971060} {"stream": "subscription_schedule", "data": {"id": "sub_sched_1O2Dg0EcXtiJtvvh7GtbtIhP", "object": "subscription_schedule", "application": null, "canceled_at": null, "completed_at": null, "created": 1697550676, "current_phase": {"end_date": 1705499476, "start_date": 1697550676}, "customer": "cus_NGoTFiJFVbSsvZ", "default_settings": {"application_fee_percent": null, "automatic_tax": {"enabled": false}, "billing_cycle_anchor": "automatic", "billing_thresholds": null, "collection_method": "charge_automatically", "default_payment_method": null, "default_source": null, "description": "Test Test", "invoice_settings": "{'days_until_due': None}", "on_behalf_of": null, "transfer_data": null}, "end_behavior": "cancel", "livemode": false, "metadata": {}, "phases": [{"add_invoice_items": [], "application_fee_percent": null, "automatic_tax": {"enabled": true}, "billing_cycle_anchor": null, "billing_thresholds": null, "collection_method": "charge_automatically", "coupon": null, "currency": "usd", "default_payment_method": null, "default_tax_rates": [], "description": "Test Test", "end_date": 1705499476, "invoice_settings": "{'days_until_due': None}", "items": [{"billing_thresholds": null, "metadata": {}, "plan": "price_1MSHZoEcXtiJtvvh6O8TYD8T", "price": "price_1MSHZoEcXtiJtvvh6O8TYD8T", "quantity": 1, "tax_rates": []}], "metadata": {}, "on_behalf_of": null, "proration_behavior": "create_prorations", "start_date": 1697550676, "transfer_data": null, "trial_end": null}], "released_at": null, "released_subscription": null, "renewal_interval": null, "status": "active", "subscription": "sub_1O2Dg0EcXtiJtvvhz7Q4zS0n", "test_clock": null, "updated": 1697550676}, "emitted_at": 1697627312079} {"stream": "transfers", "data": {"id": "tr_1NH18zEcXtiJtvvhnd827cNO", "object": "transfer", "amount": 10000, "amount_reversed": 0, "balance_transaction": "txn_1NH190EcXtiJtvvhBO3PeR7p", "created": 1686301085, "currency": "usd", "description": null, "destination": "acct_1Jx8unEYmRTj5on1", "destination_payment": "py_1NH18zEYmRTj5on1GkCCsqLK", "livemode": false, "metadata": {}, "reversals": {"object": "list", "data": [], "has_more": false, "total_count": 0.0, "url": "/v1/transfers/tr_1NH18zEcXtiJtvvhnd827cNO/reversals"}, "reversed": false, "source_transaction": null, "source_type": "card", "transfer_group": null, "updated": 1686301085}, "emitted_at": 1697627313262} {"stream": "transfers", "data": {"id": "tr_1NGoaCEcXtiJtvvhjmHtOGOm", "object": "transfer", "amount": 100, "amount_reversed": 100, "balance_transaction": "txn_1NGoaDEcXtiJtvvhsZrNMsdJ", "created": 1686252800, "currency": "usd", "description": null, "destination": "acct_1Jx8unEYmRTj5on1", "destination_payment": "py_1NGoaCEYmRTj5on1LAlAIG3a", "livemode": false, "metadata": {}, "reversals": {"object": "list", "data": [{"id": "trr_1NGolCEcXtiJtvvhOYPck3CP", "object": "transfer_reversal", "amount": 100, "balance_transaction": "txn_1NGolCEcXtiJtvvhZRy4Kd5S", "created": 1686253482, "currency": "usd", "destination_payment_refund": "pyr_1NGolBEYmRTj5on1STal3rmp", "metadata": {}, "source_refund": null, "transfer": "tr_1NGoaCEcXtiJtvvhjmHtOGOm"}], "has_more": false, "total_count": 1.0, "url": "/v1/transfers/tr_1NGoaCEcXtiJtvvhjmHtOGOm/reversals"}, "reversed": true, "source_transaction": null, "source_type": "card", "transfer_group": "ORDER10", "updated": 1686252800}, "emitted_at": 1697627313264} @@ -69,4 +69,4 @@ {"stream": "invoice_line_items", "data": {"id": "il_1MX2yfEcXtiJtvvhiunY2j1x", "object": "line_item", "amount": 25200, "amount_excluding_tax": 25200, "currency": "usd", "description": "edgao-test-product", "discount_amounts": [{"amount": 2520, "discount": "di_1MX2ysEcXtiJtvvh8ORqRVKm"}], "discountable": true, "discounts": ["di_1MX2ysEcXtiJtvvh8ORqRVKm"], "invoice_item": "ii_1MX2yfEcXtiJtvvhfhyOG7SP", "livemode": false, "metadata": {}, "period": {"end": 1675345045, "start": 1675345045}, "plan": null, "price": {"id": "price_1K9GbqEcXtiJtvvhJ3lZe4i5", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1640124902, "currency": "usd", "custom_unit_amount": null, "livemode": false, "lookup_key": null, "metadata": {}, "nickname": null, "product": "prod_KouQ5ez86yREmB", "recurring": null, "tax_behavior": "inclusive", "tiers_mode": null, "transform_quantity": null, "type": "one_time", "unit_amount": 12600, "unit_amount_decimal": "12600"}, "proration": false, "proration_details": {"credited_items": null}, "quantity": 2, "subscription": null, "tax_amounts": [{"amount": 0, "inclusive": true, "tax_rate": "txr_1MX2yfEcXtiJtvvhVcMEMTRj", "taxability_reason": "not_collecting", "taxable_amount": 0}], "tax_rates": [], "type": "invoiceitem", "unit_amount_excluding_tax": "12600", "invoice_id": "in_1MX2yFEcXtiJtvvhMXhUCgKx"}, "emitted_at": 1697627336449} {"stream": "subscription_items", "data": {"id": "si_OptSP2o3XZUBpx", "object": "subscription_item", "billing_thresholds": null, "created": 1697550677, "metadata": {}, "plan": {"id": "price_1MSHZoEcXtiJtvvh6O8TYD8T", "object": "plan", "active": true, "aggregate_usage": null, "amount": 600, "amount_decimal": "600", "billing_scheme": "per_unit", "created": 1674209524, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": null, "product": "prod_NCgx1XP2IFQyKF", "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed"}, "price": {"id": "price_1MSHZoEcXtiJtvvh6O8TYD8T", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1674209524, "currency": "usd", "custom_unit_amount": null, "livemode": false, "lookup_key": null, "metadata": {}, "nickname": null, "product": "prod_NCgx1XP2IFQyKF", "recurring": {"aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": null, "usage_type": "licensed"}, "tax_behavior": "exclusive", "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 600, "unit_amount_decimal": "600"}, "quantity": 1, "subscription": "sub_1O2Dg0EcXtiJtvvhz7Q4zS0n", "tax_rates": []}, "emitted_at": 1697627337431} {"stream": "transfer_reversals", "data": {"id": "trr_1NGolCEcXtiJtvvhOYPck3CP", "object": "transfer_reversal", "amount": 100, "balance_transaction": "txn_1NGolCEcXtiJtvvhZRy4Kd5S", "created": 1686253482, "currency": "usd", "destination_payment_refund": "pyr_1NGolBEYmRTj5on1STal3rmp", "metadata": {}, "source_refund": null, "transfer": "tr_1NGoaCEcXtiJtvvhjmHtOGOm"}, "emitted_at": 1697627338960} -{"stream": "usage_records", "data": {"id": "sis_1ODTdwEcXtiJtvvhZChEVsbN", "object": "usage_record_summary", "invoice": null, "livemode": false, "period": {"end": null, "start": 1700229076}, "subscription_item": "si_OptSP2o3XZUBpx", "total_usage": 1}, "emitted_at": 1700233660884} \ No newline at end of file +{"stream": "usage_records", "data": {"id": "sis_1OUqWiEcXtiJtvvh3WGqc4Vk", "object": "usage_record_summary", "invoice": null, "livemode": false, "period": {"end": null, "start": 1702821076}, "subscription_item": "si_OptSP2o3XZUBpx", "total_usage": 1}, "emitted_at": 1700233660884} diff --git a/airbyte-integrations/connectors/source-stripe/metadata.yaml b/airbyte-integrations/connectors/source-stripe/metadata.yaml index 257fd8162b866..2a734e9f39873 100644 --- a/airbyte-integrations/connectors/source-stripe/metadata.yaml +++ b/airbyte-integrations/connectors/source-stripe/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: e094cb9a-26de-4645-8761-65c0c425d1de - dockerImageTag: 5.1.0 + dockerImageTag: 5.1.1 dockerRepository: airbyte/source-stripe documentationUrl: https://docs.airbyte.com/integrations/sources/stripe githubIssueLabel: source-stripe diff --git a/airbyte-integrations/connectors/source-stripe/source_stripe/schemas/bank_accounts.json b/airbyte-integrations/connectors/source-stripe/source_stripe/schemas/bank_accounts.json index 9a1130c1c5b94..90361867fd2dc 100644 --- a/airbyte-integrations/connectors/source-stripe/source_stripe/schemas/bank_accounts.json +++ b/airbyte-integrations/connectors/source-stripe/source_stripe/schemas/bank_accounts.json @@ -13,6 +13,9 @@ "account_holder_type": { "type": ["null", "string"] }, + "account_type": { + "type": ["null", "string"] + }, "bank_name": { "type": ["null", "string"] }, diff --git a/airbyte-integrations/connectors/source-stripe/source_stripe/source.py b/airbyte-integrations/connectors/source-stripe/source_stripe/source.py index ee85adfb21621..ebb5dd7a1a009 100644 --- a/airbyte-integrations/connectors/source-stripe/source_stripe/source.py +++ b/airbyte-integrations/connectors/source-stripe/source_stripe/source.py @@ -464,12 +464,11 @@ def streams(self, config: Mapping[str, Any]) -> List[Stream]: ), UpdatedCursorIncrementalStripeLazySubStream( name="bank_accounts", - path=lambda self, stream_slice, *args, **kwargs: f"customers/{stream_slice['parent']['id']}/sources", + path=lambda self, stream_slice, *args, **kwargs: f"customers/{stream_slice['parent']['id']}/bank_accounts", parent=self.customers(expand_items=["data.sources"], **args), event_types=["customer.source.created", "customer.source.expiring", "customer.source.updated", "customer.source.deleted"], legacy_cursor_field=None, sub_items_attr="sources", - extra_request_params={"object": "bank_account"}, response_filter=lambda record: record["object"] == "bank_account", **args, ), diff --git a/airbyte-integrations/connectors/source-stripe/source_stripe/streams.py b/airbyte-integrations/connectors/source-stripe/source_stripe/streams.py index 109753a844d7e..7c7185549d432 100644 --- a/airbyte-integrations/connectors/source-stripe/source_stripe/streams.py +++ b/airbyte-integrations/connectors/source-stripe/source_stripe/streams.py @@ -842,7 +842,7 @@ def parse_response(self, response: requests.Response, *args, **kwargs) -> Iterab # as the events API does not support expandable items. Parent class will try getting sub-items from this object, # then from its own API. In case there are no sub-items at all for this entity, API will raise 404 error. self.logger.warning( - "Data was not found for URL: {response.request.url}. " + f"Data was not found for URL: {response.request.url}. " "If this is a path for getting child attributes like /v1/checkout/sessions//line_items when running " "the incremental sync, you may safely ignore this warning." ) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/test_streams.py b/airbyte-integrations/connectors/source-stripe/unit_tests/test_streams.py index 5f942b1521573..55da589609e73 100644 --- a/airbyte-integrations/connectors/source-stripe/unit_tests/test_streams.py +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/test_streams.py @@ -55,12 +55,8 @@ def test_request_headers(stream_by_name): } ], }, - "https://api.stripe.com/v1/customers/cus_HezytZRkaQJC8W/sources?object=bank_account&starting_after=cs_2": { + "https://api.stripe.com/v1/customers/cus_HezytZRkaQJC8W/bank_accounts?starting_after=cs_2": { "data": [ - { - "id": "cs_3", - "object": "card", - }, { "id": "cs_4", "object": "bank_account", @@ -68,8 +64,7 @@ def test_request_headers(stream_by_name): ], "has_more": False, "object": "list", - "total_count": 4, - "url": "/v1/customers/cus_HezytZRkaQJC8W/sources", + "url": "/v1/customers/cus_HezytZRkaQJC8W/bank_accounts", }, }, "bank_accounts", @@ -651,7 +646,7 @@ def test_cursorless_incremental_substream(requests_mock, stream_by_name, sync_mo "has_more": False, }, ) - requests_mock.get("/v1/customers/1/sources", json={"has_more": False, "data": [{"id": 2, "object": "bank_account"}]}) + requests_mock.get("/v1/customers/1/bank_accounts", json={"has_more": False, "data": [{"id": 2, "object": "bank_account"}]}) requests_mock.get( "/v1/events", json={ diff --git a/docs/integrations/sources/stripe.md b/docs/integrations/sources/stripe.md index 0f76e3b166a20..516ef4322ab64 100644 --- a/docs/integrations/sources/stripe.md +++ b/docs/integrations/sources/stripe.md @@ -216,6 +216,7 @@ Each record is marked with `is_deleted` flag when the appropriate event happens | Version | Date | Pull Request | Subject | |:--------|:-----------|:-------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 5.1.1 | 2024-01-04 | [33926](https://github.com/airbytehq/airbyte/pull/33926/) | Update endpoint for `bank_accounts` stream | | 5.1.0 | 2023-12-11 | [32908](https://github.com/airbytehq/airbyte/pull/32908/) | Read full refresh streams concurrently | | 5.0.2 | 2023-12-01 | [33038](https://github.com/airbytehq/airbyte/pull/33038) | Add stream slice logging for SubStream | | 5.0.1 | 2023-11-17 | [32638](https://github.com/airbytehq/airbyte/pull/32638/) | Availability stretegy: check availability of both endpoints (if applicable) - common API + events API | From 6850437d7c87755a905e0600e85fff8aec111a1e Mon Sep 17 00:00:00 2001 From: Artem Inzhyyants <36314070+artem1205@users.noreply.github.com> Date: Mon, 8 Jan 2024 20:39:07 +0100 Subject: [PATCH 003/574] =?UTF-8?q?=F0=9F=90=9B=20Source=20Shopify:=20Fix?= =?UTF-8?q?=20GraphQL=20query=20(#33827)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../source-shopify/acceptance-test-config.yml | 6 ++--- .../integration_tests/expected_records.jsonl | 6 ++--- .../connectors/source-shopify/metadata.yaml | 4 ++-- .../source-shopify/source_shopify/graphql.py | 10 ++++---- .../source-shopify/source_shopify/source.py | 2 ++ .../unit_tests/test_graphql_products.py | 16 +++++++++++++ docs/integrations/sources/shopify.md | 23 ++++++++++--------- 7 files changed, 44 insertions(+), 23 deletions(-) create mode 100644 airbyte-integrations/connectors/source-shopify/unit_tests/test_graphql_products.py diff --git a/airbyte-integrations/connectors/source-shopify/acceptance-test-config.yml b/airbyte-integrations/connectors/source-shopify/acceptance-test-config.yml index 4b1a575ff8ae2..bb0b185cfeda1 100644 --- a/airbyte-integrations/connectors/source-shopify/acceptance-test-config.yml +++ b/airbyte-integrations/connectors/source-shopify/acceptance-test-config.yml @@ -27,7 +27,7 @@ acceptance_tests: basic_read: tests: - config_path: "secrets/config.json" - timeout_seconds: 3600 + timeout_seconds: 4800 expect_records: path: "integration_tests/expected_records.jsonl" empty_streams: @@ -68,12 +68,12 @@ acceptance_tests: configured_catalog_path: "integration_tests/configured_catalog.json" future_state: future_state_path: "integration_tests/abnormal_state.json" - timeout_seconds: 3600 + timeout_seconds: 14400 full_refresh: tests: - config_path: "secrets/config.json" configured_catalog_path: "integration_tests/configured_catalog.json" - timeout_seconds: 3600 + timeout_seconds: 4800 ignored_fields: products: - name: variants/*/updated_at diff --git a/airbyte-integrations/connectors/source-shopify/integration_tests/expected_records.jsonl b/airbyte-integrations/connectors/source-shopify/integration_tests/expected_records.jsonl index 937e24d35c87e..22205205125de 100644 --- a/airbyte-integrations/connectors/source-shopify/integration_tests/expected_records.jsonl +++ b/airbyte-integrations/connectors/source-shopify/integration_tests/expected_records.jsonl @@ -25,7 +25,7 @@ {"stream": "inventory_levels", "data": {"inventory_item_id": 42185194668221, "location_id": 63590301885, "available": 12, "updated_at": "2021-06-22T18:09:27-07:00", "admin_graphql_api_id": "gid://shopify/InventoryLevel/97912455357?inventory_item_id=42185194668221", "shop_url": "airbyte-integration-test", "id": "63590301885|42185194668221"}, "emitted_at": 1697194698578} {"stream": "inventory_levels", "data": {"inventory_item_id": 42185194700989, "location_id": 63590301885, "available": 3, "updated_at": "2021-06-22T18:09:27-07:00", "admin_graphql_api_id": "gid://shopify/InventoryLevel/97912455357?inventory_item_id=42185194700989", "shop_url": "airbyte-integration-test", "id": "63590301885|42185194700989"}, "emitted_at": 1697194698579} {"stream": "inventory_levels", "data": {"inventory_item_id": 42185194733757, "location_id": 63590301885, "available": 38, "updated_at": "2021-06-22T18:09:27-07:00", "admin_graphql_api_id": "gid://shopify/InventoryLevel/97912455357?inventory_item_id=42185194733757", "shop_url": "airbyte-integration-test", "id": "63590301885|42185194733757"}, "emitted_at": 1697194698579} -{"stream": "locations", "data": {"id": 63590301885, "name": "Heroiv UPA 72", "address1": "Heroiv UPA 72", "address2": null, "city": "Lviv", "zip": "30100", "province": null, "country": "UA", "phone": "", "created_at": "2021-06-22T18:00:29-07:00", "updated_at": "2023-02-25T16:20:00-08:00", "country_code": "UA", "country_name": "Ukraine", "province_code": null, "legacy": false, "active": true, "admin_graphql_api_id": "gid://shopify/Location/63590301885", "localized_country_name": "Ukraine", "localized_province_name": null, "shop_url": "airbyte-integration-test"}, "emitted_at": 1697194701440} +{"stream":"locations","data":{"id":63590301885,"name":"Heroiv UPA 72","address1":"Heroiv UPA 72","address2":"","city":"Lviv","zip":"30100","province":null,"country":"UA","phone":"","created_at":"2021-06-22T18:00:29-07:00","updated_at":"2023-11-28T07:08:27-08:00","country_code":"UA","country_name":"Ukraine","province_code":null,"legacy":false,"active":true,"admin_graphql_api_id":"gid://shopify/Location/63590301885","localized_country_name":"Ukraine","localized_province_name":null,"shop_url":"airbyte-integration-test"},"emitted_at":1704314548257} {"stream": "metafield_articles", "data": {"id": 21519818162365, "namespace": "global", "key": "new", "value": "newvalue", "description": null, "owner_id": 558137508029, "created_at": "2022-10-07T16:09:02-07:00", "updated_at": "2022-10-07T16:09:02-07:00", "owner_resource": "article", "type": "single_line_text_field", "admin_graphql_api_id": "gid://shopify/Metafield/21519818162365", "shop_url": "airbyte-integration-test"}, "emitted_at": 1697194703693} {"stream": "metafield_articles", "data": {"id": 22365709992125, "namespace": "custom", "key": "test_blog_post_metafield", "value": "Test Article Metafield", "description": null, "owner_id": 558137508029, "created_at": "2023-04-14T03:18:26-07:00", "updated_at": "2023-04-14T03:18:26-07:00", "owner_resource": "article", "type": "single_line_text_field", "admin_graphql_api_id": "gid://shopify/Metafield/22365709992125", "shop_url": "airbyte-integration-test"}, "emitted_at": 1697194703694} {"stream": "metafield_articles", "data": {"id": 22365710352573, "namespace": "custom", "key": "test_blog_post_metafield", "value": "Test Blog Post Metafiled", "description": null, "owner_id": 558627979453, "created_at": "2023-04-14T03:19:18-07:00", "updated_at": "2023-04-14T03:19:18-07:00", "owner_resource": "article", "type": "single_line_text_field", "admin_graphql_api_id": "gid://shopify/Metafield/22365710352573", "shop_url": "airbyte-integration-test"}, "emitted_at": 1697194704159} @@ -36,8 +36,8 @@ {"stream": "metafield_customers", "data": {"id": 22346893361341, "namespace": "custom", "key": "test_definition_list_1", "value": "Teste\n", "description": null, "owner_id": 6569096478909, "created_at": "2023-04-13T04:50:10-07:00", "updated_at": "2023-04-13T04:50:10-07:00", "owner_resource": "customer", "type": "multi_line_text_field", "admin_graphql_api_id": "gid://shopify/Metafield/22346893361341", "shop_url": "airbyte-integration-test"}, "emitted_at": 1697194711312} {"stream": "metafield_customers", "data": {"id": 22346893394109, "namespace": "custom", "key": "test_definition", "value": "Taster", "description": null, "owner_id": 6569096478909, "created_at": "2023-04-13T04:50:10-07:00", "updated_at": "2023-04-13T04:50:10-07:00", "owner_resource": "customer", "type": "single_line_text_field", "admin_graphql_api_id": "gid://shopify/Metafield/22346893394109", "shop_url": "airbyte-integration-test"}, "emitted_at": 1697194711313} {"stream": "metafield_draft_orders", "data": {"id": 22532787175613, "namespace": "new_metafield", "key": "new_metafield", "value": "updated_mon_24.04.2023", "description": null, "owner_id": 929019691197, "created_at": "2023-04-24T07:18:06-07:00", "updated_at": "2023-04-24T07:18:06-07:00", "owner_resource": "draft_order", "type": "single_line_text_field", "admin_graphql_api_id": "gid://shopify/Metafield/22532787175613", "shop_url": "airbyte-integration-test"}, "emitted_at": 1697194714876} -{"stream": "metafield_locations", "data": {"id": 21524407255229, "namespace": "inventory", "key": "warehouse_2", "value": "234", "description": null, "owner_id": 63590301885, "created_at": "2022-10-12T02:21:27-07:00", "updated_at": "2022-10-12T02:21:27-07:00", "owner_resource": "location", "type": "number_integer", "admin_graphql_api_id": "gid://shopify/Metafield/21524407255229", "shop_url": "airbyte-integration-test"}, "emitted_at": 1697194717772} -{"stream": "metafield_locations", "data": {"id": 21524407681213, "namespace": "inventory", "key": "warehouse_233", "value": "564", "description": null, "owner_id": 63590301885, "created_at": "2022-10-12T02:21:35-07:00", "updated_at": "2022-10-12T02:21:35-07:00", "owner_resource": "location", "type": "number_integer", "admin_graphql_api_id": "gid://shopify/Metafield/21524407681213", "shop_url": "airbyte-integration-test"}, "emitted_at": 1697194717773} +{"stream":"metafield_locations","data":{"id":21524407255229,"namespace":"inventory","key":"warehouse_2","value":"234","description":null,"owner_id":63590301885,"created_at":"2022-10-12T02:21:27-07:00","updated_at":"2022-10-12T02:21:27-07:00","owner_resource":"location","type":"number_integer","admin_graphql_api_id":"gid://shopify/Metafield/21524407255229","shop_url":"airbyte-integration-test"},"emitted_at":1704314554082} +{"stream":"metafield_locations","data":{"id":21524407681213,"namespace":"inventory","key":"warehouse_233","value":"564","description":null,"owner_id":63590301885,"created_at":"2022-10-12T02:21:35-07:00","updated_at":"2022-10-12T02:21:35-07:00","owner_resource":"location","type":"number_integer","admin_graphql_api_id":"gid://shopify/Metafield/21524407681213","shop_url":"airbyte-integration-test"},"emitted_at":1704314554084} {"stream": "metafield_orders", "data": {"id": 22347287855293, "namespace": "my_fields", "key": "purchase_order", "value": "trtrtr", "description": null, "owner_id": 4147980107965, "created_at": "2023-04-13T05:09:08-07:00", "updated_at": "2023-04-13T05:09:08-07:00", "owner_resource": "order", "type": "single_line_text_field", "admin_graphql_api_id": "gid://shopify/Metafield/22347287855293", "shop_url": "airbyte-integration-test"}, "emitted_at": 1697194720313} {"stream": "metafield_orders", "data": {"id": 22365749805245, "namespace": "my_fields", "key": "purchase_order", "value": "Test Draft Order Metafield", "description": null, "owner_id": 3935377129661, "created_at": "2023-04-14T03:52:40-07:00", "updated_at": "2023-04-14T03:52:40-07:00", "owner_resource": "order", "type": "single_line_text_field", "admin_graphql_api_id": "gid://shopify/Metafield/22365749805245", "shop_url": "airbyte-integration-test"}, "emitted_at": 1697194720780} {"stream": "metafield_pages", "data": {"id": 22534014828733, "namespace": "new_metafield", "key": "new_metafield", "value": "updated_mon_24.04.2023", "description": null, "owner_id": 83074252989, "created_at": "2023-04-24T11:08:41-07:00", "updated_at": "2023-04-24T11:08:41-07:00", "owner_resource": "page", "type": "single_line_text_field", "admin_graphql_api_id": "gid://shopify/Metafield/22534014828733", "shop_url": "airbyte-integration-test"}, "emitted_at": 1697194723743} diff --git a/airbyte-integrations/connectors/source-shopify/metadata.yaml b/airbyte-integrations/connectors/source-shopify/metadata.yaml index 2adaad09d627c..544755b10fe30 100644 --- a/airbyte-integrations/connectors/source-shopify/metadata.yaml +++ b/airbyte-integrations/connectors/source-shopify/metadata.yaml @@ -7,11 +7,11 @@ data: - ${shop}.myshopify.com - shopify.com connectorBuildOptions: - baseImage: docker.io/airbyte/python-connector-base:1.1.0@sha256:bd98f6505c6764b1b5f99d3aedc23dfc9e9af631a62533f60eb32b1d3dbab20c + baseImage: docker.io/airbyte/python-connector-base:1.2.0@sha256:c22a9d97464b69d6ef01898edf3f8612dc11614f05a84984451dde195f337db9 connectorSubtype: api connectorType: source definitionId: 9da77001-af33-4bcd-be46-6252bf9342b9 - dockerImageTag: 1.1.4 + dockerImageTag: 1.1.5 dockerRepository: airbyte/source-shopify documentationUrl: https://docs.airbyte.com/integrations/sources/shopify githubIssueLabel: source-shopify diff --git a/airbyte-integrations/connectors/source-shopify/source_shopify/graphql.py b/airbyte-integrations/connectors/source-shopify/source_shopify/graphql.py index e729c74be0f7f..e8d38c64cdff6 100644 --- a/airbyte-integrations/connectors/source-shopify/source_shopify/graphql.py +++ b/airbyte-integrations/connectors/source-shopify/source_shopify/graphql.py @@ -26,10 +26,12 @@ def _camel_to_snake(camel_case: str): def get_query_products(first: int, filter_field: str, filter_value: str, next_page_token: Optional[str]): op = sgqlc.operation.Operation(_schema_root.query_type) snake_case_filter_field = _camel_to_snake(filter_field) - if next_page_token: - products = op.products(first=first, query=f"{snake_case_filter_field}:>'{filter_value}'", after=next_page_token) - else: - products = op.products(first=first, query=f"{snake_case_filter_field}:>'{filter_value}'") + products_args = { + "first": first, + "query": f"{snake_case_filter_field}:>'{filter_value}'" if filter_value else None, + "after": next_page_token, + } + products = op.products(**products_args) products.nodes.id() products.nodes.title() products.nodes.updated_at() diff --git a/airbyte-integrations/connectors/source-shopify/source_shopify/source.py b/airbyte-integrations/connectors/source-shopify/source_shopify/source.py index 8c37b5f18106a..8225aa5f08c6a 100644 --- a/airbyte-integrations/connectors/source-shopify/source_shopify/source.py +++ b/airbyte-integrations/connectors/source-shopify/source_shopify/source.py @@ -78,6 +78,8 @@ def request_params(self, next_page_token: Mapping[str, Any] = None, **kwargs) -> else: params["order"] = f"{self.order_field} asc" params[self.filter_field] = self.default_filter_field_value + if self.config.get("end_date") and self.filter_field == "updated_at_min": + params["updated_at_max"] = self.config.get("end_date") return params @limiter.balance_rate_limit() diff --git a/airbyte-integrations/connectors/source-shopify/unit_tests/test_graphql_products.py b/airbyte-integrations/connectors/source-shopify/unit_tests/test_graphql_products.py new file mode 100644 index 0000000000000..d1a9f02de29ba --- /dev/null +++ b/airbyte-integrations/connectors/source-shopify/unit_tests/test_graphql_products.py @@ -0,0 +1,16 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import pytest +from source_shopify.graphql import get_query_products + + +@pytest.mark.parametrize( + "page_size, filter_value, next_page_token, expected_query", + [ + (100, None, None, 'query {\n products(first: 100, query: null, after: null) {\n nodes {\n id\n title\n updatedAt\n createdAt\n publishedAt\n status\n vendor\n productType\n tags\n options {\n id\n name\n position\n values\n }\n handle\n description\n tracksInventory\n totalInventory\n totalVariants\n onlineStoreUrl\n onlineStorePreviewUrl\n descriptionHtml\n isGiftCard\n legacyResourceId\n mediaCount\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}'), + (200, "2027-07-11T13:07:45-07:00", None, 'query {\n products(first: 200, query: "updated_at:>\'2027-07-11T13:07:45-07:00\'", after: null) {\n nodes {\n id\n title\n updatedAt\n createdAt\n publishedAt\n status\n vendor\n productType\n tags\n options {\n id\n name\n position\n values\n }\n handle\n description\n tracksInventory\n totalInventory\n totalVariants\n onlineStoreUrl\n onlineStorePreviewUrl\n descriptionHtml\n isGiftCard\n legacyResourceId\n mediaCount\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}'), + (250, "2027-07-11T13:07:45-07:00", "end_cursor_value", 'query {\n products(first: 250, query: "updated_at:>\'2027-07-11T13:07:45-07:00\'", after: "end_cursor_value") {\n nodes {\n id\n title\n updatedAt\n createdAt\n publishedAt\n status\n vendor\n productType\n tags\n options {\n id\n name\n position\n values\n }\n handle\n description\n tracksInventory\n totalInventory\n totalVariants\n onlineStoreUrl\n onlineStorePreviewUrl\n descriptionHtml\n isGiftCard\n legacyResourceId\n mediaCount\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}'), + ], +) +def test_get_query_products(page_size, filter_value, next_page_token, expected_query): + assert get_query_products(page_size, 'updatedAt', filter_value, next_page_token) == expected_query diff --git a/docs/integrations/sources/shopify.md b/docs/integrations/sources/shopify.md index a616ff2c502f0..a135de8dd621f 100644 --- a/docs/integrations/sources/shopify.md +++ b/docs/integrations/sources/shopify.md @@ -209,17 +209,18 @@ If a child stream is synced independently of its parent stream, a full sync will ## Changelog | Version | Date | Pull Request | Subject | -| :------ | :--------- | :------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------ | -| 1.1.4 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | -| 1.1.3 | 2023-10-17 | [31500](https://github.com/airbytehq/airbyte/pull/31500) | Fixed the issue caused by the `missing access token` while setup the new source and not yet authenticated | -| 1.1.2 | 2023-10-13 | [31381](https://github.com/airbytehq/airbyte/pull/31381) | Fixed the issue caused by the `state` presence while fetching the `deleted events` with pagination | -| 1.1.1 | 2023-09-18 | [30560](https://github.com/airbytehq/airbyte/pull/30560) | Performance testing - include socat binary in docker image | -| 1.1.0 | 2023-09-07 | [30246](https://github.com/airbytehq/airbyte/pull/30246) | Added ability to fetch `destroyed` records for `Articles, Blogs, CustomCollections, Orders, Pages, PriceRules, Products` | -| 1.0.0 | 2023-08-11 | [29361](https://github.com/airbytehq/airbyte/pull/29361) | Migrate to the `2023-07` Shopify API Version | -| 0.6.2 | 2023-08-09 | [29302](https://github.com/airbytehq/airbyte/pull/29302) | Handle the `Internal Server Error` when entity could be fetched | -| 0.6.1 | 2023-08-08 | [28291](https://github.com/airbytehq/airbyte/pull/28291) | Allow `shop` field to accept `*.myshopify.com` shop names, updated `OAuth Spec` | -| 0.6.0 | 2023-08-02 | [28770](https://github.com/airbytehq/airbyte/pull/28770) | Added `Disputes` stream | -| 0.5.1 | 2023-07-13 | [28700](https://github.com/airbytehq/airbyte/pull/28700) | Improved `error messages` with more user-friendly description, refactored code | +|:--------|:-----------|:---------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------| +| 1.1.5 | 2023-12-28 | [33827](https://github.com/airbytehq/airbyte/pull/33827) | Fix GraphQL query | +| 1.1.4 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | +| 1.1.3 | 2023-10-17 | [31500](https://github.com/airbytehq/airbyte/pull/31500) | Fixed the issue caused by the `missing access token` while setup the new source and not yet authenticated | +| 1.1.2 | 2023-10-13 | [31381](https://github.com/airbytehq/airbyte/pull/31381) | Fixed the issue caused by the `state` presence while fetching the `deleted events` with pagination | +| 1.1.1 | 2023-09-18 | [30560](https://github.com/airbytehq/airbyte/pull/30560) | Performance testing - include socat binary in docker image | +| 1.1.0 | 2023-09-07 | [30246](https://github.com/airbytehq/airbyte/pull/30246) | Added ability to fetch `destroyed` records for `Articles, Blogs, CustomCollections, Orders, Pages, PriceRules, Products` | +| 1.0.0 | 2023-08-11 | [29361](https://github.com/airbytehq/airbyte/pull/29361) | Migrate to the `2023-07` Shopify API Version | +| 0.6.2 | 2023-08-09 | [29302](https://github.com/airbytehq/airbyte/pull/29302) | Handle the `Internal Server Error` when entity could be fetched | +| 0.6.1 | 2023-08-08 | [28291](https://github.com/airbytehq/airbyte/pull/28291) | Allow `shop` field to accept `*.myshopify.com` shop names, updated `OAuth Spec` | +| 0.6.0 | 2023-08-02 | [28770](https://github.com/airbytehq/airbyte/pull/28770) | Added `Disputes` stream | +| 0.5.1 | 2023-07-13 | [28700](https://github.com/airbytehq/airbyte/pull/28700) | Improved `error messages` with more user-friendly description, refactored code | | 0.5.0 | 2023-06-13 | [27732](https://github.com/airbytehq/airbyte/pull/27732) | License Update: Elv2 | | 0.4.0 | 2023-06-13 | [27083](https://github.com/airbytehq/airbyte/pull/27083) | Added `CustomerSavedSearch`, `CustomerAddress` and `Countries` streams | | 0.3.4 | 2023-05-10 | [25961](https://github.com/airbytehq/airbyte/pull/25961) | Added validation for `shop` in input configuration (accepts non-url-like inputs) | From 551f15930c669132789118995dd8cc9140733d3c Mon Sep 17 00:00:00 2001 From: Artem Inzhyyants <36314070+artem1205@users.noreply.github.com> Date: Mon, 8 Jan 2024 20:40:39 +0100 Subject: [PATCH 004/574] =?UTF-8?q?=09=F0=9F=90=9B=20Source=20Marketo:=20R?= =?UTF-8?q?aise=20config=20error=20if=20quota=20exceeded=20(#33999)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../connectors/source-marketo/metadata.yaml | 4 +- .../source-marketo/source_marketo/source.py | 11 ++++- .../source-marketo/unit_tests/test_source.py | 35 ++++++++++++- docs/integrations/sources/marketo.md | 49 ++++++++++--------- 4 files changed, 70 insertions(+), 29 deletions(-) diff --git a/airbyte-integrations/connectors/source-marketo/metadata.yaml b/airbyte-integrations/connectors/source-marketo/metadata.yaml index 7ae5c7d213844..be212d905e415 100644 --- a/airbyte-integrations/connectors/source-marketo/metadata.yaml +++ b/airbyte-integrations/connectors/source-marketo/metadata.yaml @@ -6,11 +6,11 @@ data: hosts: - "*.mktorest.com" connectorBuildOptions: - baseImage: docker.io/airbyte/python-connector-base:1.1.0@sha256:bd98f6505c6764b1b5f99d3aedc23dfc9e9af631a62533f60eb32b1d3dbab20c + baseImage: docker.io/airbyte/python-connector-base:1.2.0@sha256:c22a9d97464b69d6ef01898edf3f8612dc11614f05a84984451dde195f337db9 connectorSubtype: api connectorType: source definitionId: 9e0556f4-69df-4522-a3fb-03264d36b348 - dockerImageTag: 1.2.3 + dockerImageTag: 1.2.4 dockerRepository: airbyte/source-marketo documentationUrl: https://docs.airbyte.com/integrations/sources/marketo githubIssueLabel: source-marketo diff --git a/airbyte-integrations/connectors/source-marketo/source_marketo/source.py b/airbyte-integrations/connectors/source-marketo/source_marketo/source.py index ac276e54b8ad2..62d4ded151966 100644 --- a/airbyte-integrations/connectors/source-marketo/source_marketo/source.py +++ b/airbyte-integrations/connectors/source-marketo/source_marketo/source.py @@ -5,6 +5,7 @@ import csv import datetime import json +import re from abc import ABC from time import sleep from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Tuple @@ -17,6 +18,8 @@ from airbyte_cdk.sources.streams.availability_strategy import AvailabilityStrategy from airbyte_cdk.sources.streams.http import HttpStream from airbyte_cdk.sources.streams.http.auth import Oauth2Authenticator +from airbyte_cdk.utils import AirbyteTracedException +from airbyte_protocol.models import FailureType from .utils import STRING_TYPES, clean_string, format_value, to_datetime_str @@ -300,8 +303,12 @@ def path(self, **kwargs) -> str: def should_retry(self, response: requests.Response) -> bool: if response.status_code == 429 or 500 <= response.status_code < 600: return True - record = next(self.parse_response(response, {}), {}) - status, export_id = record.get("status", "").lower(), record.get("exportId") + if errors := response.json().get("errors"): + if errors[0].get("code") == "1029" and re.match("Export daily quota \d+MB exceeded", errors[0].get("message")): + message = "Daily limit for job extractions has been reached (resets daily at 12:00AM CST)." + raise AirbyteTracedException(internal_message=response.text, message=message, failure_type=FailureType.config_error) + result = response.json().get("result")[0] + status, export_id = result.get("status", "").lower(), result.get("exportId") if status != "created" or not export_id: self.logger.warning(f"Failed to create export job! Status is {status}!") return True diff --git a/airbyte-integrations/connectors/source-marketo/unit_tests/test_source.py b/airbyte-integrations/connectors/source-marketo/unit_tests/test_source.py index 30d1cfdb72f13..e19ac926204dd 100644 --- a/airbyte-integrations/connectors/source-marketo/unit_tests/test_source.py +++ b/airbyte-integrations/connectors/source-marketo/unit_tests/test_source.py @@ -10,8 +10,19 @@ import pendulum import pytest +import requests from airbyte_cdk.models.airbyte_protocol import SyncMode -from source_marketo.source import Activities, Campaigns, IncrementalMarketoStream, Leads, MarketoStream, Programs, SourceMarketo +from airbyte_cdk.utils import AirbyteTracedException +from source_marketo.source import ( + Activities, + Campaigns, + IncrementalMarketoStream, + Leads, + MarketoExportCreate, + MarketoStream, + Programs, + SourceMarketo, +) def test_create_export_job(mocker, send_email_stream, caplog): @@ -26,6 +37,28 @@ def test_create_export_job(mocker, send_email_stream, caplog): assert "Failed to create export job! Status is failed!" in caplog.records[-1].message +def test_should_retry_quota_exceeded(config, requests_mock): + create_job_url = "https://602-euo-598.mktorest.com/rest/v1/leads/export/create.json?batchSize=300" + response_json = { + "requestId": "d2ca#18c0b9833bf", + "success": False, + "errors": [ + { + "code": "1029", + "message": "Export daily quota 500MB exceeded." + } + ] + } + requests_mock.register_uri("GET", create_job_url, status_code=200, json=response_json) + + response = requests.get(create_job_url) + with pytest.raises(AirbyteTracedException) as e: + MarketoExportCreate(config).should_retry(response) + + assert e.value.message == "Daily limit for job extractions has been reached (resets daily at 12:00AM CST)." + + + @pytest.mark.parametrize( "activity, expected_schema", ( diff --git a/docs/integrations/sources/marketo.md b/docs/integrations/sources/marketo.md index 120e1fcee8cb0..4be65376be1b3 100644 --- a/docs/integrations/sources/marketo.md +++ b/docs/integrations/sources/marketo.md @@ -115,27 +115,28 @@ If the 50,000 limit is too stringent, contact Marketo support for a quota increa ## Changelog -| Version | Date | Pull Request | Subject | -|:---------|:-----------|:---------------------------------------------------------|:-------------------------------------------------------------------------------------------------| -| `1.2.3` | 2023-08-02 | [28999](https://github.com/airbytehq/airbyte/pull/28999) | Fix for ` _csv.Error: line contains NUL` | -| `1.2.2` | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | -| `1.2.1` | 2023-09-18 | [30533](https://github.com/airbytehq/airbyte/pull/30533) | Fix `json_schema` for stream `Leads` | -| `1.2.0` | 2023-06-26 | [27726](https://github.com/airbytehq/airbyte/pull/27726) | License Update: Elv2 | -| `1.1.0` | 2023-04-18 | [23956](https://github.com/airbytehq/airbyte/pull/23956) | Add `Segmentations` Stream | -| `1.0.4` | 2023-04-25 | [25481](https://github.com/airbytehq/airbyte/pull/25481) | Minor fix for bug caused by `<=` producing additional API call when there is a single date slice | -| `1.0.3` | 2023-02-13 | [22938](https://github.com/airbytehq/airbyte/pull/22938) | Specified date formatting in specification | -| `1.0.2` | 2023-02-01 | [22203](https://github.com/airbytehq/airbyte/pull/22203) | Handle Null cursor values | -| `1.0.1` | 2023-01-31 | [22015](https://github.com/airbytehq/airbyte/pull/22015) | Set `AvailabilityStrategy` for streams explicitly to `None` | -| `1.0.0` | 2023-01-25 | [21790](https://github.com/airbytehq/airbyte/pull/21790) | Fix `activities_*` stream schemas | -| `0.1.12` | 2023-01-19 | [20973](https://github.com/airbytehq/airbyte/pull/20973) | Fix encoding error (note: this change is not in version 1.0.0, but is in later versions | -| `0.1.11` | 2022-09-30 | [17445](https://github.com/airbytehq/airbyte/pull/17445) | Do not use temporary files for memory optimization | -| `0.1.10` | 2022-09-30 | [17445](https://github.com/airbytehq/airbyte/pull/17445) | Optimize memory consumption | -| `0.1.9` | 2022-09-28 | [17304](https://github.com/airbytehq/airbyte/pull/17304) | Migrate to per-stream sate. | -| `0.1.7` | 2022-08-23 | [15817](https://github.com/airbytehq/airbyte/pull/15817) | Improved unit test coverage | -| `0.1.6` | 2022-08-21 | [15824](https://github.com/airbytehq/airbyte/pull/15824) | Fix semi incremental streams: do not ignore start date, make one api call instead of multiple | -| `0.1.5` | 2022-08-16 | [15683](https://github.com/airbytehq/airbyte/pull/15683) | Retry failed creation of a job instead of skipping it | -| `0.1.4` | 2022-06-20 | [13930](https://github.com/airbytehq/airbyte/pull/13930) | Process failing creation of export jobs | -| `0.1.3` | 2021-12-10 | [8429](https://github.com/airbytehq/airbyte/pull/8578) | Updated titles and descriptions | -| `0.1.2` | 2021-12-03 | [8483](https://github.com/airbytehq/airbyte/pull/8483) | Improve field conversion to conform schema | -| `0.1.1` | 2021-11-29 | [0000](https://github.com/airbytehq/airbyte/pull/0000) | Fix timestamp value format issue | -| `0.1.0` | 2021-09-06 | [5863](https://github.com/airbytehq/airbyte/pull/5863) | Release Marketo CDK Connector | \ No newline at end of file +| Version | Date | Pull Request | Subject | +|:---------|:-----------|:---------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------| +| `1.2.4` | 2024-01-08 | [33999](https://github.com/airbytehq/airbyte/pull/33999) | Fix for `Export daily quota exceeded` | +| `1.2.3` | 2023-08-02 | [28999](https://github.com/airbytehq/airbyte/pull/28999) | Fix for ` _csv.Error: line contains NUL` | +| `1.2.2` | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | +| `1.2.1` | 2023-09-18 | [30533](https://github.com/airbytehq/airbyte/pull/30533) | Fix `json_schema` for stream `Leads` | +| `1.2.0` | 2023-06-26 | [27726](https://github.com/airbytehq/airbyte/pull/27726) | License Update: Elv2 | +| `1.1.0` | 2023-04-18 | [23956](https://github.com/airbytehq/airbyte/pull/23956) | Add `Segmentations` Stream | +| `1.0.4` | 2023-04-25 | [25481](https://github.com/airbytehq/airbyte/pull/25481) | Minor fix for bug caused by `<=` producing additional API call when there is a single date slice | +| `1.0.3` | 2023-02-13 | [22938](https://github.com/airbytehq/airbyte/pull/22938) | Specified date formatting in specification | +| `1.0.2` | 2023-02-01 | [22203](https://github.com/airbytehq/airbyte/pull/22203) | Handle Null cursor values | +| `1.0.1` | 2023-01-31 | [22015](https://github.com/airbytehq/airbyte/pull/22015) | Set `AvailabilityStrategy` for streams explicitly to `None` | +| `1.0.0` | 2023-01-25 | [21790](https://github.com/airbytehq/airbyte/pull/21790) | Fix `activities_*` stream schemas | +| `0.1.12` | 2023-01-19 | [20973](https://github.com/airbytehq/airbyte/pull/20973) | Fix encoding error (note: this change is not in version 1.0.0, but is in later versions | +| `0.1.11` | 2022-09-30 | [17445](https://github.com/airbytehq/airbyte/pull/17445) | Do not use temporary files for memory optimization | +| `0.1.10` | 2022-09-30 | [17445](https://github.com/airbytehq/airbyte/pull/17445) | Optimize memory consumption | +| `0.1.9` | 2022-09-28 | [17304](https://github.com/airbytehq/airbyte/pull/17304) | Migrate to per-stream sate. | +| `0.1.7` | 2022-08-23 | [15817](https://github.com/airbytehq/airbyte/pull/15817) | Improved unit test coverage | +| `0.1.6` | 2022-08-21 | [15824](https://github.com/airbytehq/airbyte/pull/15824) | Fix semi incremental streams: do not ignore start date, make one api call instead of multiple | +| `0.1.5` | 2022-08-16 | [15683](https://github.com/airbytehq/airbyte/pull/15683) | Retry failed creation of a job instead of skipping it | +| `0.1.4` | 2022-06-20 | [13930](https://github.com/airbytehq/airbyte/pull/13930) | Process failing creation of export jobs | +| `0.1.3` | 2021-12-10 | [8429](https://github.com/airbytehq/airbyte/pull/8578) | Updated titles and descriptions | +| `0.1.2` | 2021-12-03 | [8483](https://github.com/airbytehq/airbyte/pull/8483) | Improve field conversion to conform schema | +| `0.1.1` | 2021-11-29 | [0000](https://github.com/airbytehq/airbyte/pull/0000) | Fix timestamp value format issue | +| `0.1.0` | 2021-09-06 | [5863](https://github.com/airbytehq/airbyte/pull/5863) | Release Marketo CDK Connector | \ No newline at end of file From 8b00460d2f46e45aa110078894efee237b2d6606 Mon Sep 17 00:00:00 2001 From: Joe Bell Date: Mon, 8 Jan 2024 11:54:50 -0800 Subject: [PATCH 005/574] Destination Redshift - Reorder Spec options (#34014) --- .../destination-redshift/metadata.yaml | 2 +- .../src/main/resources/spec.json | 17 ++++++++++------- docs/integrations/destinations/redshift.md | 3 ++- .../upgrading_to_destinations_v2.md | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/airbyte-integrations/connectors/destination-redshift/metadata.yaml b/airbyte-integrations/connectors/destination-redshift/metadata.yaml index a363329ca957e..9531fc7f51b15 100644 --- a/airbyte-integrations/connectors/destination-redshift/metadata.yaml +++ b/airbyte-integrations/connectors/destination-redshift/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: database connectorType: destination definitionId: f7a7d195-377f-cf5b-70a5-be6b819019dc - dockerImageTag: 0.7.13 + dockerImageTag: 0.7.14 dockerRepository: airbyte/destination-redshift documentationUrl: https://docs.airbyte.com/integrations/destinations/redshift githubIssueLabel: destination-redshift diff --git a/airbyte-integrations/connectors/destination-redshift/src/main/resources/spec.json b/airbyte-integrations/connectors/destination-redshift/src/main/resources/spec.json index 3e11adfd9bc9d..28a274642bbf7 100644 --- a/airbyte-integrations/connectors/destination-redshift/src/main/resources/spec.json +++ b/airbyte-integrations/connectors/destination-redshift/src/main/resources/spec.json @@ -253,28 +253,31 @@ "type": "boolean", "description": "(Early Access) Use Destinations V2.", "title": "Use Destinations V2 (Early Access)", - "order": 9 + "order": 9, + "group": "connection" }, "raw_data_schema": { "type": "string", "description": "(Early Access) The schema to write raw tables into", "title": "Destinations V2 Raw Table Schema (Early Access)", - "order": 10 + "order": 10, + "group": "connection" }, "enable_incremental_final_table_updates": { "type": "boolean", "default": false, "description": "When enabled your data will load into your final tables incrementally while your data is still being synced. When Disabled (the default), your data loads into your final tables once at the end of a sync. Note that this option only applies if you elect to create Final tables", - "title": "Enable Loading Data Incrementally to Final Tables", - "order": 11 + "title": "Enable Loading Data Incrementally to Final Tables (Early Access)", + "order": 11, + "group": "connection" }, "disable_type_dedupe": { "type": "boolean", "default": false, "description": "Disable Writing Final Tables. WARNING! The data format in _airbyte_data is likely stable but there are no guarantees that other metadata columns will remain the same in future versions", - "title": "Disable Final Tables. (WARNING! Unstable option; Columns in raw table schema might change between versions)", - "order": 8, - "group": "advanced" + "title": "Disable Final Tables. (WARNING! Unstable option; Columns in raw table schema might change between versions) (Early Access)", + "order": 12, + "group": "connection" } }, "groups": [ diff --git a/docs/integrations/destinations/redshift.md b/docs/integrations/destinations/redshift.md index e4e484650c957..63553b1d06d82 100644 --- a/docs/integrations/destinations/redshift.md +++ b/docs/integrations/destinations/redshift.md @@ -215,8 +215,9 @@ Each stream will be output into its own raw table in Redshift. Each table will c | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0.7.14 | 2024-01-08 | [\#34014](https://github.com/airbytehq/airbyte/pull/34014) | Update order of options in spec | | 0.7.13 | 2024-01-05 | [\#33948](https://github.com/airbytehq/airbyte/pull/33948) | Fix NPE when prepare tables fail; Add case sensitive session for super; Bastion heartbeats added | -| 0.7.12 | 2024-01-03 | [#33924](https://github.com/airbytehq/airbyte/pull/33924) | Add new ap-southeast-3 AWS region | +| 0.7.12 | 2024-01-03 | [\#33924](https://github.com/airbytehq/airbyte/pull/33924) | Add new ap-southeast-3 AWS region | | 0.7.11 | 2024-01-04 | [\#33730](https://github.com/airbytehq/airbyte/pull/33730) | Internal code structure changes | | 0.7.10 | 2024-01-04 | [\#33728](https://github.com/airbytehq/airbyte/pull/33728) | Allow users to disable final table creation | | 0.7.9 | 2024-01-03 | [\#33877](https://github.com/airbytehq/airbyte/pull/33877) | Fix Jooq StackOverflowError | diff --git a/docs/release_notes/upgrading_to_destinations_v2.md b/docs/release_notes/upgrading_to_destinations_v2.md index 2201f7b16fdfc..eee8ad098b474 100644 --- a/docs/release_notes/upgrading_to_destinations_v2.md +++ b/docs/release_notes/upgrading_to_destinations_v2.md @@ -133,7 +133,7 @@ When you are done testing, you can disable or delete this testing connection, an If you have written downstream transformations directly from the output of raw tables, or use the "Raw JSON" normalization setting, you should know that: - Multiple column names are being updated (from `airbyte_ab_id` to `airbyte_raw_id`, and `airbyte_emitted_at` to `airbyte_extracted_at`). -- The location of raw tables will from now on default to an `airbyte` schema in your destination. +- The location of raw tables will from now on default to an `airbyte_internal` schema in your destination. - When you upgrade to a [Destinations V2 compatible version](#destinations-v2-effective-versions) of your destination, we will leave a copy of your existing raw tables as they are, and new syncs will work from a new copy we make in the new `airbyte_internal` schema. Although existing downstream dashboards will go stale, they will not be broken. - You can dual write by following the [steps above](#upgrading-connections-one-by-one-with-dual-writing) and copying your raw data to the schema of your newly created connection. From 4955933b6a85f784502a9bf7ef048ad4bce070e4 Mon Sep 17 00:00:00 2001 From: Augustin Date: Mon, 8 Jan 2024 21:00:40 +0100 Subject: [PATCH 006/574] airbyte-ci: log more context info in CI (#33994) --- airbyte-ci/connectors/pipelines/README.md | 1 + .../connectors/pipelines/pipelines/cli/airbyte_ci.py | 7 +++++-- airbyte-ci/connectors/pipelines/pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/airbyte-ci/connectors/pipelines/README.md b/airbyte-ci/connectors/pipelines/README.md index 6ce7c9c1a0e4d..ad890b75397d2 100644 --- a/airbyte-ci/connectors/pipelines/README.md +++ b/airbyte-ci/connectors/pipelines/README.md @@ -521,6 +521,7 @@ E.G.: running `pytest` on a specific test folder: | Version | PR | Description | | ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| 3.1.0 | [#33994](https://github.com/airbytehq/airbyte/pull/33994) | Log more context information in CI. | | 3.0.2 | [#33987](https://github.com/airbytehq/airbyte/pull/33987) | Fix type checking issue when running --help | | 3.0.1 | [#33981](https://github.com/airbytehq/airbyte/pull/33981) | Fix issues with deploying dagster, pin pendulum version in dagster-cli install | | 3.0.0 | [#33582](https://github.com/airbytehq/airbyte/pull/33582) | Upgrade to Dagger 0.9.5 | diff --git a/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py b/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py index 0b64fa7bcb97f..028bb54df7c74 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py +++ b/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py @@ -24,6 +24,7 @@ from pipelines.cli.lazy_group import LazyGroup from pipelines.cli.telemetry import click_track_command from pipelines.consts import DAGGER_WRAP_ENV_VAR_NAME, CIContext +from pipelines.dagger.actions.connector.hooks import get_dagger_sdk_version from pipelines.helpers import github from pipelines.helpers.git import get_current_git_branch, get_current_git_revision from pipelines.helpers.utils import get_current_epoch_time @@ -81,7 +82,9 @@ def set_working_directory_to_root() -> None: os.chdir(working_dir) -def log_git_info(ctx: click.Context) -> None: +def log_context_info(ctx: click.Context) -> None: + main_logger.info(f"Running airbyte-ci version {__installed_version__}") + main_logger.info(f"Running dagger version {get_dagger_sdk_version()}") main_logger.info("Running airbyte-ci in CI mode.") main_logger.info(f"CI Context: {ctx.obj['ci_context']}") main_logger.info(f"CI Report Bucket Name: {ctx.obj['ci_report_bucket_name']}") @@ -235,7 +238,7 @@ async def airbyte_ci(ctx: click.Context) -> None: # noqa D103 check_local_docker_configuration() if not ctx.obj["is_local"]: - log_git_info(ctx) + log_context_info(ctx) set_working_directory_to_root() diff --git a/airbyte-ci/connectors/pipelines/pyproject.toml b/airbyte-ci/connectors/pipelines/pyproject.toml index 4c71cb053f515..faf30e9fc8add 100644 --- a/airbyte-ci/connectors/pipelines/pyproject.toml +++ b/airbyte-ci/connectors/pipelines/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pipelines" -version = "3.0.2" +version = "3.1.0" description = "Packaged maintained by the connector operations team to perform CI for connectors' pipelines" authors = ["Airbyte "] From f8f64386e4b9c3b3f2b8a70bf0d45146d6024d56 Mon Sep 17 00:00:00 2001 From: "Roman Yermilov [GL]" <86300758+roman-yermilov-gl@users.noreply.github.com> Date: Mon, 8 Jan 2024 21:43:20 +0100 Subject: [PATCH 007/574] =?UTF-8?q?=F0=9F=90=9BSource=20Hubspot:=20fix=20p?= =?UTF-8?q?roperty=5Fhistory=20PK=20(#33844)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration_tests/expected_records.jsonl | 8 +++--- .../connectors/source-hubspot/metadata.yaml | 2 +- .../source-hubspot/source_hubspot/streams.py | 27 +++++++++++++++---- .../source-hubspot/unit_tests/test_source.py | 2 +- docs/integrations/sources/hubspot.md | 1 + 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/airbyte-integrations/connectors/source-hubspot/integration_tests/expected_records.jsonl b/airbyte-integrations/connectors/source-hubspot/integration_tests/expected_records.jsonl index d6d1c26ba1d9d..0109a703be275 100644 --- a/airbyte-integrations/connectors/source-hubspot/integration_tests/expected_records.jsonl +++ b/airbyte-integrations/connectors/source-hubspot/integration_tests/expected_records.jsonl @@ -64,9 +64,9 @@ {"stream": "workflows", "data": {"migrationStatus": {"portalId": 8727216, "workflowId": 21058115, "migrationStatus": "EXECUTION_MIGRATED", "enrollmentMigrationStatus": "PLATFORM_OWNED", "platformOwnsActions": true, "lastSuccessfulMigrationTimestamp": null, "enrollmentMigrationTimestamp": null, "flowId": 50206671}, "name": "Test Workflow", "id": 21058115, "type": "DRIP_DELAY", "enabled": false, "creationSource": {"sourceApplication": {"source": "DIRECT_API"}, "createdAt": 1610635826795}, "updateSource": {"sourceApplication": {"source": "DIRECT_API", "serviceName": "AutomationPlatformService-web_BackfillILSListIds"}, "updatedAt": 1611847907577}, "contactListIds": {"enrolled": 12, "active": 13, "completed": 14, "succeeded": 15}, "personaTagIds": [], "contactCounts": {"active": 0, "enrolled": 0}, "portalId": 8727216, "insertedAt": 1610635826921, "updatedAt": 1611847907577, "contactListIds_enrolled": 12, "contactListIds_active": 13, "contactListIds_completed": 14, "contactListIds_succeeded": 15}, "emitted_at": 1697714264418} {"stream": "workflows", "data": {"migrationStatus": {"portalId": 8727216, "workflowId": 21058121, "migrationStatus": "EXECUTION_MIGRATED", "enrollmentMigrationStatus": "PLATFORM_OWNED", "platformOwnsActions": true, "lastSuccessfulMigrationTimestamp": null, "enrollmentMigrationTimestamp": null, "flowId": 50205684}, "name": "Test Workflow 1", "id": 21058121, "type": "DRIP_DELAY", "enabled": false, "creationSource": {"sourceApplication": {"source": "DIRECT_API"}, "createdAt": 1610635850713}, "updateSource": {"sourceApplication": {"source": "DIRECT_API", "serviceName": "AutomationPlatformService-web_BackfillILSListIds"}, "updatedAt": 1611847907579}, "contactListIds": {"enrolled": 16, "active": 17, "completed": 18, "succeeded": 19}, "personaTagIds": [], "contactCounts": {"active": 0, "enrolled": 0}, "portalId": 8727216, "insertedAt": 1610635850758, "updatedAt": 1611847907579, "contactListIds_enrolled": 16, "contactListIds_active": 17, "contactListIds_completed": 18, "contactListIds_succeeded": 19}, "emitted_at": 1697714264419} {"stream": "workflows", "data": {"migrationStatus": {"portalId": 8727216, "workflowId": 21058122, "migrationStatus": "EXECUTION_MIGRATED", "enrollmentMigrationStatus": "PLATFORM_OWNED", "platformOwnsActions": true, "lastSuccessfulMigrationTimestamp": null, "enrollmentMigrationTimestamp": null, "flowId": 50205036}, "name": "Test Workflow 2", "id": 21058122, "type": "DRIP_DELAY", "enabled": false, "creationSource": {"sourceApplication": {"source": "DIRECT_API"}, "createdAt": 1610635859664}, "updateSource": {"sourceApplication": {"source": "DIRECT_API", "serviceName": "AutomationPlatformService-web_BackfillILSListIds"}, "updatedAt": 1611847907578}, "contactListIds": {"enrolled": 20, "active": 21, "completed": 22, "succeeded": 23}, "personaTagIds": [], "contactCounts": {"active": 0, "enrolled": 0}, "portalId": 8727216, "insertedAt": 1610635859748, "updatedAt": 1611847907578, "contactListIds_enrolled": 20, "contactListIds_active": 21, "contactListIds_completed": 22, "contactListIds_succeeded": 23}, "emitted_at": 1697714264420} -{"stream": "cars", "data": {"id": "5938880072", "properties": {"car_id": 1, "car_name": 3232324, "hs_all_accessible_team_ids": null, "hs_all_assigned_business_unit_ids": null, "hs_all_owner_ids": null, "hs_all_team_ids": null, "hs_created_by_user_id": 12282590, "hs_createdate": "2023-04-12T17:57:15.836000+00:00", "hs_lastmodifieddate": "2023-04-12T17:59:20.189000+00:00", "hs_merged_object_ids": null, "hs_object_id": 5938880072, "hs_object_source": null, "hs_object_source_id": null, "hs_object_source_label": null, "hs_object_source_user_id": null, "hs_pinned_engagement_id": null, "hs_read_only": null, "hs_unique_creation_key": null, "hs_updated_by_user_id": 12282590, "hs_user_ids_of_all_notification_followers": null, "hs_user_ids_of_all_notification_unfollowers": null, "hs_user_ids_of_all_owners": null, "hs_was_imported": null, "hubspot_owner_assigneddate": null, "hubspot_owner_id": null, "hubspot_team_id": null}, "createdAt": "2023-04-12T17:57:15.836Z", "updatedAt": "2023-04-12T17:59:20.189Z", "archived": false, "properties_car_id": 1, "properties_car_name": 3232324, "properties_hs_all_accessible_team_ids": null, "properties_hs_all_assigned_business_unit_ids": null, "properties_hs_all_owner_ids": null, "properties_hs_all_team_ids": null, "properties_hs_created_by_user_id": 12282590, "properties_hs_createdate": "2023-04-12T17:57:15.836000+00:00", "properties_hs_lastmodifieddate": "2023-04-12T17:59:20.189000+00:00", "properties_hs_merged_object_ids": null, "properties_hs_object_id": 5938880072, "properties_hs_object_source": null, "properties_hs_object_source_id": null, "properties_hs_object_source_label": null, "properties_hs_object_source_user_id": null, "properties_hs_pinned_engagement_id": null, "properties_hs_read_only": null, "properties_hs_unique_creation_key": null, "properties_hs_updated_by_user_id": 12282590, "properties_hs_user_ids_of_all_notification_followers": null, "properties_hs_user_ids_of_all_notification_unfollowers": null, "properties_hs_user_ids_of_all_owners": null, "properties_hs_was_imported": null, "properties_hubspot_owner_assigneddate": null, "properties_hubspot_owner_id": null, "properties_hubspot_team_id": null}, "emitted_at": 1697714265295} -{"stream": "cars", "data": {"id": "5938880073", "properties": {"car_id": 2, "car_name": 23232, "hs_all_accessible_team_ids": null, "hs_all_assigned_business_unit_ids": null, "hs_all_owner_ids": null, "hs_all_team_ids": null, "hs_created_by_user_id": 12282590, "hs_createdate": "2023-04-12T17:57:20.583000+00:00", "hs_lastmodifieddate": "2023-04-12T17:59:20.189000+00:00", "hs_merged_object_ids": null, "hs_object_id": 5938880073, "hs_object_source": null, "hs_object_source_id": null, "hs_object_source_label": null, "hs_object_source_user_id": null, "hs_pinned_engagement_id": null, "hs_read_only": null, "hs_unique_creation_key": null, "hs_updated_by_user_id": 12282590, "hs_user_ids_of_all_notification_followers": null, "hs_user_ids_of_all_notification_unfollowers": null, "hs_user_ids_of_all_owners": null, "hs_was_imported": null, "hubspot_owner_assigneddate": null, "hubspot_owner_id": null, "hubspot_team_id": null}, "createdAt": "2023-04-12T17:57:20.583Z", "updatedAt": "2023-04-12T17:59:20.189Z", "archived": false, "properties_car_id": 2, "properties_car_name": 23232, "properties_hs_all_accessible_team_ids": null, "properties_hs_all_assigned_business_unit_ids": null, "properties_hs_all_owner_ids": null, "properties_hs_all_team_ids": null, "properties_hs_created_by_user_id": 12282590, "properties_hs_createdate": "2023-04-12T17:57:20.583000+00:00", "properties_hs_lastmodifieddate": "2023-04-12T17:59:20.189000+00:00", "properties_hs_merged_object_ids": null, "properties_hs_object_id": 5938880073, "properties_hs_object_source": null, "properties_hs_object_source_id": null, "properties_hs_object_source_label": null, "properties_hs_object_source_user_id": null, "properties_hs_pinned_engagement_id": null, "properties_hs_read_only": null, "properties_hs_unique_creation_key": null, "properties_hs_updated_by_user_id": 12282590, "properties_hs_user_ids_of_all_notification_followers": null, "properties_hs_user_ids_of_all_notification_unfollowers": null, "properties_hs_user_ids_of_all_owners": null, "properties_hs_was_imported": null, "properties_hubspot_owner_assigneddate": null, "properties_hubspot_owner_id": null, "properties_hubspot_team_id": null}, "emitted_at": 1697714265296} -{"stream": "pets", "data": {"id": "5936415312", "properties": {"hs_all_accessible_team_ids": null, "hs_all_assigned_business_unit_ids": null, "hs_all_owner_ids": null, "hs_all_team_ids": null, "hs_created_by_user_id": 12282590, "hs_createdate": "2023-04-12T17:08:50.632000+00:00", "hs_lastmodifieddate": "2023-04-12T17:08:50.632000+00:00", "hs_merged_object_ids": null, "hs_object_id": 5936415312, "hs_object_source": null, "hs_object_source_id": null, "hs_object_source_label": null, "hs_object_source_user_id": null, "hs_pinned_engagement_id": null, "hs_read_only": null, "hs_unique_creation_key": null, "hs_updated_by_user_id": 12282590, "hs_user_ids_of_all_notification_followers": null, "hs_user_ids_of_all_notification_unfollowers": null, "hs_user_ids_of_all_owners": null, "hs_was_imported": null, "hubspot_owner_assigneddate": null, "hubspot_owner_id": null, "hubspot_team_id": null, "pet_name": "Marcos Pet", "pet_type": "Dog"}, "createdAt": "2023-04-12T17:08:50.632Z", "updatedAt": "2023-04-12T17:08:50.632Z", "archived": false, "properties_hs_all_accessible_team_ids": null, "properties_hs_all_assigned_business_unit_ids": null, "properties_hs_all_owner_ids": null, "properties_hs_all_team_ids": null, "properties_hs_created_by_user_id": 12282590, "properties_hs_createdate": "2023-04-12T17:08:50.632000+00:00", "properties_hs_lastmodifieddate": "2023-04-12T17:08:50.632000+00:00", "properties_hs_merged_object_ids": null, "properties_hs_object_id": 5936415312, "properties_hs_object_source": null, "properties_hs_object_source_id": null, "properties_hs_object_source_label": null, "properties_hs_object_source_user_id": null, "properties_hs_pinned_engagement_id": null, "properties_hs_read_only": null, "properties_hs_unique_creation_key": null, "properties_hs_updated_by_user_id": 12282590, "properties_hs_user_ids_of_all_notification_followers": null, "properties_hs_user_ids_of_all_notification_unfollowers": null, "properties_hs_user_ids_of_all_owners": null, "properties_hs_was_imported": null, "properties_hubspot_owner_assigneddate": null, "properties_hubspot_owner_id": null, "properties_hubspot_team_id": null, "properties_pet_name": "Marcos Pet", "properties_pet_type": "Dog"}, "emitted_at": 1697714266285} -{"stream": "pets", "data": {"id": "5938880054", "properties": {"hs_all_accessible_team_ids": null, "hs_all_assigned_business_unit_ids": null, "hs_all_owner_ids": null, "hs_all_team_ids": null, "hs_created_by_user_id": 12282590, "hs_createdate": "2023-04-12T17:53:12.692000+00:00", "hs_lastmodifieddate": "2023-04-12T17:53:12.692000+00:00", "hs_merged_object_ids": null, "hs_object_id": 5938880054, "hs_object_source": null, "hs_object_source_id": null, "hs_object_source_label": null, "hs_object_source_user_id": null, "hs_pinned_engagement_id": null, "hs_read_only": null, "hs_unique_creation_key": null, "hs_updated_by_user_id": 12282590, "hs_user_ids_of_all_notification_followers": null, "hs_user_ids_of_all_notification_unfollowers": null, "hs_user_ids_of_all_owners": null, "hs_was_imported": null, "hubspot_owner_assigneddate": null, "hubspot_owner_id": null, "hubspot_team_id": null, "pet_name": "Integration Test Pet", "pet_type": "Unknown"}, "createdAt": "2023-04-12T17:53:12.692Z", "updatedAt": "2023-04-12T17:53:12.692Z", "archived": false, "properties_hs_all_accessible_team_ids": null, "properties_hs_all_assigned_business_unit_ids": null, "properties_hs_all_owner_ids": null, "properties_hs_all_team_ids": null, "properties_hs_created_by_user_id": 12282590, "properties_hs_createdate": "2023-04-12T17:53:12.692000+00:00", "properties_hs_lastmodifieddate": "2023-04-12T17:53:12.692000+00:00", "properties_hs_merged_object_ids": null, "properties_hs_object_id": 5938880054, "properties_hs_object_source": null, "properties_hs_object_source_id": null, "properties_hs_object_source_label": null, "properties_hs_object_source_user_id": null, "properties_hs_pinned_engagement_id": null, "properties_hs_read_only": null, "properties_hs_unique_creation_key": null, "properties_hs_updated_by_user_id": 12282590, "properties_hs_user_ids_of_all_notification_followers": null, "properties_hs_user_ids_of_all_notification_unfollowers": null, "properties_hs_user_ids_of_all_owners": null, "properties_hs_was_imported": null, "properties_hubspot_owner_assigneddate": null, "properties_hubspot_owner_id": null, "properties_hubspot_team_id": null, "properties_pet_name": "Integration Test Pet", "properties_pet_type": "Unknown"}, "emitted_at": 1697714266285} +{"stream": "cars", "data": {"id": "5938880072", "properties": {"car_id": 1, "car_name": 3232324, "hs_all_accessible_team_ids": null, "hs_all_assigned_business_unit_ids": null, "hs_all_owner_ids": null, "hs_all_team_ids": null, "hs_created_by_user_id": 12282590, "hs_createdate": "2023-04-12T17:57:15.836000+00:00", "hs_lastmodifieddate": "2023-04-12T17:59:20.189000+00:00", "hs_merged_object_ids": null, "hs_object_id": 5938880072, "hs_object_source": "CRM_UI", "hs_object_source_id": "userId:12282590", "hs_object_source_label": "CRM_UI", "hs_object_source_user_id": 12282590, "hs_pinned_engagement_id": null, "hs_read_only": null, "hs_unique_creation_key": null, "hs_updated_by_user_id": 12282590, "hs_user_ids_of_all_notification_followers": null, "hs_user_ids_of_all_notification_unfollowers": null, "hs_user_ids_of_all_owners": null, "hs_was_imported": null, "hubspot_owner_assigneddate": null, "hubspot_owner_id": null, "hubspot_team_id": null}, "createdAt": "2023-04-12T17:57:15.836Z", "updatedAt": "2023-04-12T17:59:20.189Z", "archived": false, "properties_car_id": 1, "properties_car_name": 3232324, "properties_hs_all_accessible_team_ids": null, "properties_hs_all_assigned_business_unit_ids": null, "properties_hs_all_owner_ids": null, "properties_hs_all_team_ids": null, "properties_hs_created_by_user_id": 12282590, "properties_hs_createdate": "2023-04-12T17:57:15.836000+00:00", "properties_hs_lastmodifieddate": "2023-04-12T17:59:20.189000+00:00", "properties_hs_merged_object_ids": null, "properties_hs_object_id": 5938880072, "properties_hs_object_source": "CRM_UI", "properties_hs_object_source_id": "userId:12282590", "properties_hs_object_source_label": "CRM_UI", "properties_hs_object_source_user_id": 12282590, "properties_hs_pinned_engagement_id": null, "properties_hs_read_only": null, "properties_hs_unique_creation_key": null, "properties_hs_updated_by_user_id": 12282590, "properties_hs_user_ids_of_all_notification_followers": null, "properties_hs_user_ids_of_all_notification_unfollowers": null, "properties_hs_user_ids_of_all_owners": null, "properties_hs_was_imported": null, "properties_hubspot_owner_assigneddate": null, "properties_hubspot_owner_id": null, "properties_hubspot_team_id": null}, "emitted_at": 1703882548289} +{"stream": "cars", "data": {"id": "5938880073", "properties": {"car_id": 2, "car_name": 23232, "hs_all_accessible_team_ids": null, "hs_all_assigned_business_unit_ids": null, "hs_all_owner_ids": null, "hs_all_team_ids": null, "hs_created_by_user_id": 12282590, "hs_createdate": "2023-04-12T17:57:20.583000+00:00", "hs_lastmodifieddate": "2023-04-12T17:59:20.189000+00:00", "hs_merged_object_ids": null, "hs_object_id": 5938880073, "hs_object_source": "CRM_UI", "hs_object_source_id": "userId:12282590", "hs_object_source_label": "CRM_UI", "hs_object_source_user_id": 12282590, "hs_pinned_engagement_id": null, "hs_read_only": null, "hs_unique_creation_key": null, "hs_updated_by_user_id": 12282590, "hs_user_ids_of_all_notification_followers": null, "hs_user_ids_of_all_notification_unfollowers": null, "hs_user_ids_of_all_owners": null, "hs_was_imported": null, "hubspot_owner_assigneddate": null, "hubspot_owner_id": null, "hubspot_team_id": null}, "createdAt": "2023-04-12T17:57:20.583Z", "updatedAt": "2023-04-12T17:59:20.189Z", "archived": false, "properties_car_id": 2, "properties_car_name": 23232, "properties_hs_all_accessible_team_ids": null, "properties_hs_all_assigned_business_unit_ids": null, "properties_hs_all_owner_ids": null, "properties_hs_all_team_ids": null, "properties_hs_created_by_user_id": 12282590, "properties_hs_createdate": "2023-04-12T17:57:20.583000+00:00", "properties_hs_lastmodifieddate": "2023-04-12T17:59:20.189000+00:00", "properties_hs_merged_object_ids": null, "properties_hs_object_id": 5938880073, "properties_hs_object_source": "CRM_UI", "properties_hs_object_source_id": "userId:12282590", "properties_hs_object_source_label": "CRM_UI", "properties_hs_object_source_user_id": 12282590, "properties_hs_pinned_engagement_id": null, "properties_hs_read_only": null, "properties_hs_unique_creation_key": null, "properties_hs_updated_by_user_id": 12282590, "properties_hs_user_ids_of_all_notification_followers": null, "properties_hs_user_ids_of_all_notification_unfollowers": null, "properties_hs_user_ids_of_all_owners": null, "properties_hs_was_imported": null, "properties_hubspot_owner_assigneddate": null, "properties_hubspot_owner_id": null, "properties_hubspot_team_id": null}, "emitted_at": 1703882548293} +{"stream": "pets", "data": {"id": "5936415312", "properties": {"hs_all_accessible_team_ids": null, "hs_all_assigned_business_unit_ids": null, "hs_all_owner_ids": null, "hs_all_team_ids": null, "hs_created_by_user_id": 12282590, "hs_createdate": "2023-04-12T17:08:50.632000+00:00", "hs_lastmodifieddate": "2023-04-12T17:08:50.632000+00:00", "hs_merged_object_ids": null, "hs_object_id": 5936415312, "hs_object_source": "CRM_UI", "hs_object_source_id": "userId:12282590", "hs_object_source_label": "CRM_UI", "hs_object_source_user_id": 12282590, "hs_pinned_engagement_id": null, "hs_read_only": null, "hs_unique_creation_key": null, "hs_updated_by_user_id": 12282590, "hs_user_ids_of_all_notification_followers": null, "hs_user_ids_of_all_notification_unfollowers": null, "hs_user_ids_of_all_owners": null, "hs_was_imported": null, "hubspot_owner_assigneddate": null, "hubspot_owner_id": null, "hubspot_team_id": null, "pet_name": "Marcos Pet", "pet_type": "Dog"}, "createdAt": "2023-04-12T17:08:50.632Z", "updatedAt": "2023-04-12T17:08:50.632Z", "archived": false, "properties_hs_all_accessible_team_ids": null, "properties_hs_all_assigned_business_unit_ids": null, "properties_hs_all_owner_ids": null, "properties_hs_all_team_ids": null, "properties_hs_created_by_user_id": 12282590, "properties_hs_createdate": "2023-04-12T17:08:50.632000+00:00", "properties_hs_lastmodifieddate": "2023-04-12T17:08:50.632000+00:00", "properties_hs_merged_object_ids": null, "properties_hs_object_id": 5936415312, "properties_hs_object_source": "CRM_UI", "properties_hs_object_source_id": "userId:12282590", "properties_hs_object_source_label": "CRM_UI", "properties_hs_object_source_user_id": 12282590, "properties_hs_pinned_engagement_id": null, "properties_hs_read_only": null, "properties_hs_unique_creation_key": null, "properties_hs_updated_by_user_id": 12282590, "properties_hs_user_ids_of_all_notification_followers": null, "properties_hs_user_ids_of_all_notification_unfollowers": null, "properties_hs_user_ids_of_all_owners": null, "properties_hs_was_imported": null, "properties_hubspot_owner_assigneddate": null, "properties_hubspot_owner_id": null, "properties_hubspot_team_id": null, "properties_pet_name": "Marcos Pet", "properties_pet_type": "Dog"}, "emitted_at": 1703886126793} +{"stream": "pets", "data": {"id": "5938880054", "properties": {"hs_all_accessible_team_ids": null, "hs_all_assigned_business_unit_ids": null, "hs_all_owner_ids": null, "hs_all_team_ids": null, "hs_created_by_user_id": 12282590, "hs_createdate": "2023-04-12T17:53:12.692000+00:00", "hs_lastmodifieddate": "2023-04-12T17:53:12.692000+00:00", "hs_merged_object_ids": null, "hs_object_id": 5938880054, "hs_object_source": "CRM_UI", "hs_object_source_id": "userId:12282590", "hs_object_source_label": "CRM_UI", "hs_object_source_user_id": 12282590, "hs_pinned_engagement_id": null, "hs_read_only": null, "hs_unique_creation_key": null, "hs_updated_by_user_id": 12282590, "hs_user_ids_of_all_notification_followers": null, "hs_user_ids_of_all_notification_unfollowers": null, "hs_user_ids_of_all_owners": null, "hs_was_imported": null, "hubspot_owner_assigneddate": null, "hubspot_owner_id": null, "hubspot_team_id": null, "pet_name": "Integration Test Pet", "pet_type": "Unknown"}, "createdAt": "2023-04-12T17:53:12.692Z", "updatedAt": "2023-04-12T17:53:12.692Z", "archived": false, "properties_hs_all_accessible_team_ids": null, "properties_hs_all_assigned_business_unit_ids": null, "properties_hs_all_owner_ids": null, "properties_hs_all_team_ids": null, "properties_hs_created_by_user_id": 12282590, "properties_hs_createdate": "2023-04-12T17:53:12.692000+00:00", "properties_hs_lastmodifieddate": "2023-04-12T17:53:12.692000+00:00", "properties_hs_merged_object_ids": null, "properties_hs_object_id": 5938880054, "properties_hs_object_source": "CRM_UI", "properties_hs_object_source_id": "userId:12282590", "properties_hs_object_source_label": "CRM_UI", "properties_hs_object_source_user_id": 12282590, "properties_hs_pinned_engagement_id": null, "properties_hs_read_only": null, "properties_hs_unique_creation_key": null, "properties_hs_updated_by_user_id": 12282590, "properties_hs_user_ids_of_all_notification_followers": null, "properties_hs_user_ids_of_all_notification_unfollowers": null, "properties_hs_user_ids_of_all_owners": null, "properties_hs_was_imported": null, "properties_hubspot_owner_assigneddate": null, "properties_hubspot_owner_id": null, "properties_hubspot_team_id": null, "properties_pet_name": "Integration Test Pet", "properties_pet_type": "Unknown"}, "emitted_at": 1703886126795} {"stream": "contacts_web_analytics", "data": {"objectType": "CONTACT", "objectId": "401", "eventType": "pe8727216_airbyte_contact_custom_event", "occurredAt": "2023-12-01T22:08:25.435Z", "id": "d287cdb7-3e8a-4f4d-92db-486e32f99ad4", "properties_hs_region": "officiis exercitationem modi adipisicing odit Hic", "properties_hs_campaign_id": "libero", "properties_hs_page_url": "Lorem", "properties_hs_element_id": "dolor sit", "properties_hs_browser": "architecto molestias, officiis exercitationem sit", "properties_hs_screen_width": "1531.0", "properties_hs_device_type": "sit adipisicing nobis officiis modi dolor sit", "properties_hs_link_href": "dolor magnam,", "properties_hs_element_class": "exercitationem modi nobis amet odit molestias,", "properties_hs_operating_system": "culpa! ipsum adipisicing consectetur nobis culpa!", "properties_hs_touchpoint_source": "libero modi odit ipsum Lorem accusantium culpa!", "properties_hs_utm_medium": "elit. ipsum officiis molestias, ipsum dolor quas"}, "emitted_at": 1701822848687} {"stream": "contacts_web_analytics", "data": {"objectType": "CONTACT", "objectId": "401", "eventType": "pe8727216_airbyte_contact_custom_event", "occurredAt": "2023-12-01T22:08:25.723Z", "id": "2f756b9a-a68d-4566-8e63-bc66b9149b41", "properties_hs_page_id": "modi sit", "properties_hs_city": "possimus modi culpa! veniam Lorem odit Lorem quas", "properties_hs_parent_module_id": "reprehenderit exercitationem dolor adipisicing", "properties_hs_user_agent": "possimus reprehenderit architecto odit ipsum, sit", "properties_hs_operating_version": "adipisicing", "properties_hs_element_id": "architecto exercitationem consectetur modi Lorem", "properties_hs_page_content_type": "amet", "properties_hs_screen_height": "4588.0", "properties_hs_operating_system": "reiciendis placeat possimus ipsum, adipisicing", "properties_hs_language": "adipisicing reprehenderit sit ipsum, amet nobis", "properties_hs_region": "placeat accusantium adipisicing culpa! modi quas", "properties_hs_utm_source": "molestias, reprehenderit reprehenderit", "properties_hs_referrer": "possimus consectetur odit sit Lorem nobis culpa!"}, "emitted_at": 1701822848688} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-hubspot/metadata.yaml b/airbyte-integrations/connectors/source-hubspot/metadata.yaml index 81bff30f8bde9..e68f2ffd9280e 100644 --- a/airbyte-integrations/connectors/source-hubspot/metadata.yaml +++ b/airbyte-integrations/connectors/source-hubspot/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: 36c891d9-4bd9-43ac-bad2-10e12756272c - dockerImageTag: 2.0.1 + dockerImageTag: 2.0.2 dockerRepository: airbyte/source-hubspot documentationUrl: https://docs.airbyte.com/integrations/sources/hubspot githubIssueLabel: source-hubspot diff --git a/airbyte-integrations/connectors/source-hubspot/source_hubspot/streams.py b/airbyte-integrations/connectors/source-hubspot/source_hubspot/streams.py index 7b0400acb8814..e388558f06825 100644 --- a/airbyte-integrations/connectors/source-hubspot/source_hubspot/streams.py +++ b/airbyte-integrations/connectors/source-hubspot/source_hubspot/streams.py @@ -1834,6 +1834,11 @@ def entity(self) -> str: def primary_key(self) -> str: """Indicates a field name which is considered to be a primary key of the stream""" + @property + @abstractmethod + def entity_primary_key(self) -> str: + """Indicates a field name which is considered to be a primary key of the parent entity""" + @property @abstractmethod def additional_keys(self) -> list: @@ -1873,7 +1878,7 @@ def request_params( def _transform(self, records: Iterable) -> Iterable: for record in records: properties = record.get("properties") - primary_key = record.get(self.primary_key) + primary_key = record.get(self.entity_primary_key) additional_keys = {additional_key: record.get(additional_key) for additional_key in self.additional_keys} value_dict: Dict for property_name, value_dict in properties.items(): @@ -1888,7 +1893,7 @@ def _transform(self, records: Iterable) -> Iterable: if versions: for version in versions: version["property"] = property_name - version[self.primary_key] = primary_key + version[self.entity_primary_key] = primary_key yield version | additional_keys @@ -1922,9 +1927,13 @@ def entity(self): return "contacts" @property - def primary_key(self) -> list: + def entity_primary_key(self) -> list: return "vid" + @property + def primary_key(self) -> list: + return ["vid", "property", "timestamp"] + @property def additional_keys(self) -> list: return ["portal-id", "is-contact", "canonical-vid"] @@ -1977,9 +1986,13 @@ def entity(self) -> str: return "companies" @property - def primary_key(self) -> list: + def entity_primary_key(self) -> list: return "companyId" + @property + def primary_key(self) -> list: + return ["companyId", "property", "timestamp"] + @property def additional_keys(self) -> list: return ["portalId", "isDeleted"] @@ -2045,9 +2058,13 @@ def entity(self) -> set: return "deals" @property - def primary_key(self) -> list: + def entity_primary_key(self) -> list: return "dealId" + @property + def primary_key(self) -> list: + return ["dealId", "property", "timestamp"] + @property def additional_keys(self) -> list: return ["portalId", "isDeleted"] diff --git a/airbyte-integrations/connectors/source-hubspot/unit_tests/test_source.py b/airbyte-integrations/connectors/source-hubspot/unit_tests/test_source.py index cd6e6fa60386f..6989843ff717d 100644 --- a/airbyte-integrations/connectors/source-hubspot/unit_tests/test_source.py +++ b/airbyte-integrations/connectors/source-hubspot/unit_tests/test_source.py @@ -143,7 +143,7 @@ def test_cast_datetime(common_params, caplog): "type": "LOG", "log": { "level": "WARN", - "message": f"Couldn't parse date/datetime string in {field_name}, trying to parse timestamp... Field value: {field_value}. Ex: argument of type 'DateTime' is not iterable", + "message": f"Couldn't parse date/datetime string in {field_name}, trying to parse timestamp... Field value: {field_value}. Ex: argument 'input': 'DateTime' object cannot be converted to 'PyString'", }, } assert expected_warining_message["log"]["message"] in caplog.text diff --git a/docs/integrations/sources/hubspot.md b/docs/integrations/sources/hubspot.md index efc0f0c781082..97d00e2a9b94d 100644 --- a/docs/integrations/sources/hubspot.md +++ b/docs/integrations/sources/hubspot.md @@ -304,6 +304,7 @@ The connector is restricted by normal HubSpot [rate limitations](https://legacyd | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 2.0.2 | 2023-12-15 | [33844](https://github.com/airbytehq/airbyte/pull/33844) | Make property_history PK combined to support Incremental/Deduped sync type | | 2.0.1 | 2023-12-15 | [33527](https://github.com/airbytehq/airbyte/pull/33527) | Make query string calculated correctly for ProertyHistory streams to avoid 414 HTTP Errors | | 2.0.0 | 2023-12-08 | [33266](https://github.com/airbytehq/airbyte/pull/33266) | Added ContactsPropertyHistory, CompaniesPropertyHistory, DealsPropertyHistory streams | | 1.9.0 | 2023-12-04 | [33042](https://github.com/airbytehq/airbyte/pull/33042) | Added Web Analytics streams | From c284588d99cd6447ed92f1dd560c2eaee9eb58cc Mon Sep 17 00:00:00 2001 From: Marius Posta Date: Mon, 8 Jan 2024 13:05:28 -0800 Subject: [PATCH 008/574] source-mssql: shorten capture job polling interval in tests (#33510) --- .../connectors/source-mssql/metadata.yaml | 2 +- .../source/mssql/MssqlCdcTargetPosition.java | 25 ++++++++++++++++--- .../mssql/CdcMssqlSourceAcceptanceTest.java | 1 + .../source/mssql/CdcMssqlSourceTest.java | 7 +++--- .../source/mssql/CdcStateCompressionTest.java | 14 +++++++++-- .../source/mssql/MsSQLTestDatabase.java | 16 ++++++++---- docs/integrations/sources/mssql.md | 1 + 7 files changed, 51 insertions(+), 15 deletions(-) diff --git a/airbyte-integrations/connectors/source-mssql/metadata.yaml b/airbyte-integrations/connectors/source-mssql/metadata.yaml index e82e4669dea05..050bb08d179ca 100644 --- a/airbyte-integrations/connectors/source-mssql/metadata.yaml +++ b/airbyte-integrations/connectors/source-mssql/metadata.yaml @@ -9,7 +9,7 @@ data: connectorSubtype: database connectorType: source definitionId: b5ea17b1-f170-46dc-bc31-cc744ca984c1 - dockerImageTag: 3.5.0 + dockerImageTag: 3.5.1 dockerRepository: airbyte/source-mssql documentationUrl: https://docs.airbyte.com/integrations/sources/mssql githubIssueLabel: source-mssql diff --git a/airbyte-integrations/connectors/source-mssql/src/main/java/io/airbyte/integrations/source/mssql/MssqlCdcTargetPosition.java b/airbyte-integrations/connectors/source-mssql/src/main/java/io/airbyte/integrations/source/mssql/MssqlCdcTargetPosition.java index dbe0e3ea7fc00..98645cef1d045 100644 --- a/airbyte-integrations/connectors/source-mssql/src/main/java/io/airbyte/integrations/source/mssql/MssqlCdcTargetPosition.java +++ b/airbyte-integrations/connectors/source-mssql/src/main/java/io/airbyte/integrations/source/mssql/MssqlCdcTargetPosition.java @@ -14,6 +14,7 @@ import io.debezium.connector.sqlserver.Lsn; import java.io.IOException; import java.sql.SQLException; +import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Optional; @@ -23,6 +24,9 @@ public class MssqlCdcTargetPosition implements CdcTargetPosition { private static final Logger LOGGER = LoggerFactory.getLogger(MssqlCdcTargetPosition.class); + + public static final Duration MAX_LSN_QUERY_DELAY = Duration.ZERO; + public static final Duration MAX_LSN_QUERY_DELAY_TEST = Duration.ofSeconds(1); public final Lsn targetLsn; public MssqlCdcTargetPosition(final Lsn targetLsn) { @@ -77,9 +81,22 @@ public int hashCode() { public static MssqlCdcTargetPosition getTargetPosition(final JdbcDatabase database, final String dbName) { try { - final List jsonNodes = database - .bufferedResultSetQuery(connection -> connection.createStatement().executeQuery( - "USE [" + dbName + "]; SELECT sys.fn_cdc_get_max_lsn() AS max_lsn;"), JdbcUtils.getDefaultSourceOperations()::rowToJson); + // We might have to wait a bit before querying the max_lsn to give the CDC capture job + // a chance to catch up. This is important in tests, where reads might occur in quick succession + // which might leave the CT tables (which Debezium consumes) in a stale state. + final JsonNode sourceConfig = database.getSourceConfig(); + final Duration delay = (sourceConfig != null && sourceConfig.has("is_test") && sourceConfig.get("is_test").asBoolean()) + ? MAX_LSN_QUERY_DELAY_TEST + : MAX_LSN_QUERY_DELAY; + final String maxLsnQuery = """ + USE [%s]; + WAITFOR DELAY '%02d:%02d:%02d'; + SELECT sys.fn_cdc_get_max_lsn() AS max_lsn; + """.formatted(dbName, delay.toHours(), delay.toMinutesPart(), delay.toSecondsPart()); + // Query the high-water mark. + final List jsonNodes = database.bufferedResultSetQuery( + connection -> connection.createStatement().executeQuery(maxLsnQuery), + JdbcUtils.getDefaultSourceOperations()::rowToJson); Preconditions.checkState(jsonNodes.size() == 1); if (jsonNodes.get(0).get("max_lsn") != null) { final Lsn maxLsn = Lsn.valueOf(jsonNodes.get(0).get("max_lsn").binaryValue()); @@ -87,7 +104,7 @@ public static MssqlCdcTargetPosition getTargetPosition(final JdbcDatabase databa return new MssqlCdcTargetPosition(maxLsn); } else { throw new RuntimeException("SQL returned max LSN as null, this might be because the SQL Server Agent is not running. " + - "Please enable the Agent and try again (https://docs.microsoft.com/en-us/sql/ssms/agent/start-stop-or-pause-the-sql-server-agent-service?view=sql-server-ver15)"); + "Please enable the Agent and try again (https://docs.microsoft.com/en-us/sql/ssms/agent/start-stop-or-pause-the-sql-server-agent-service)"); } } catch (final SQLException | IOException e) { throw new RuntimeException(e); diff --git a/airbyte-integrations/connectors/source-mssql/src/test-integration/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mssql/src/test-integration/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceAcceptanceTest.java index acaf4597a77b6..a9e5771d398af 100644 --- a/airbyte-integrations/connectors/source-mssql/src/test-integration/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceAcceptanceTest.java +++ b/airbyte-integrations/connectors/source-mssql/src/test-integration/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceAcceptanceTest.java @@ -118,6 +118,7 @@ protected void setupEnvironment(final TestDestinationEnv environment) { // enable cdc on tables for designated role .with(enableCdcSqlFmt, SCHEMA_NAME, STREAM_NAME, CDC_ROLE_NAME) .with(enableCdcSqlFmt, SCHEMA_NAME, STREAM_NAME2, CDC_ROLE_NAME) + .withShortenedCapturePollingInterval() .withWaitUntilMaxLsnAvailable() // revoke user permissions .with("REVOKE ALL FROM %s CASCADE;", testdb.getUserName()) diff --git a/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceTest.java b/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceTest.java index 445bc4ade004f..0c5ecc3438e50 100644 --- a/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceTest.java +++ b/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceTest.java @@ -98,8 +98,8 @@ protected MsSQLTestDatabase createTestDatabase() { .withConnectionProperty("databaseName", testdb.getDatabaseName()) .initialized() .withSnapshotIsolation() - .withCdc() - .withWaitUntilAgentRunning(); + .withWaitUntilAgentRunning() + .withCdc(); } @Override @@ -134,7 +134,8 @@ protected void setup() { \t@supports_net_changes = 0"""; testdb .with(enableCdcSqlFmt, modelsSchema(), MODELS_STREAM_NAME, CDC_ROLE_NAME) - .with(enableCdcSqlFmt, randomSchema(), RANDOM_TABLE_NAME, CDC_ROLE_NAME); + .with(enableCdcSqlFmt, randomSchema(), RANDOM_TABLE_NAME, CDC_ROLE_NAME) + .withShortenedCapturePollingInterval(); // Create a test user to be used by the source, with proper permissions. testdb diff --git a/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcStateCompressionTest.java b/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcStateCompressionTest.java index 55f022766d1ff..95ab8bc1f15a9 100644 --- a/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcStateCompressionTest.java +++ b/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcStateCompressionTest.java @@ -85,6 +85,7 @@ public void setup() { testdb .with("CREATE TABLE %s.test_table_%d (id INT IDENTITY(1,1) PRIMARY KEY);", TEST_SCHEMA, i) .with(enableCdcSqlFmt, TEST_SCHEMA, i, CDC_ROLE_NAME, i, 1) + .withShortenedCapturePollingInterval() .with("INSERT INTO %s.test_table_%d DEFAULT VALUES", TEST_SCHEMA, i); } @@ -122,7 +123,8 @@ public void setup() { testdb .with(sb.toString()) .with(enableCdcSqlFmt, TEST_SCHEMA, i, CDC_ROLE_NAME, i, 2) - .with(disableCdcSqlFmt, TEST_SCHEMA, i, i, 1); + .with(disableCdcSqlFmt, TEST_SCHEMA, i, i, 1) + .withShortenedCapturePollingInterval(); } } @@ -156,8 +158,16 @@ private JsonNode config() { .with(JdbcUtils.USERNAME_KEY, testUserName()) .with(JdbcUtils.PASSWORD_KEY, testdb.getPassword()) .withSchemas(TEST_SCHEMA) - .withCdcReplication() .withoutSsl() + // Configure for CDC replication but with a higher timeout than usual. + // This is because Debezium requires more time than usual to build the initial snapshot. + .with("is_test", true) + .with("replication_method", Map.of( + "method", "CDC", + "data_to_sync", "Existing and New", + "initial_waiting_seconds", 60, + "snapshot_isolation", "Snapshot")) + .build(); } diff --git a/airbyte-integrations/connectors/source-mssql/src/testFixtures/java/io/airbyte/integrations/source/mssql/MsSQLTestDatabase.java b/airbyte-integrations/connectors/source-mssql/src/testFixtures/java/io/airbyte/integrations/source/mssql/MsSQLTestDatabase.java index 3b79d8d496d76..aae8f6333788a 100644 --- a/airbyte-integrations/connectors/source-mssql/src/testFixtures/java/io/airbyte/integrations/source/mssql/MsSQLTestDatabase.java +++ b/airbyte-integrations/connectors/source-mssql/src/testFixtures/java/io/airbyte/integrations/source/mssql/MsSQLTestDatabase.java @@ -103,6 +103,11 @@ public MsSQLTestDatabase withWaitUntilAgentStopped() { return self(); } + public MsSQLTestDatabase withShortenedCapturePollingInterval() { + return with("EXEC sys.sp_cdc_change_job @job_type = 'capture', @pollinginterval = %d;", + MssqlCdcTargetPosition.MAX_LSN_QUERY_DELAY_TEST.toSeconds()); + } + private void waitForAgentState(final boolean running) { final String expectedValue = running ? "Running." : "Stopped."; LOGGER.debug("Waiting for SQLServerAgent state to change to '{}'.", expectedValue); @@ -258,11 +263,12 @@ protected MsSQLConfigBuilder(MsSQLTestDatabase testDatabase) { } public MsSQLConfigBuilder withCdcReplication() { - return with("replication_method", Map.of( - "method", "CDC", - "data_to_sync", "Existing and New", - "initial_waiting_seconds", DEFAULT_CDC_REPLICATION_INITIAL_WAIT.getSeconds(), - "snapshot_isolation", "Snapshot")); + return with("is_test", true) + .with("replication_method", Map.of( + "method", "CDC", + "data_to_sync", "Existing and New", + "initial_waiting_seconds", DEFAULT_CDC_REPLICATION_INITIAL_WAIT.getSeconds(), + "snapshot_isolation", "Snapshot")); } public MsSQLConfigBuilder withSchemas(String... schemas) { diff --git a/docs/integrations/sources/mssql.md b/docs/integrations/sources/mssql.md index 50b686239e240..21ca86e71094e 100644 --- a/docs/integrations/sources/mssql.md +++ b/docs/integrations/sources/mssql.md @@ -342,6 +342,7 @@ WHERE actor_definition_id ='b5ea17b1-f170-46dc-bc31-cc744ca984c1' AND (configura | Version | Date | Pull Request | Subject | |:--------|:-----------|:------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------| +| 3.5.1 | 2024-01-05 | [33510](https://github.com/airbytehq/airbyte/pull/33510) | Test-only changes. | | 3.5.0 | 2023-12-19 | [33071](https://github.com/airbytehq/airbyte/pull/33071) | Fix SSL configuration parameters | | 3.4.1 | 2024-01-02 | [33755](https://github.com/airbytehq/airbyte/pull/33755) | Encode binary to base64 format | | 3.4.0 | 2023-12-19 | [33481](https://github.com/airbytehq/airbyte/pull/33481) | Remove LEGACY state flag | From 0e4ffb561a9520be3fa4ea68e84324e59b9e9b3e Mon Sep 17 00:00:00 2001 From: Gireesh Sreepathi Date: Mon, 8 Jan 2024 15:13:30 -0800 Subject: [PATCH 009/574] Destination BigQuery: add row ids for dummy inserts in check (#34021) Signed-off-by: Gireesh Sreepathi --- .../connectors/destination-bigquery/metadata.yaml | 2 +- .../destination/bigquery/BigQueryUtils.java | 10 +++++++--- docs/integrations/destinations/bigquery.md | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/airbyte-integrations/connectors/destination-bigquery/metadata.yaml b/airbyte-integrations/connectors/destination-bigquery/metadata.yaml index 165f71b8ff3e2..27e56ad79b3d5 100644 --- a/airbyte-integrations/connectors/destination-bigquery/metadata.yaml +++ b/airbyte-integrations/connectors/destination-bigquery/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: database connectorType: destination definitionId: 22f6c74f-5699-40ff-833c-4a879ea40133 - dockerImageTag: 2.3.27 + dockerImageTag: 2.3.28 dockerRepository: airbyte/destination-bigquery documentationUrl: https://docs.airbyte.com/integrations/destinations/bigquery githubIssueLabel: destination-bigquery diff --git a/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryUtils.java b/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryUtils.java index e84e91e1c19ab..255b685190b28 100644 --- a/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryUtils.java +++ b/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryUtils.java @@ -19,6 +19,7 @@ import com.google.cloud.bigquery.Field; import com.google.cloud.bigquery.FieldList; import com.google.cloud.bigquery.InsertAllRequest; +import com.google.cloud.bigquery.InsertAllRequest.RowToInsert; import com.google.cloud.bigquery.InsertAllResponse; import com.google.cloud.bigquery.Job; import com.google.cloud.bigquery.JobId; @@ -159,13 +160,15 @@ private static void attemptCreateTableAndTestInsert(final BigQuery bigquery, fin CHECK_TEST_TMP_TABLE_NAME, testTableSchema); // Try to make test (dummy records) insert to make sure that user has required permissions + // Use ids for BigQuery client to attempt idempotent retries. + // See https://github.com/airbytehq/airbyte/issues/33982 try { final InsertAllResponse response = bigquery.insertAll(InsertAllRequest .newBuilder(test_connection_table_name) - .addRow(Map.of("id", 1, "name", "James")) - .addRow(Map.of("id", 2, "name", "Eugene")) - .addRow(Map.of("id", 3, "name", "Angelina")) + .addRow(RowToInsert.of("1", ImmutableMap.of("id", 1, "name", "James"))) + .addRow(RowToInsert.of("2", ImmutableMap.of("id", 2, "name", "Eugene"))) + .addRow(RowToInsert.of("3", ImmutableMap.of("id", 3, "name", "Angelina"))) .build()); if (response.hasErrors()) { @@ -175,6 +178,7 @@ private static void attemptCreateTableAndTestInsert(final BigQuery bigquery, fin } } } catch (final BigQueryException e) { + LOGGER.error("Dummy inserts in check failed", e); throw new ConfigErrorException("Failed to check connection: \n" + e.getMessage()); } finally { test_connection_table_name.delete(); diff --git a/docs/integrations/destinations/bigquery.md b/docs/integrations/destinations/bigquery.md index c73b50c6cc583..55d902896ff1a 100644 --- a/docs/integrations/destinations/bigquery.md +++ b/docs/integrations/destinations/bigquery.md @@ -142,6 +142,7 @@ Now that you have set up the BigQuery destination connector, check out the follo | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 2.3.28 | 2024-01-08 | [34021](https://github.com/airbytehq/airbyte/pull/34021) | Add idempotency ids in dummy insert for check call | | 2.3.27 | 2024-01-05 | [\#33948](https://github.com/airbytehq/airbyte/pull/33948) | Skip retrieving initial table state when setup fails | | 2.3.26 | 2024-01-04 | [\#33730](https://github.com/airbytehq/airbyte/pull/33730) | Internal code structure changes | | 2.3.25 | 2023-12-20 | [\#33704](https://github.com/airbytehq/airbyte/pull/33704) | Update to java CDK 0.10.0 (no changes) | @@ -301,4 +302,4 @@ Now that you have set up the BigQuery destination connector, check out the follo | 0.3.10 | 2021-07-28 | [\#3549](https://github.com/airbytehq/airbyte/issues/3549) | Add extended logs and made JobId filled with region and projectId | | 0.3.9 | 2021-07-28 | [\#5026](https://github.com/airbytehq/airbyte/pull/5026) | Add sanitized json fields in raw tables to handle quotes in column names | | 0.3.6 | 2021-06-18 | [\#3947](https://github.com/airbytehq/airbyte/issues/3947) | Service account credentials are now optional. | -| 0.3.4 | 2021-06-07 | [\#3277](https://github.com/airbytehq/airbyte/issues/3277) | Add dataset location option | +| 0.3.4 | 2021-06-07 | [\#3277](https://github.com/airbytehq/airbyte/issues/3277) | Add dataset location option | \ No newline at end of file From 1737ab13db115dd7e8afe7ccc19b1741aee4828d Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Mon, 8 Jan 2024 15:26:43 -0800 Subject: [PATCH 010/574] Update typing-deduping.md - Loading Data Incrementally to Final Tables (#34034) --- .../core-concepts/typing-deduping.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/using-airbyte/core-concepts/typing-deduping.md b/docs/using-airbyte/core-concepts/typing-deduping.md index 3905d16469752..d287fd5d75bfa 100644 --- a/docs/using-airbyte/core-concepts/typing-deduping.md +++ b/docs/using-airbyte/core-concepts/typing-deduping.md @@ -15,12 +15,6 @@ This page refers to new functionality added by [Destinations V2](/release_notes/ - Internal Airbyte tables in the `airbyte_internal` schema: Airbyte will now generate all raw tables in the `airbyte_internal` schema. We no longer clutter your desired schema with raw data tables. - Incremental delivery for large syncs: Data will be incrementally delivered to your final tables when possible. No more waiting hours to see the first rows in your destination table. -:::note - -Typing and Deduping may cause an increase in your destination's compute cost. This cost will vary depending on the amount of data that is transformed and is not related to Airbyte credit usage. - -::: - ## `_airbyte_meta` Errors "Per-row error handling" is a new paradigm for Airbyte which provides greater flexibility for our users. Airbyte now separates `data-moving problems` from `data-content problems`. Prior to Destinations V2, both types of errors were handled the same way: by failing the sync. Now, a failing sync means that Airbyte could not _move_ all of your data. You can query the `_airbyte_meta` column to see which rows failed for _content_ reasons, and why. This is a more flexible approach, as you can now decide how to handle rows with errors on a case-by-case basis. @@ -81,3 +75,14 @@ In legacy normalization, columns of [Airbyte type](/understanding-airbyte/suppor You also now see the following changes in Airbyte-provided columns: ![Airbyte Destinations V2 Column Changes](../../release_notes/assets/updated_table_columns.png) + +## Loading Data Incrementally to Final Tables + +:::note + +Typing and Deduping may cause an increase in your destination's compute cost. This cost will vary depending on the amount of data that is transformed and is not related to Airbyte credit usage. Enabling loading data incrementally to final tables may further increase this cost. + +::: + +V2 destinations may include the option "Enable Loading Data Incrementally to Final Tables". When enabled your data will load into your final tables incrementally while your data is still being synced. When Disabled (the default), your data loads into your final tables once at the end of a sync. Note that this option only applies if you elect to create Final tables. + From c8ca4b13ffdca5b7d1a6059ad049f6fda72eda44 Mon Sep 17 00:00:00 2001 From: Alexandre Girard Date: Mon, 8 Jan 2024 17:40:48 -0800 Subject: [PATCH 011/574] :bug: fix declarative oauth initialization (#32967) Co-authored-by: girarda --- .../sources/declarative/auth/oauth.py | 72 +++++++++++-------- .../requests_native_auth/abstract_oauth.py | 45 ++++++------ .../sources/declarative/auth/test_oauth.py | 25 ++++++- .../test_model_to_component_factory.py | 14 ++-- 4 files changed, 97 insertions(+), 59 deletions(-) diff --git a/airbyte-cdk/python/airbyte_cdk/sources/declarative/auth/oauth.py b/airbyte-cdk/python/airbyte_cdk/sources/declarative/auth/oauth.py index 4e83c570be6e6..d858677b63249 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/declarative/auth/oauth.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/declarative/auth/oauth.py @@ -46,8 +46,8 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut refresh_token: Optional[Union[InterpolatedString, str]] = None scopes: Optional[List[str]] = None token_expiry_date: Optional[Union[InterpolatedString, str]] = None - _token_expiry_date: pendulum.DateTime = field(init=False, repr=False, default=None) - token_expiry_date_format: str = None + _token_expiry_date: Optional[pendulum.DateTime] = field(init=False, repr=False, default=None) + token_expiry_date_format: Optional[str] = None token_expiry_is_time_of_expiration: bool = False access_token_name: Union[InterpolatedString, str] = "access_token" expires_in_name: Union[InterpolatedString, str] = "expires_in" @@ -55,65 +55,79 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut grant_type: Union[InterpolatedString, str] = "refresh_token" message_repository: MessageRepository = NoopMessageRepository() - def __post_init__(self, parameters: Mapping[str, Any]): - self.token_refresh_endpoint = InterpolatedString.create(self.token_refresh_endpoint, parameters=parameters) - self.client_id = InterpolatedString.create(self.client_id, parameters=parameters) - self.client_secret = InterpolatedString.create(self.client_secret, parameters=parameters) + def __post_init__(self, parameters: Mapping[str, Any]) -> None: + super().__init__() + self._token_refresh_endpoint = InterpolatedString.create(self.token_refresh_endpoint, parameters=parameters) + self._client_id = InterpolatedString.create(self.client_id, parameters=parameters) + self._client_secret = InterpolatedString.create(self.client_secret, parameters=parameters) if self.refresh_token is not None: - self.refresh_token = InterpolatedString.create(self.refresh_token, parameters=parameters) + self._refresh_token = InterpolatedString.create(self.refresh_token, parameters=parameters) + else: + self._refresh_token = None self.access_token_name = InterpolatedString.create(self.access_token_name, parameters=parameters) self.expires_in_name = InterpolatedString.create(self.expires_in_name, parameters=parameters) self.grant_type = InterpolatedString.create(self.grant_type, parameters=parameters) self._refresh_request_body = InterpolatedMapping(self.refresh_request_body or {}, parameters=parameters) - self._token_expiry_date = ( - pendulum.parse(InterpolatedString.create(self.token_expiry_date, parameters=parameters).eval(self.config)) + self._token_expiry_date: pendulum.DateTime = ( + pendulum.parse(InterpolatedString.create(self.token_expiry_date, parameters=parameters).eval(self.config)) # type: ignore # pendulum.parse returns a datetime in this context if self.token_expiry_date - else pendulum.now().subtract(days=1) + else pendulum.now().subtract(days=1) # type: ignore # substract does not have type hints ) - self._access_token = None + self._access_token: Optional[str] = None # access_token is initialized by a setter - if self.get_grant_type() == "refresh_token" and self.refresh_token is None: + if self.get_grant_type() == "refresh_token" and self._refresh_token is None: raise ValueError("OAuthAuthenticator needs a refresh_token parameter if grant_type is set to `refresh_token`") def get_token_refresh_endpoint(self) -> str: - return self.token_refresh_endpoint.eval(self.config) + refresh_token: str = self._token_refresh_endpoint.eval(self.config) + if not refresh_token: + raise ValueError("OAuthAuthenticator was unable to evaluate token_refresh_endpoint parameter") + return refresh_token def get_client_id(self) -> str: - return self.client_id.eval(self.config) + client_id: str = self._client_id.eval(self.config) + if not client_id: + raise ValueError("OAuthAuthenticator was unable to evaluate client_id parameter") + return client_id def get_client_secret(self) -> str: - return self.client_secret.eval(self.config) + client_secret: str = self._client_secret.eval(self.config) + if not client_secret: + raise ValueError("OAuthAuthenticator was unable to evaluate client_secret parameter") + return client_secret def get_refresh_token(self) -> Optional[str]: - return None if self.refresh_token is None else self.refresh_token.eval(self.config) + return None if self._refresh_token is None else self._refresh_token.eval(self.config) - def get_scopes(self) -> [str]: - return self.scopes + def get_scopes(self) -> List[str]: + return self.scopes or [] - def get_access_token_name(self) -> InterpolatedString: - return self.access_token_name.eval(self.config) + def get_access_token_name(self) -> str: + return self.access_token_name.eval(self.config) # type: ignore # eval returns a string in this context - def get_expires_in_name(self) -> InterpolatedString: - return self.expires_in_name.eval(self.config) + def get_expires_in_name(self) -> str: + return self.expires_in_name.eval(self.config) # type: ignore # eval returns a string in this context - def get_grant_type(self) -> InterpolatedString: - return self.grant_type.eval(self.config) + def get_grant_type(self) -> str: + return self.grant_type.eval(self.config) # type: ignore # eval returns a string in this context def get_refresh_request_body(self) -> Mapping[str, Any]: - return self._refresh_request_body.eval(self.config) + return self._refresh_request_body.eval(self.config) # type: ignore # eval should return a Mapping in this context def get_token_expiry_date(self) -> pendulum.DateTime: - return self._token_expiry_date + return self._token_expiry_date # type: ignore # _token_expiry_date is a pendulum.DateTime. It is never None despite what mypy thinks - def set_token_expiry_date(self, value: Union[str, int]): + def set_token_expiry_date(self, value: Union[str, int]) -> None: self._token_expiry_date = self._parse_token_expiration_date(value) @property def access_token(self) -> str: + if self._access_token is None: + raise ValueError("access_token is not set") return self._access_token @access_token.setter - def access_token(self, value: str): + def access_token(self, value: str) -> None: self._access_token = value @property @@ -130,5 +144,5 @@ class DeclarativeSingleUseRefreshTokenOauth2Authenticator(SingleUseRefreshTokenO Declarative version of SingleUseRefreshTokenOauth2Authenticator which can be used in declarative connectors. """ - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) diff --git a/airbyte-cdk/python/airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py b/airbyte-cdk/python/airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py index 22e2caa6a2e85..0dd450413dd48 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py @@ -45,7 +45,7 @@ def __init__( self._refresh_token_error_key = refresh_token_error_key self._refresh_token_error_values = refresh_token_error_values - def __call__(self, request: requests.Request) -> requests.Request: + def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest: """Attach the HTTP headers required to authenticate on the HTTP request""" request.headers.update(self.get_auth_header()) return request @@ -65,7 +65,7 @@ def get_access_token(self) -> str: def token_has_expired(self) -> bool: """Returns True if the token is expired""" - return pendulum.now() > self.get_token_expiry_date() + return pendulum.now() > self.get_token_expiry_date() # type: ignore # this is always a bool despite what mypy thinks def build_refresh_request_body(self) -> Mapping[str, Any]: """ @@ -80,7 +80,7 @@ def build_refresh_request_body(self) -> Mapping[str, Any]: "refresh_token": self.get_refresh_token(), } - if self.get_scopes: + if self.get_scopes(): payload["scopes"] = self.get_scopes() if self.get_refresh_request_body(): @@ -93,7 +93,10 @@ def build_refresh_request_body(self) -> Mapping[str, Any]: def _wrap_refresh_token_exception(self, exception: requests.exceptions.RequestException) -> bool: try: - exception_content = exception.response.json() + if exception.response is not None: + exception_content = exception.response.json() + else: + return False except JSONDecodeError: return False return ( @@ -109,15 +112,16 @@ def _wrap_refresh_token_exception(self, exception: requests.exceptions.RequestEx ), max_time=300, ) - def _get_refresh_access_token_response(self): + def _get_refresh_access_token_response(self) -> Any: try: response = requests.request(method="POST", url=self.get_token_refresh_endpoint(), data=self.build_refresh_request_body()) self._log_response(response) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: - if e.response.status_code == 429 or e.response.status_code >= 500: - raise DefaultBackoffException(request=e.response.request, response=e.response) + if e.response is not None: + if e.response.status_code == 429 or e.response.status_code >= 500: + raise DefaultBackoffException(request=e.response.request, response=e.response) if self._wrap_refresh_token_exception(e): message = "Refresh token is invalid or expired. Please re-authenticate from Sources//Settings." raise AirbyteTracedException(internal_message=message, message=message, failure_type=FailureType.config_error) @@ -147,7 +151,7 @@ def _parse_token_expiration_date(self, value: Union[str, int]) -> pendulum.DateT raise ValueError( f"Invalid token expiry date format {self.token_expiry_date_format}; a string representing the format is required." ) - return pendulum.from_format(value, self.token_expiry_date_format) + return pendulum.from_format(str(value), self.token_expiry_date_format) else: return pendulum.now().add(seconds=int(float(value))) @@ -192,7 +196,7 @@ def get_token_expiry_date(self) -> pendulum.DateTime: """Expiration date of the access token""" @abstractmethod - def set_token_expiry_date(self, value: Union[str, int]): + def set_token_expiry_date(self, value: Union[str, int]) -> None: """Setter for access token expiration date""" @abstractmethod @@ -228,14 +232,15 @@ def _message_repository(self) -> Optional[MessageRepository]: """ return _NOOP_MESSAGE_REPOSITORY - def _log_response(self, response: requests.Response): - self._message_repository.log_message( - Level.DEBUG, - lambda: format_http_message( - response, - "Refresh token", - "Obtains access token", - self._NO_STREAM_NAME, - is_auxiliary=True, - ), - ) + def _log_response(self, response: requests.Response) -> None: + if self._message_repository: + self._message_repository.log_message( + Level.DEBUG, + lambda: format_http_message( + response, + "Refresh token", + "Obtains access token", + self._NO_STREAM_NAME, + is_auxiliary=True, + ), + ) diff --git a/airbyte-cdk/python/unit_tests/sources/declarative/auth/test_oauth.py b/airbyte-cdk/python/unit_tests/sources/declarative/auth/test_oauth.py index 0992d0e331bce..bd019d374987c 100644 --- a/airbyte-cdk/python/unit_tests/sources/declarative/auth/test_oauth.py +++ b/airbyte-cdk/python/unit_tests/sources/declarative/auth/test_oauth.py @@ -81,7 +81,6 @@ def test_refresh_with_encode_config_params(self): "client_id": base64.b64encode(config["client_id"].encode("utf-8")).decode(), "client_secret": base64.b64encode(config["client_secret"].encode("utf-8")).decode(), "refresh_token": None, - "scopes": None, } assert body == expected @@ -104,7 +103,6 @@ def test_refresh_with_decode_config_params(self): "client_id": "some_client_id", "client_secret": "some_client_secret", "refresh_token": None, - "scopes": None, } assert body == expected @@ -126,7 +124,6 @@ def test_refresh_without_refresh_token(self): "client_id": "some_client_id", "client_secret": "some_client_secret", "refresh_token": None, - "scopes": None, } assert body == expected @@ -278,6 +275,28 @@ def test_set_token_expiry_date_no_format(self, mocker, expires_in_response, next assert "access_token" == token assert oauth.get_token_expiry_date() == pendulum.parse(next_day) + def test_error_handling(self, mocker): + oauth = DeclarativeOauth2Authenticator( + token_refresh_endpoint="{{ config['refresh_endpoint'] }}", + client_id="{{ config['client_id'] }}", + client_secret="{{ config['client_secret'] }}", + refresh_token="{{ config['refresh_token'] }}", + config=config, + scopes=["scope1", "scope2"], + refresh_request_body={ + "custom_field": "{{ config['custom_field'] }}", + "another_field": "{{ config['another_field'] }}", + "scopes": ["no_override"], + }, + parameters={}, + ) + resp.status_code = 400 + mocker.patch.object(resp, "json", return_value={"access_token": "access_token", "expires_in": 123}) + mocker.patch.object(requests, "request", side_effect=mock_request, autospec=True) + with pytest.raises(requests.exceptions.HTTPError) as e: + oauth.refresh_access_token() + assert e.value.errno == 400 + def mock_request(method, url, data): if url == "refresh_end": diff --git a/airbyte-cdk/python/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py b/airbyte-cdk/python/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py index 15f774879c600..08cea962086ea 100644 --- a/airbyte-cdk/python/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py +++ b/airbyte-cdk/python/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py @@ -303,10 +303,10 @@ def test_interpolate_config(): ) assert isinstance(authenticator, DeclarativeOauth2Authenticator) - assert authenticator.client_id.eval(input_config) == "some_client_id" - assert authenticator.client_secret.string == "some_client_secret" - assert authenticator.token_refresh_endpoint.eval(input_config) == "https://api.sendgrid.com/v3/auth" - assert authenticator.refresh_token.eval(input_config) == "verysecrettoken" + assert authenticator._client_id.eval(input_config) == "some_client_id" + assert authenticator._client_secret.string == "some_client_secret" + assert authenticator._token_refresh_endpoint.eval(input_config) == "https://api.sendgrid.com/v3/auth" + assert authenticator._refresh_token.eval(input_config) == "verysecrettoken" assert authenticator._refresh_request_body.mapping == {"body_field": "yoyoyo", "interpolated_body_field": "{{ config['apikey'] }}"} assert authenticator.get_refresh_request_body() == {"body_field": "yoyoyo", "interpolated_body_field": "verysecrettoken"} @@ -332,9 +332,9 @@ def test_interpolate_config_with_token_expiry_date_format(): assert isinstance(authenticator, DeclarativeOauth2Authenticator) assert authenticator.token_expiry_date_format == "%Y-%m-%d %H:%M:%S.%f+00:00" assert authenticator.token_expiry_is_time_of_expiration - assert authenticator.client_id.eval(input_config) == "some_client_id" - assert authenticator.client_secret.string == "some_client_secret" - assert authenticator.token_refresh_endpoint.eval(input_config) == "https://api.sendgrid.com/v3/auth" + assert authenticator._client_id.eval(input_config) == "some_client_id" + assert authenticator._client_secret.string == "some_client_secret" + assert authenticator._token_refresh_endpoint.eval(input_config) == "https://api.sendgrid.com/v3/auth" def test_single_use_oauth_branch(): From 62d3b56a60febf4cfc92d73cc9b9fab7629de94c Mon Sep 17 00:00:00 2001 From: Alexandre Girard Date: Mon, 8 Jan 2024 17:40:58 -0800 Subject: [PATCH 012/574] Docs: fixing wrong jinja example and stating how airbyte decides last date cursor (#34000) Co-authored-by: Segers --- .../connector-builder-ui/incremental-sync.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/connector-development/connector-builder-ui/incremental-sync.md b/docs/connector-development/connector-builder-ui/incremental-sync.md index 0a4db2bc7a545..4b05f8d48ba9b 100644 --- a/docs/connector-development/connector-builder-ui/incremental-sync.md +++ b/docs/connector-development/connector-builder-ui/incremental-sync.md @@ -82,10 +82,14 @@ Then when a sync is triggered for the same connection the next day, the followin curl 'https://content.guardianapis.com/search?from-date=2023-04-15T07:30:58Z&to-date={``}' +:::info +If the last record read has a datetime earlier than the end time of the stream interval, the end time of the interval will be stored in the state. +::: + The `from-date` is set to the cutoff date of articles synced already and the `to-date` is set to the current date. :::info -In some cases, it's helpful to reference the start and end date of the interval that's currently synced, for example if it needs to be injected into the URL path of the current stream. In these cases it can be referenced using the `{{ stream_interval.start_date }}` and `{{ stream_interval.end_date }}` [placeholders](/connector-development/config-based/understanding-the-yaml-file/reference#variables). Check out [the tutorial](./tutorial.mdx#adding-incremental-reads) for such a case. +In some cases, it's helpful to reference the start and end date of the interval that's currently synced, for example if it needs to be injected into the URL path of the current stream. In these cases it can be referenced using the `{{ stream_interval.start_time }}` and `{{ stream_interval.end_time }}` [placeholders](/connector-development/config-based/understanding-the-yaml-file/reference#variables). Check out [the tutorial](./tutorial.mdx#adding-incremental-reads) for such a case. ::: ## Incremental sync without time filtering From ce270c62884ace085206c8e63b61bf29184b005b Mon Sep 17 00:00:00 2001 From: girarda Date: Tue, 9 Jan 2024 02:06:26 +0000 Subject: [PATCH 013/574] =?UTF-8?q?=F0=9F=A4=96=20Bump=20patch=20version?= =?UTF-8?q?=20of=20Python=20CDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airbyte-cdk/python/.bumpversion.cfg | 2 +- airbyte-cdk/python/CHANGELOG.md | 3 +++ airbyte-cdk/python/Dockerfile | 4 ++-- airbyte-cdk/python/setup.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/airbyte-cdk/python/.bumpversion.cfg b/airbyte-cdk/python/.bumpversion.cfg index c53cdfefe230e..1ff825346ad44 100644 --- a/airbyte-cdk/python/.bumpversion.cfg +++ b/airbyte-cdk/python/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.58.2 +current_version = 0.58.3 commit = False [bumpversion:file:setup.py] diff --git a/airbyte-cdk/python/CHANGELOG.md b/airbyte-cdk/python/CHANGELOG.md index d060b0faf9853..a4fbf2da64af6 100644 --- a/airbyte-cdk/python/CHANGELOG.md +++ b/airbyte-cdk/python/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 0.58.3 +fix declarative oauth initialization + ## 0.58.2 Integration tests: adding debug mode to improve logging diff --git a/airbyte-cdk/python/Dockerfile b/airbyte-cdk/python/Dockerfile index 16766bcbc1b96..313adf12dfbce 100644 --- a/airbyte-cdk/python/Dockerfile +++ b/airbyte-cdk/python/Dockerfile @@ -10,7 +10,7 @@ RUN apk --no-cache upgrade \ && apk --no-cache add tzdata build-base # install airbyte-cdk -RUN pip install --prefix=/install airbyte-cdk==0.58.2 +RUN pip install --prefix=/install airbyte-cdk==0.58.3 # build a clean environment FROM base @@ -32,5 +32,5 @@ ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] # needs to be the same as CDK -LABEL io.airbyte.version=0.58.2 +LABEL io.airbyte.version=0.58.3 LABEL io.airbyte.name=airbyte/source-declarative-manifest diff --git a/airbyte-cdk/python/setup.py b/airbyte-cdk/python/setup.py index a5cac26e35fd0..5d1cb1cadedff 100644 --- a/airbyte-cdk/python/setup.py +++ b/airbyte-cdk/python/setup.py @@ -36,7 +36,7 @@ name="airbyte-cdk", # The version of the airbyte-cdk package is used at runtime to validate manifests. That validation must be # updated if our semver format changes such as using release candidate versions. - version="0.58.2", + version="0.58.3", description="A framework for writing Airbyte Connectors.", long_description=README, long_description_content_type="text/markdown", From 59e3e193e03a52c399745a2e1c98d57e88f52323 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 9 Jan 2024 11:16:02 +0100 Subject: [PATCH 014/574] airbyte-lib: Improve error message on missing config (#33968) --- airbyte-lib/airbyte_lib/source.py | 16 +++++++++++----- .../tests/integration_tests/test_integration.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/airbyte-lib/airbyte_lib/source.py b/airbyte-lib/airbyte_lib/source.py index 3a496285fed12..96528a760d3a9 100644 --- a/airbyte-lib/airbyte_lib/source.py +++ b/airbyte-lib/airbyte_lib/source.py @@ -55,7 +55,7 @@ def __init__( self.executor = executor self.name = name self.streams: Optional[List[str]] = None - self.config: Optional[Dict[str, Any]] = None + self._config_dict: Optional[Dict[str, Any]] = None self._last_log_messages: List[str] = [] if config is not None: self.set_config(config) @@ -71,7 +71,13 @@ def set_streams(self, streams: List[str]): def set_config(self, config: Dict[str, Any]): self._validate_config(config) - self.config = config + self._config_dict = config + + @property + def _config(self) -> Dict[str, Any]: + if self._config_dict is None: + raise Exception("Config is not set, either set in get_connector or via source.set_config") + return self._config_dict def _discover(self) -> AirbyteCatalog: """ @@ -83,7 +89,7 @@ def _discover(self) -> AirbyteCatalog: * Listen to the messages and return the first AirbyteCatalog that comes along. * Make sure the subprocess is killed when the function returns. """ - with as_temp_files([self.config]) as [config_file]: + with as_temp_files([self._config]) as [config_file]: for msg in self._execute(["discover", "--config", config_file]): if msg.type == Type.CATALOG and msg.catalog: return msg.catalog @@ -156,7 +162,7 @@ def check(self): * Listen to the messages and return the first AirbyteCatalog that comes along. * Make sure the subprocess is killed when the function returns. """ - with as_temp_files([self.config]) as [config_file]: + with as_temp_files([self._config]) as [config_file]: for msg in self._execute(["check", "--config", config_file]): if msg.type == Type.CONNECTION_STATUS and msg.connectionStatus: if msg.connectionStatus.status == Status.FAILED: @@ -202,7 +208,7 @@ def _read_catalog(self, catalog: ConfiguredAirbyteCatalog) -> Iterable[AirbyteRe * execute the connector with read --config --catalog * Listen to the messages and return the AirbyteRecordMessages that come along. """ - with as_temp_files([self.config, catalog.json()]) as [ + with as_temp_files([self._config, catalog.json()]) as [ config_file, catalog_file, ]: diff --git a/airbyte-lib/tests/integration_tests/test_integration.py b/airbyte-lib/tests/integration_tests/test_integration.py index 2032f747452f5..863d0b1b90a60 100644 --- a/airbyte-lib/tests/integration_tests/test_integration.py +++ b/airbyte-lib/tests/integration_tests/test_integration.py @@ -58,6 +58,21 @@ def test_check_fail(): source.check() +@pytest.mark.parametrize( + "method_call", + [ + pytest.param(lambda source: source.check(), id="check"), + pytest.param(lambda source: list(source.read_stream("stream1")), id="read_stream"), + pytest.param(lambda source: source.read_all(), id="read_all"), + ], +) +def test_check_fail_on_missing_config(method_call): + source = ab.get_connector("source-test") + + with pytest.raises(Exception, match="Config is not set, either set in get_connector or via source.set_config"): + method_call(source) + + def test_sync(): source = ab.get_connector("source-test", config={"apiKey": "test"}) cache = ab.get_in_memory_cache() From 76d9e179233b52072b323c6f3df0da4af7e50b74 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 9 Jan 2024 11:29:42 +0100 Subject: [PATCH 015/574] airbyte-lib: Add pip_url option and enforce source versions in a more consistent manner (#33967) --- airbyte-lib/airbyte_lib/executor.py | 79 ++++++++++++++----- airbyte-lib/airbyte_lib/factories.py | 35 ++++++-- .../integration_tests/test_integration.py | 34 +++++++- 3 files changed, 117 insertions(+), 31 deletions(-) diff --git a/airbyte-lib/airbyte_lib/executor.py b/airbyte-lib/airbyte_lib/executor.py index 98efb85073387..0b0815b509314 100644 --- a/airbyte-lib/airbyte_lib/executor.py +++ b/airbyte-lib/airbyte_lib/executor.py @@ -10,11 +10,21 @@ from airbyte_lib.registry import ConnectorMetadata +_LATEST_VERSION = "latest" + class Executor(ABC): - def __init__(self, metadata: ConnectorMetadata, target_version: str = "latest"): + def __init__( + self, + metadata: ConnectorMetadata, + target_version: str | None = None, + ) -> None: self.metadata = metadata - self.target_version = target_version if target_version != "latest" else metadata.latest_available_version + self.enforce_version = target_version is not None + if target_version is None or target_version == _LATEST_VERSION: + self.target_version = metadata.latest_available_version + else: + self.target_version = target_version @abstractmethod def execute(self, args: List[str]) -> Iterable[str]: @@ -73,10 +83,21 @@ def _stream_from_file(file: IO[str]): class VenvExecutor(Executor): - def __init__(self, metadata: ConnectorMetadata, target_version: str = "latest", install_if_missing: bool = False): + def __init__( + self, + metadata: ConnectorMetadata, + target_version: str | None = None, + install_if_missing: bool = False, + pip_url: str | None = None, + ) -> None: super().__init__(metadata, target_version) self.install_if_missing = install_if_missing + # This is a temporary install path that will be replaced with a proper package + # name once they are published. + # TODO: Replace with `f"airbyte-{self.metadata.name}"` + self.pip_url = pip_url or f"../airbyte-integrations/connectors/{self.metadata.name}" + def _get_venv_name(self): return f".venv-{self.metadata.name}" @@ -94,9 +115,7 @@ def install(self): pip_path = os.path.join(venv_name, "bin", "pip") - # TODO this is a temporary install path that will be replaced with a proper package name once they are published. At this point we are also using the version - package_to_install = f"../airbyte-integrations/connectors/{self.metadata.name}" - self._run_subprocess_and_raise_on_failure([pip_path, "install", "-e", package_to_install]) + self._run_subprocess_and_raise_on_failure([pip_path, "install", "-e", self.pip_url]) def _get_installed_version(self): """ @@ -105,11 +124,26 @@ def _get_installed_version(self): venv_name = self._get_venv_name() connector_name = self.metadata.name return subprocess.check_output( - [os.path.join(venv_name, "bin", "python"), "-c", f"from importlib.metadata import version; print(version('{connector_name}'))"], + [ + os.path.join(venv_name, "bin", "python"), + "-c", + f"from importlib.metadata import version; print(version('{connector_name}'))", + ], universal_newlines=True, ).strip() - def ensure_installation(self): + def ensure_installation( + self, + ): + """ + Ensure that the connector is installed in a virtual environment. + If not yet installed and if install_if_missing is True, then install. + + Optionally, verify that the installed version matches the target version. + + Note: Version verification is not supported for connectors installed from a + local path. + """ venv_name = f".venv-{self.metadata.name}" venv_path = Path(venv_name) if not venv_path.exists(): @@ -119,19 +153,22 @@ def ensure_installation(self): connector_path = self._get_connector_path() if not connector_path.exists(): - raise Exception(f"Could not find connector {self.metadata.name} in venv {venv_name}") - - installed_version = self._get_installed_version() - if installed_version != self.target_version: - # If the version doesn't match, reinstall - self.install() - - # Check the version again - version_after_install = self._get_installed_version() - if version_after_install != self.target_version: - raise Exception( - f"Failed to install connector {self.metadata.name} version {self.target_version}. Installed version is {version_after_install}" - ) + raise FileNotFoundError( + f"Could not find connector '{self.metadata.name}' " f"in venv '{venv_name}' with connector path '{connector_path}'." + ) + + if self.enforce_version: + installed_version = self._get_installed_version() + if installed_version != self.target_version: + # If the version doesn't match, reinstall + self.install() + + # Check the version again + version_after_install = self._get_installed_version() + if version_after_install != self.target_version: + raise Exception( + f"Failed to install connector {self.metadata.name} version {self.target_version}. Installed version is {version_after_install}" + ) def execute(self, args: List[str]) -> Iterable[str]: connector_path = self._get_connector_path() diff --git a/airbyte-lib/airbyte_lib/factories.py b/airbyte-lib/airbyte_lib/factories.py index 5b2f543b49ed8..7722541a03c5d 100644 --- a/airbyte-lib/airbyte_lib/factories.py +++ b/airbyte-lib/airbyte_lib/factories.py @@ -1,10 +1,10 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. -from typing import Any, Dict, Optional +from typing import Any from airbyte_lib.cache import InMemoryCache -from airbyte_lib.executor import PathExecutor, VenvExecutor +from airbyte_lib.executor import Executor, PathExecutor, VenvExecutor from airbyte_lib.registry import get_connector_metadata from airbyte_lib.source import Source @@ -15,20 +15,41 @@ def get_in_memory_cache(): def get_connector( name: str, - version: str = "latest", - config: Optional[Dict[str, Any]] = None, + version: str | None = None, + pip_url: str | None = None, + config: dict[str, Any] | None = None, use_local_install: bool = False, install_if_missing: bool = False, ): """ Get a connector by name and version. :param name: connector name - :param version: connector version - if not provided, the most recent version will be used - :param config: connector config - if not provided, you need to set it later via the set_config method + :param version: connector version - if not provided, the currently installed version will be used. If no version is installed, the latest available version will be used. The version can also be set to "latest" to force the use of the latest available version. + :param pip_url: connector pip URL - if not provided, the pip url will be inferred from the connector name. + :param config: connector config - if not provided, you need to set it later via the set_config method. :param use_local_install: whether to use a virtual environment to run the connector. If True, the connector is expected to be available on the path (e.g. installed via pip). If False, the connector will be installed automatically in a virtual environment. :param install_if_missing: whether to install the connector if it is not available locally. This parameter is ignored if use_local_install is True. """ metadata = get_connector_metadata(name) + if use_local_install: + if pip_url: + raise ValueError("Param 'pip_url' is not supported when 'use_local_install' is True") + if version: + raise ValueError("Param 'version' is not supported when 'use_local_install' is True") + executor: Executor = PathExecutor( + metadata=metadata, + target_version=version, + ) + + else: + executor = VenvExecutor( + metadata=metadata, + target_version=version, + install_if_missing=install_if_missing, + pip_url=pip_url, + ) return Source( - PathExecutor(metadata, version) if use_local_install else VenvExecutor(metadata, version, install_if_missing), name, config + executor=executor, + name=name, + config=config, ) diff --git a/airbyte-lib/tests/integration_tests/test_integration.py b/airbyte-lib/tests/integration_tests/test_integration.py index 863d0b1b90a60..31de856d8b0ac 100644 --- a/airbyte-lib/tests/integration_tests/test_integration.py +++ b/airbyte-lib/tests/integration_tests/test_integration.py @@ -5,6 +5,7 @@ import airbyte_lib as ab import pytest +from airbyte_lib.registry import _update_cache @pytest.fixture(scope="module", autouse=True) @@ -40,9 +41,36 @@ def test_non_existing_connector(): ab.get_connector("source-not-existing", config={"apiKey": "abc"}) -def test_wrong_version(): - with pytest.raises(Exception): - ab.get_connector("source-test", version="1.2.3", config={"apiKey": "abc"}) +@pytest.mark.parametrize( + "latest_available_version, requested_version, raises", + [ + ("0.0.1", None, False), + ("1.2.3", None, False), + ("0.0.1", "latest", False), + ("1.2.3", "latest", True), + ("0.0.1", "0.0.1", False), + ("1.2.3", "1.2.3", True), + ]) +def test_version_enforcement(raises, latest_available_version, requested_version): + """" + Ensures version enforcement works as expected: + * If no version is specified, the current version is accepted + * If the version is specified as "latest", only the latest available version is accepted + * If the version is specified as a semantic version, only the exact version is accepted + + In this test, the actually installed version is 0.0.1 + """ + _update_cache() + from airbyte_lib.registry import _cache + _cache["source-test"].latest_available_version = latest_available_version + if raises: + with pytest.raises(Exception): + ab.get_connector("source-test", version=requested_version, config={"apiKey": "abc"}) + else: + ab.get_connector("source-test", version=requested_version, config={"apiKey": "abc"}) + + # reset + _cache["source-test"].latest_available_version = "0.0.1" def test_check(): From 94f981ccbbe2f27a7c8a80bb12de4fbc4a345413 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 9 Jan 2024 11:30:37 +0100 Subject: [PATCH 016/574] Source Facebook Marketing: Convert to airbyte-lib (#33934) --- .../connectors/source-facebook-marketing/main.py | 8 ++------ .../source-facebook-marketing/metadata.yaml | 2 +- .../connectors/source-facebook-marketing/setup.py | 5 +++++ .../source_facebook_marketing/run.py | 15 +++++++++++++++ docs/integrations/sources/facebook-marketing.md | 1 + 5 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py diff --git a/airbyte-integrations/connectors/source-facebook-marketing/main.py b/airbyte-integrations/connectors/source-facebook-marketing/main.py index 64be48a5343e1..fc25c7149e939 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/main.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/main.py @@ -3,11 +3,7 @@ # -import sys - -from airbyte_cdk.entrypoint import launch -from source_facebook_marketing import SourceFacebookMarketing +from source_facebook_marketing.run import run if __name__ == "__main__": - source = SourceFacebookMarketing() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml b/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml index 6334e3aa57c72..faf2e1fa31bcb 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml +++ b/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: e7778cfc-e97c-4458-9ecb-b4f2bba8946c - dockerImageTag: 1.2.2 + dockerImageTag: 1.2.3 dockerRepository: airbyte/source-facebook-marketing documentationUrl: https://docs.airbyte.com/integrations/sources/facebook-marketing githubIssueLabel: source-facebook-marketing diff --git a/airbyte-integrations/connectors/source-facebook-marketing/setup.py b/airbyte-integrations/connectors/source-facebook-marketing/setup.py index 144e8b73abc70..44f12e25a0d1f 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/setup.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/setup.py @@ -25,4 +25,9 @@ extras_require={ "tests": TEST_REQUIREMENTS, }, + entry_points={ + "console_scripts": [ + "source-facebook-marketing=source_facebook_marketing.run:run", + ], + }, ) diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py new file mode 100644 index 0000000000000..c99ffb1439061 --- /dev/null +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py @@ -0,0 +1,15 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch + +from .source import SourceFacebookMarketing + + +def run(): + source = SourceFacebookMarketing() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/facebook-marketing.md b/docs/integrations/sources/facebook-marketing.md index 46465eefd2582..fb39df1d2d82c 100644 --- a/docs/integrations/sources/facebook-marketing.md +++ b/docs/integrations/sources/facebook-marketing.md @@ -203,6 +203,7 @@ The Facebook Marketing connector uses the `lookback_window` parameter to repeate | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 1.2.3 | 2024-01-04 | [33934](https://github.com/airbytehq/airbyte/pull/33828) | Make ready for airbyte-lib | | 1.2.2 | 2024-01-02 | [33828](https://github.com/airbytehq/airbyte/pull/33828) | Add insights job timeout to be an option, so a user can specify their own value | | 1.2.1 | 2023-11-22 | [32731](https://github.com/airbytehq/airbyte/pull/32731) | Removed validation that blocked personal ad accounts during `check` | | 1.2.0 | 2023-10-31 | [31999](https://github.com/airbytehq/airbyte/pull/31999) | Extend the `AdCreatives` stream schema | From e7ff2a10aba3a4b8371811191a3cbbc1e6fa67bd Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 9 Jan 2024 11:30:47 +0100 Subject: [PATCH 017/574] Source Salesforce: Convert to airbyte-lib (#33936) --- .../connectors/source-salesforce/main.py | 40 +---------------- .../source-salesforce/metadata.yaml | 2 +- .../connectors/source-salesforce/setup.py | 5 +++ .../source_salesforce/run.py | 45 +++++++++++++++++++ docs/integrations/sources/salesforce.md | 3 +- 5 files changed, 55 insertions(+), 40 deletions(-) create mode 100644 airbyte-integrations/connectors/source-salesforce/source_salesforce/run.py diff --git a/airbyte-integrations/connectors/source-salesforce/main.py b/airbyte-integrations/connectors/source-salesforce/main.py index 5ec9f05e10420..67536217f497f 100644 --- a/airbyte-integrations/connectors/source-salesforce/main.py +++ b/airbyte-integrations/connectors/source-salesforce/main.py @@ -3,43 +3,7 @@ # -import sys -import traceback -from datetime import datetime -from typing import List - -from airbyte_cdk.entrypoint import AirbyteEntrypoint, launch -from airbyte_cdk.models import AirbyteErrorTraceMessage, AirbyteMessage, AirbyteTraceMessage, TraceType, Type -from source_salesforce import SourceSalesforce - - -def _get_source(args: List[str]): - catalog_path = AirbyteEntrypoint.extract_catalog(args) - config_path = AirbyteEntrypoint.extract_config(args) - try: - return SourceSalesforce( - SourceSalesforce.read_catalog(catalog_path) if catalog_path else None, - SourceSalesforce.read_config(config_path) if config_path else None, - ) - except Exception as error: - print( - AirbyteMessage( - type=Type.TRACE, - trace=AirbyteTraceMessage( - type=TraceType.ERROR, - emitted_at=int(datetime.now().timestamp() * 1000), - error=AirbyteErrorTraceMessage( - message=f"Error starting the sync. This could be due to an invalid configuration or catalog. Please contact Support for assistance. Error: {error}", - stack_trace=traceback.format_exc(), - ), - ), - ).json() - ) - return None - +from source_salesforce.run import run if __name__ == "__main__": - _args = sys.argv[1:] - source = _get_source(_args) - if source: - launch(source, _args) + run() diff --git a/airbyte-integrations/connectors/source-salesforce/metadata.yaml b/airbyte-integrations/connectors/source-salesforce/metadata.yaml index c1a446b30196e..498448722b27c 100644 --- a/airbyte-integrations/connectors/source-salesforce/metadata.yaml +++ b/airbyte-integrations/connectors/source-salesforce/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: b117307c-14b6-41aa-9422-947e34922962 - dockerImageTag: 2.2.1 + dockerImageTag: 2.2.2 dockerRepository: airbyte/source-salesforce documentationUrl: https://docs.airbyte.com/integrations/sources/salesforce githubIssueLabel: source-salesforce diff --git a/airbyte-integrations/connectors/source-salesforce/setup.py b/airbyte-integrations/connectors/source-salesforce/setup.py index 22e6250d46608..4add132d7cb51 100644 --- a/airbyte-integrations/connectors/source-salesforce/setup.py +++ b/airbyte-integrations/connectors/source-salesforce/setup.py @@ -20,4 +20,9 @@ extras_require={ "tests": TEST_REQUIREMENTS, }, + entry_points={ + "console_scripts": [ + "source-salesforce=source_salesforce.run:run", + ], + }, ) diff --git a/airbyte-integrations/connectors/source-salesforce/source_salesforce/run.py b/airbyte-integrations/connectors/source-salesforce/source_salesforce/run.py new file mode 100644 index 0000000000000..7fe23dc8958c9 --- /dev/null +++ b/airbyte-integrations/connectors/source-salesforce/source_salesforce/run.py @@ -0,0 +1,45 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys +import traceback +from datetime import datetime +from typing import List + +from airbyte_cdk.entrypoint import AirbyteEntrypoint, launch +from airbyte_cdk.models import AirbyteErrorTraceMessage, AirbyteMessage, AirbyteTraceMessage, TraceType, Type +from source_salesforce import SourceSalesforce + + +def _get_source(args: List[str]): + catalog_path = AirbyteEntrypoint.extract_catalog(args) + config_path = AirbyteEntrypoint.extract_config(args) + try: + return SourceSalesforce( + SourceSalesforce.read_catalog(catalog_path) if catalog_path else None, + SourceSalesforce.read_config(config_path) if config_path else None, + ) + except Exception as error: + print( + AirbyteMessage( + type=Type.TRACE, + trace=AirbyteTraceMessage( + type=TraceType.ERROR, + emitted_at=int(datetime.now().timestamp() * 1000), + error=AirbyteErrorTraceMessage( + message=f"Error starting the sync. This could be due to an invalid configuration or catalog. Please contact Support for assistance. Error: {error}", + stack_trace=traceback.format_exc(), + ), + ), + ).json() + ) + return None + + +def run(): + _args = sys.argv[1:] + source = _get_source(_args) + if source: + launch(source, _args) diff --git a/docs/integrations/sources/salesforce.md b/docs/integrations/sources/salesforce.md index 738664f7f6aaa..282fc09a901cc 100644 --- a/docs/integrations/sources/salesforce.md +++ b/docs/integrations/sources/salesforce.md @@ -193,6 +193,7 @@ Now that you have set up the Salesforce source connector, check out the followin | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------| +| 2.2.2 | 2024-01-04 | [33936](https://github.com/airbytehq/airbyte/pull/33936) | Prepare for airbyte-lib | | 2.2.1 | 2023-12-12 | [33342](https://github.com/airbytehq/airbyte/pull/33342) | Added new ContentDocumentLink stream | | 2.2.0 | 2023-12-12 | [33350](https://github.com/airbytehq/airbyte/pull/33350) | Sync streams concurrently on full refresh | | 2.1.6 | 2023-11-28 | [32535](https://github.com/airbytehq/airbyte/pull/32535) | Run full refresh syncs concurrently | @@ -273,4 +274,4 @@ Now that you have set up the Salesforce source connector, check out the followin | 0.1.1 | 2021-09-21 | [6209](https://github.com/airbytehq/airbyte/pull/6209) | Fix bug with pagination for BULK API | | 0.1.0 | 2021-09-08 | [5619](https://github.com/airbytehq/airbyte/pull/5619) | Salesforce Aitbyte-Native Connector | - \ No newline at end of file + From 2edcfb36fbba0f498e901f8b781a5f990bbc0e37 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 9 Jan 2024 11:31:00 +0100 Subject: [PATCH 018/574] Source S3: Convert to airbyte-lib (#33937) --- .../connectors/source-s3/.coveragerc | 3 +- .../connectors/source-s3/main.py | 37 +--------------- .../connectors/source-s3/metadata.yaml | 2 +- .../connectors/source-s3/setup.py | 5 +++ .../connectors/source-s3/source_s3/run.py | 42 +++++++++++++++++++ docs/integrations/sources/s3.md | 3 +- 6 files changed, 54 insertions(+), 38 deletions(-) create mode 100644 airbyte-integrations/connectors/source-s3/source_s3/run.py diff --git a/airbyte-integrations/connectors/source-s3/.coveragerc b/airbyte-integrations/connectors/source-s3/.coveragerc index 0c7476c810736..4c1de9ec08539 100644 --- a/airbyte-integrations/connectors/source-s3/.coveragerc +++ b/airbyte-integrations/connectors/source-s3/.coveragerc @@ -3,4 +3,5 @@ omit = source_s3/exceptions.py source_s3/stream.py source_s3/utils.py - source_s3/source_files_abstract/source.py \ No newline at end of file + source_s3/source_files_abstract/source.py + source_s3/run.py \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-s3/main.py b/airbyte-integrations/connectors/source-s3/main.py index c3b6b0bc32ede..cb0007d5581b5 100644 --- a/airbyte-integrations/connectors/source-s3/main.py +++ b/airbyte-integrations/connectors/source-s3/main.py @@ -3,40 +3,7 @@ # -import sys -import traceback -from datetime import datetime -from typing import List - -from airbyte_cdk.entrypoint import AirbyteEntrypoint, launch -from airbyte_cdk.models import AirbyteErrorTraceMessage, AirbyteMessage, AirbyteTraceMessage, TraceType, Type -from source_s3.v4 import Config, Cursor, SourceS3, SourceS3StreamReader - - -def get_source(args: List[str]): - catalog_path = AirbyteEntrypoint.extract_catalog(args) - try: - return SourceS3(SourceS3StreamReader(), Config, catalog_path, cursor_cls=Cursor) - except Exception: - print( - AirbyteMessage( - type=Type.TRACE, - trace=AirbyteTraceMessage( - type=TraceType.ERROR, - emitted_at=int(datetime.now().timestamp() * 1000), - error=AirbyteErrorTraceMessage( - message="Error starting the sync. This could be due to an invalid configuration or catalog. Please contact Support for assistance.", - stack_trace=traceback.format_exc(), - ), - ), - ).json() - ) - return None - +from source_s3.run import run if __name__ == "__main__": - _args = sys.argv[1:] - source = get_source(_args) - - if source: - launch(source, _args) + run() diff --git a/airbyte-integrations/connectors/source-s3/metadata.yaml b/airbyte-integrations/connectors/source-s3/metadata.yaml index f3bed3cd1ff41..8f13506605fdf 100644 --- a/airbyte-integrations/connectors/source-s3/metadata.yaml +++ b/airbyte-integrations/connectors/source-s3/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: file connectorType: source definitionId: 69589781-7828-43c5-9f63-8925b1c1ccc2 - dockerImageTag: 4.3.0 + dockerImageTag: 4.3.1 dockerRepository: airbyte/source-s3 documentationUrl: https://docs.airbyte.com/integrations/sources/s3 githubIssueLabel: source-s3 diff --git a/airbyte-integrations/connectors/source-s3/setup.py b/airbyte-integrations/connectors/source-s3/setup.py index 3171ff163f0e6..e2d0000f29499 100644 --- a/airbyte-integrations/connectors/source-s3/setup.py +++ b/airbyte-integrations/connectors/source-s3/setup.py @@ -33,4 +33,9 @@ extras_require={ "tests": TEST_REQUIREMENTS, }, + entry_points={ + "console_scripts": [ + "source-s3=source_s3.run:run", + ], + }, ) diff --git a/airbyte-integrations/connectors/source-s3/source_s3/run.py b/airbyte-integrations/connectors/source-s3/source_s3/run.py new file mode 100644 index 0000000000000..67379f1ec4ab2 --- /dev/null +++ b/airbyte-integrations/connectors/source-s3/source_s3/run.py @@ -0,0 +1,42 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys +import traceback +from datetime import datetime +from typing import List + +from airbyte_cdk.entrypoint import AirbyteEntrypoint, launch +from airbyte_cdk.models import AirbyteErrorTraceMessage, AirbyteMessage, AirbyteTraceMessage, TraceType, Type +from source_s3.v4 import Config, Cursor, SourceS3, SourceS3StreamReader + + +def get_source(args: List[str]): + catalog_path = AirbyteEntrypoint.extract_catalog(args) + try: + return SourceS3(SourceS3StreamReader(), Config, catalog_path, cursor_cls=Cursor) + except Exception: + print( + AirbyteMessage( + type=Type.TRACE, + trace=AirbyteTraceMessage( + type=TraceType.ERROR, + emitted_at=int(datetime.now().timestamp() * 1000), + error=AirbyteErrorTraceMessage( + message="Error starting the sync. This could be due to an invalid configuration or catalog. Please contact Support for assistance.", + stack_trace=traceback.format_exc(), + ), + ), + ).json() + ) + return None + + +def run(): + _args = sys.argv[1:] + source = get_source(_args) + + if source: + launch(source, _args) diff --git a/docs/integrations/sources/s3.md b/docs/integrations/sources/s3.md index 533d25b292855..36e5eb5fef31c 100644 --- a/docs/integrations/sources/s3.md +++ b/docs/integrations/sources/s3.md @@ -256,6 +256,7 @@ To perform the text extraction from PDF and Docx files, the connector uses the [ | Version | Date | Pull Request | Subject | |:--------|:-----------|:----------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------| +| 4.3.1 | 2024-01-04 | [33937](https://github.com/airbytehq/airbyte/pull/33937) | Prepare for airbyte-lib | | 4.3.0 | 2023-12-14 | [33411](https://github.com/airbytehq/airbyte/pull/33411) | Bump CDK version to auto-set primary key for document file streams and support raw txt files | | 4.2.4 | 2023-12-06 | [33187](https://github.com/airbytehq/airbyte/pull/33187) | Bump CDK version to hide source-defined primary key | | 4.2.3 | 2023-11-16 | [32608](https://github.com/airbytehq/airbyte/pull/32608) | Improve document file type parser | @@ -334,4 +335,4 @@ To perform the text extraction from PDF and Docx files, the connector uses the [ | 0.1.3 | 2021-08-04 | [5197](https://github.com/airbytehq/airbyte/pull/5197) | Fixed bug where sync could hang indefinitely on schema inference | | 0.1.2 | 2021-08-02 | [5135](https://github.com/airbytehq/airbyte/pull/5135) | Fixed bug in spec so it displays in UI correctly | | 0.1.1 | 2021-07-30 | [4990](https://github.com/airbytehq/airbyte/pull/4990/commits/ff5f70662c5f84eabc03526cddfcc9d73c58c0f4) | Fixed documentation url in source definition | -| 0.1.0 | 2021-07-30 | [4990](https://github.com/airbytehq/airbyte/pull/4990) | Created S3 source connector | \ No newline at end of file +| 0.1.0 | 2021-07-30 | [4990](https://github.com/airbytehq/airbyte/pull/4990) | Created S3 source connector | From 804a7bf8adb757031648f972e0be71259f23159c Mon Sep 17 00:00:00 2001 From: Daryna Ishchenko <80129833+darynaishchenko@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:47:58 +0200 Subject: [PATCH 019/574] :hospital: Source Gitlab: increase test coverage, update Groups, Commits and Projects schemas. (#33676) --- .../integration_tests/expected_records.jsonl | 34 ++++---- .../expected_records_with_ids.jsonl | 34 ++++---- .../connectors/source-gitlab/metadata.yaml | 2 +- .../source_gitlab/schemas/commits.json | 11 +++ .../source_gitlab/schemas/groups.json | 9 +++ .../source_gitlab/schemas/issues.json | 31 +++---- .../source_gitlab/schemas/jobs.json | 3 + .../source_gitlab/schemas/projects.json | 9 +++ .../source-gitlab/source_gitlab/streams.py | 4 +- .../source-gitlab/unit_tests/test_config.json | 1 + .../unit_tests/test_config_migrations.py | 21 +++++ .../source-gitlab/unit_tests/test_source.py | 55 ++++++++++++- .../source-gitlab/unit_tests/test_streams.py | 52 ++++++++++++ .../source-gitlab/unit_tests/test_utils.py | 17 ++++ docs/integrations/sources/gitlab.md | 81 ++++++++++--------- 15 files changed, 271 insertions(+), 93 deletions(-) create mode 100644 airbyte-integrations/connectors/source-gitlab/unit_tests/test_config.json create mode 100644 airbyte-integrations/connectors/source-gitlab/unit_tests/test_config_migrations.py create mode 100644 airbyte-integrations/connectors/source-gitlab/unit_tests/test_utils.py diff --git a/airbyte-integrations/connectors/source-gitlab/integration_tests/expected_records.jsonl b/airbyte-integrations/connectors/source-gitlab/integration_tests/expected_records.jsonl index b1c7dfe2eb8f8..8db7341432c97 100644 --- a/airbyte-integrations/connectors/source-gitlab/integration_tests/expected_records.jsonl +++ b/airbyte-integrations/connectors/source-gitlab/integration_tests/expected_records.jsonl @@ -7,9 +7,9 @@ {"stream": "merge_requests", "data": {"id": 92594931, "iid": 3, "project_id": 25157276, "title": "add fake CI config", "description": "", "state": "merged", "created_at": "2021-03-18T12:49:13.091Z", "updated_at": "2021-03-18T12:51:06.319Z", "merged_by": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "merge_user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "merged_at": "2021-03-18T12:51:06.470Z", "closed_by": null, "closed_at": null, "target_branch": "master", "source_branch": "ykurochkin/add-fake-CI-config", "user_notes_count": 0, "upvotes": 0, "downvotes": 0, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "assignees": [], "assignee": null, "reviewers": [], "source_project_id": 25157276, "target_project_id": 25157276, "labels": [], "draft": false, "work_in_progress": false, "milestone": null, "merge_when_pipeline_succeeds": false, "merge_status": "can_be_merged", "detailed_merge_status": "not_open", "sha": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "merge_commit_sha": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "squash_commit_sha": null, "discussion_locked": null, "should_remove_source_branch": null, "force_remove_source_branch": true, "prepared_at": "2021-03-18T12:49:13.091Z", "reference": "!3", "references": {"short": "!3", "relative": "!3", "full": "new-group-airbute/new-ci-test-project!3"}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests/3", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "squash": false, "squash_on_merge": false, "task_completion_status": {"count": 0, "completed_count": 0}, "has_conflicts": false, "blocking_discussions_resolved": true, "approvals_before_merge": null, "author_id": 8375961, "assignee_id": null, "closed_by_id": null, "milestone_id": null, "merged_by_id": 8375961}, "emitted_at": 1696948541619} {"stream": "merge_requests", "data": {"id": 92593913, "iid": 2, "project_id": 25157276, "title": "update readme.md", "description": "", "state": "opened", "created_at": "2021-03-18T12:42:30.200Z", "updated_at": "2021-03-18T12:42:30.200Z", "merged_by": null, "merge_user": null, "merged_at": null, "closed_by": null, "closed_at": null, "target_branch": "master", "source_branch": "ykurochkin/test-branch", "user_notes_count": 0, "upvotes": 0, "downvotes": 0, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "assignees": [], "assignee": null, "reviewers": [], "source_project_id": 25157276, "target_project_id": 25157276, "labels": [], "draft": false, "work_in_progress": false, "milestone": null, "merge_when_pipeline_succeeds": false, "merge_status": "can_be_merged", "detailed_merge_status": "mergeable", "sha": "9b0c5cf345f0ca1a3fb3ae253e74e0616abf8129", "merge_commit_sha": null, "squash_commit_sha": null, "discussion_locked": null, "should_remove_source_branch": null, "force_remove_source_branch": true, "prepared_at": "2021-03-18T12:42:30.200Z", "reference": "!2", "references": {"short": "!2", "relative": "!2", "full": "new-group-airbute/new-ci-test-project!2"}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests/2", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "squash": false, "squash_on_merge": false, "task_completion_status": {"count": 0, "completed_count": 0}, "has_conflicts": false, "blocking_discussions_resolved": true, "approvals_before_merge": null, "author_id": 8375961, "assignee_id": null, "closed_by_id": null, "milestone_id": null, "merged_by_id": null}, "emitted_at": 1696948541622} {"stream": "merge_requests", "data": {"id": 92111504, "iid": 1, "project_id": 25157276, "title": "Draft: Resolve \"Fake Issue 30\"", "description": "Closes #31", "state": "opened", "created_at": "2021-03-15T16:08:05.071Z", "updated_at": "2021-03-15T16:08:05.071Z", "merged_by": null, "merge_user": null, "merged_at": null, "closed_by": null, "closed_at": null, "target_branch": "master", "source_branch": "31-fake-issue-30", "user_notes_count": 0, "upvotes": 0, "downvotes": 0, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "assignees": [8375961], "assignee": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "reviewers": [], "source_project_id": 25157276, "target_project_id": 25157276, "labels": ["bug"], "draft": true, "work_in_progress": true, "milestone": null, "merge_when_pipeline_succeeds": false, "merge_status": "cannot_be_merged", "detailed_merge_status": "draft_status", "sha": "2831d897ba0214f8d3168647e8ad4232b83987ef", "merge_commit_sha": null, "squash_commit_sha": null, "discussion_locked": null, "should_remove_source_branch": null, "force_remove_source_branch": true, "prepared_at": "2021-03-15T16:08:05.071Z", "reference": "!1", "references": {"short": "!1", "relative": "!1", "full": "new-group-airbute/new-ci-test-project!1"}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests/1", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "squash": false, "squash_on_merge": false, "task_completion_status": {"count": 0, "completed_count": 0}, "has_conflicts": true, "blocking_discussions_resolved": true, "approvals_before_merge": null, "author_id": 8375961, "assignee_id": 8375961, "closed_by_id": null, "milestone_id": null, "merged_by_id": null}, "emitted_at": 1696948541624} -{"stream": "groups", "data": {"id": 11329647, "web_url": "https://gitlab.com/groups/new-group-airbute", "name": "New Group Airbute", "path": "new-group-airbute", "description": "", "visibility": "public", "share_with_group_lock": false, "require_two_factor_authentication": false, "two_factor_grace_period": 48, "project_creation_level": "developer", "auto_devops_enabled": null, "subgroup_creation_level": "maintainer", "emails_disabled": null, "mentions_disabled": null, "lfs_enabled": true, "default_branch_protection": 2, "default_branch_protection_defaults": {"allowed_to_push": [{"access_level": 30}], "allow_force_push": true, "allowed_to_merge": [{"access_level": 30}]}, "avatar_url": null, "request_access_enabled": true, "full_name": "New Group Airbute", "full_path": "new-group-airbute", "created_at": "2021-03-15T15:55:53.613Z", "parent_id": null, "shared_runners_setting": "enabled", "ldap_cn": null, "ldap_access": null, "wiki_access_level": "enabled", "shared_with_groups": [], "runners_token": "GR1348941-PhosPap-Sf1UxL1g6m4", "prevent_sharing_groups_outside_hierarchy": false, "shared_projects": [], "shared_runners_minutes_limit": null, "extra_shared_runners_minutes_limit": null, "prevent_forking_outside_group": null, "membership_lock": false, "projects": [{"id": 25157276, "path_with_namespace": "new-group-airbute/new-ci-test-project"}]}, "emitted_at": 1696948783668} -{"stream": "groups", "data": {"id": 61014882, "web_url": "https://gitlab.com/groups/new-group-airbute/test-subgroup-airbyte/test-private-sg", "name": "Test Private SG", "path": "test-private-sg", "description": "", "visibility": "private", "share_with_group_lock": false, "require_two_factor_authentication": false, "two_factor_grace_period": 48, "project_creation_level": "developer", "auto_devops_enabled": null, "subgroup_creation_level": "maintainer", "emails_disabled": null, "mentions_disabled": null, "lfs_enabled": true, "default_branch_protection": 2, "default_branch_protection_defaults": {"allowed_to_push": [{"access_level": 30}], "allow_force_push": true, "allowed_to_merge": [{"access_level": 30}]}, "avatar_url": null, "request_access_enabled": true, "full_name": "New Group Airbute / Test Subgroup Airbyte / Test Private SG", "full_path": "new-group-airbute/test-subgroup-airbyte/test-private-sg", "created_at": "2022-12-02T08:46:22.648Z", "parent_id": 61014863, "shared_runners_setting": "enabled", "ldap_cn": null, "ldap_access": null, "wiki_access_level": "enabled", "shared_with_groups": [], "runners_token": "GR1348941bjUaJQy2zzar-JmNBjfq", "shared_projects": [], "shared_runners_minutes_limit": null, "extra_shared_runners_minutes_limit": null, "prevent_forking_outside_group": null, "membership_lock": false, "projects": []}, "emitted_at": 1696948783989} -{"stream": "groups", "data": {"id": 61015181, "web_url": "https://gitlab.com/groups/new-group-airbute/test-public-sg/test-sg-public-2/test-private-subsubg-1", "name": "Test Private SubSubG 1", "path": "test-private-subsubg-1", "description": "", "visibility": "private", "share_with_group_lock": false, "require_two_factor_authentication": false, "two_factor_grace_period": 48, "project_creation_level": "developer", "auto_devops_enabled": null, "subgroup_creation_level": "maintainer", "emails_disabled": null, "mentions_disabled": null, "lfs_enabled": true, "default_branch_protection": 2, "default_branch_protection_defaults": {"allowed_to_push": [{"access_level": 30}], "allow_force_push": true, "allowed_to_merge": [{"access_level": 30}]}, "avatar_url": null, "request_access_enabled": true, "full_name": "New Group Airbute / Test Public SG / Test SG Public 2 / Test Private SubSubG 1", "full_path": "new-group-airbute/test-public-sg/test-sg-public-2/test-private-subsubg-1", "created_at": "2022-12-02T08:54:42.252Z", "parent_id": 61014943, "shared_runners_setting": "enabled", "ldap_cn": null, "ldap_access": null, "wiki_access_level": "enabled", "shared_with_groups": [], "runners_token": "GR1348941x8xQf6K-UvnnyJ-bcut4", "shared_projects": [], "shared_runners_minutes_limit": null, "extra_shared_runners_minutes_limit": null, "prevent_forking_outside_group": null, "membership_lock": false, "projects": [{"id": 41551658, "path_with_namespace": "new-group-airbute/test-public-sg/test-sg-public-2/test-private-subsubg-1/test_project_in_nested_subgroup"}]}, "emitted_at": 1696948784394} +{"stream": "groups", "data": {"id": 68657749, "web_url": "https://gitlab.com/groups/empty-group4", "name": "Empty Group", "path": "empty-group4", "description": "", "visibility": "private", "share_with_group_lock": false, "require_two_factor_authentication": false, "two_factor_grace_period": 48, "project_creation_level": "developer", "auto_devops_enabled": null, "subgroup_creation_level": "maintainer", "emails_disabled": false, "emails_enabled": true, "mentions_disabled": null, "lfs_enabled": true, "default_branch_protection": 2, "default_branch_protection_defaults": {"allowed_to_push": [{"access_level": 30}], "allow_force_push": true, "allowed_to_merge": [{"access_level": 30}]}, "avatar_url": null, "request_access_enabled": true, "full_name": "Empty Group", "full_path": "empty-group4", "created_at": "2023-06-09T13:47:19.446Z", "parent_id": null, "organization_id": 1, "shared_runners_setting": "enabled", "ldap_cn": null, "ldap_access": null, "wiki_access_level": "enabled", "shared_with_groups": [], "runners_token": "GR1348941-x4xtBM6_zdFj-ED8QF8", "prevent_sharing_groups_outside_hierarchy": false, "shared_projects": [], "shared_runners_minutes_limit": null, "extra_shared_runners_minutes_limit": null, "prevent_forking_outside_group": null, "membership_lock": false, "projects": []}, "emitted_at": 1704732558754} +{"stream": "groups", "data": {"id": 11329647, "web_url": "https://gitlab.com/groups/new-group-airbute", "name": "New Group Airbute", "path": "new-group-airbute", "description": "", "visibility": "public", "share_with_group_lock": false, "require_two_factor_authentication": false, "two_factor_grace_period": 48, "project_creation_level": "developer", "auto_devops_enabled": null, "subgroup_creation_level": "maintainer", "emails_disabled": false, "emails_enabled": true, "mentions_disabled": null, "lfs_enabled": true, "default_branch_protection": 2, "default_branch_protection_defaults": {"allowed_to_push": [{"access_level": 30}], "allow_force_push": true, "allowed_to_merge": [{"access_level": 30}]}, "avatar_url": null, "request_access_enabled": true, "full_name": "New Group Airbute", "full_path": "new-group-airbute", "created_at": "2021-03-15T15:55:53.613Z", "parent_id": null, "organization_id": 1, "shared_runners_setting": "enabled", "ldap_cn": null, "ldap_access": null, "wiki_access_level": "enabled", "shared_with_groups": [], "runners_token": "GR1348941-PhosPap-Sf1UxL1g6m4", "prevent_sharing_groups_outside_hierarchy": false, "shared_projects": [], "shared_runners_minutes_limit": null, "extra_shared_runners_minutes_limit": null, "prevent_forking_outside_group": null, "membership_lock": false, "projects": [{"id": 25157276, "path_with_namespace": "new-group-airbute/new-ci-test-project"}]}, "emitted_at": 1704732559167} +{"stream": "groups", "data": {"id": 61014882, "web_url": "https://gitlab.com/groups/new-group-airbute/test-subgroup-airbyte/test-private-sg", "name": "Test Private SG", "path": "test-private-sg", "description": "", "visibility": "private", "share_with_group_lock": false, "require_two_factor_authentication": false, "two_factor_grace_period": 48, "project_creation_level": "developer", "auto_devops_enabled": null, "subgroup_creation_level": "maintainer", "emails_disabled": false, "emails_enabled": true, "mentions_disabled": null, "lfs_enabled": true, "default_branch_protection": 2, "default_branch_protection_defaults": {"allowed_to_push": [{"access_level": 30}], "allow_force_push": true, "allowed_to_merge": [{"access_level": 30}]}, "avatar_url": null, "request_access_enabled": true, "full_name": "New Group Airbute / Test Subgroup Airbyte / Test Private SG", "full_path": "new-group-airbute/test-subgroup-airbyte/test-private-sg", "created_at": "2022-12-02T08:46:22.648Z", "parent_id": 61014863, "organization_id": 1, "shared_runners_setting": "enabled", "ldap_cn": null, "ldap_access": null, "wiki_access_level": "enabled", "shared_with_groups": [], "runners_token": "GR1348941bjUaJQy2zzar-JmNBjfq", "shared_projects": [], "shared_runners_minutes_limit": null, "extra_shared_runners_minutes_limit": null, "prevent_forking_outside_group": null, "membership_lock": false, "projects": []}, "emitted_at": 1704732559573} {"stream": "epic_issues", "data": {"id": 120214448, "iid": 31, "project_id": 25156633, "title": "Unit tests", "description": null, "state": "opened", "created_at": "2022-12-11T10:50:25.940Z", "updated_at": "2022-12-11T10:50:25.940Z", "closed_at": null, "closed_by": null, "labels": [], "milestone": null, "assignees": [], "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "type": "ISSUE", "assignee": null, "user_notes_count": 0, "merge_requests_count": 0, "upvotes": 0, "downvotes": 0, "due_date": null, "confidential": false, "discussion_locked": null, "issue_type": "issue", "web_url": "https://gitlab.com/airbyte.io/ci-test-project/-/issues/31", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "task_completion_status": {"count": 0, "completed_count": 0}, "weight": null, "blocking_issues_count": 0, "has_tasks": true, "task_status": "", "_links": {"self": "https://gitlab.com/api/v4/projects/25156633/issues/31", "notes": "https://gitlab.com/api/v4/projects/25156633/issues/31/notes", "award_emoji": "https://gitlab.com/api/v4/projects/25156633/issues/31/award_emoji", "project": "https://gitlab.com/api/v4/projects/25156633", "closed_as_duplicate_of": null}, "references": {"short": "#31", "relative": "#31", "full": "airbyte.io/ci-test-project#31"}, "severity": "UNKNOWN", "moved_to_id": null, "service_desk_reply_to": null, "epic_iid": 1, "epic": {"id": 678569, "iid": 1, "title": "Source Gitlab: certify to Beta", "url": "/groups/airbyte.io/-/epics/1", "group_id": 11266951, "human_readable_end_date": "Dec 30, 2022", "human_readable_timestamp": "Past due"}, "iteration": null, "epic_issue_id": 1899479, "relative_position": 0, "milestone_id": null, "assignee_id": null, "author_id": 8375961}, "emitted_at": 1696949059273} {"stream": "epic_issues", "data": {"id": 80659730, "iid": 13, "project_id": 25032440, "title": "Start a free trial of GitLab Gold - no credit card required :rocket:", "description": "At any point while using the free version of GitLab you can start a trial of GitLab Gold for free for 30 days. With a GitLab Gold trial, you'll get access to all of the most popular features across all of the paid tiers within GitLab. \n \n:white_check_mark: Reduce risk by requiring team leaders to approve merge requests.\n \n:white_check_mark: Ensure code quality with Multiple code reviews.\n \n:white_check_mark: Run your CI pipelines for up to 50,000 minutes (~9,500 CI builds).\n \n:white_check_mark: Plan and organize parallel development with multiple issue boards.\n \n:white_check_mark: Report on the productivity of each team in your organization by using issue analytics. \n \n:white_check_mark: Dynamically scan Docker images for vulnerabilities before production pushes. \n \n:white_check_mark: Scan security vulnerabilities, license compliance and dependencies in your CI pipelines. \n \n:white_check_mark: Get alerted when your application performance degrades. \n \n:white_check_mark: And so much more, [you can view all the features here](https://about.gitlab.com/pricing/gitlab-com/feature-comparison/). \n \n## Next steps\n* [ ] [Click here to start a trial of GitLab Gold.](https://gitlab.com/-/trial_registrations/new?glm_content=user_onboarding_whats_in_paid_tiers&glm_source=gitlab.com)", "state": "opened", "created_at": "2021-03-10T17:16:56.091Z", "updated_at": "2023-10-10T11:44:39.796Z", "closed_at": null, "closed_by": null, "labels": ["Novice"], "milestone": null, "assignees": [8375961], "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "type": "ISSUE", "assignee": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "user_notes_count": 0, "merge_requests_count": 0, "upvotes": 0, "downvotes": 0, "due_date": null, "confidential": false, "discussion_locked": null, "issue_type": "issue", "web_url": "https://gitlab.com/airbyte.io/learn-gitlab/-/issues/13", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "task_completion_status": {"count": 1, "completed_count": 0}, "weight": null, "blocking_issues_count": 0, "has_tasks": false, "_links": {"self": "https://gitlab.com/api/v4/projects/25032440/issues/13", "notes": "https://gitlab.com/api/v4/projects/25032440/issues/13/notes", "award_emoji": "https://gitlab.com/api/v4/projects/25032440/issues/13/award_emoji", "project": "https://gitlab.com/api/v4/projects/25032440", "closed_as_duplicate_of": null}, "references": {"short": "#13", "relative": "#13", "full": "airbyte.io/learn-gitlab#13"}, "severity": "UNKNOWN", "moved_to_id": null, "service_desk_reply_to": null, "epic_iid": 1, "epic": {"id": 678569, "iid": 1, "title": "Source Gitlab: certify to Beta", "url": "/groups/airbyte.io/-/epics/1", "group_id": 11266951, "human_readable_end_date": "Dec 30, 2022", "human_readable_timestamp": "Past due"}, "iteration": null, "epic_issue_id": 3762298, "relative_position": -513, "milestone_id": null, "assignee_id": 8375961, "author_id": 8375961}, "emitted_at": 1696949059274} {"stream": "issues", "data": {"id": 80943819, "iid": 32, "project_id": 25157276, "title": "Fake Issue 31", "description": null, "state": "opened", "created_at": "2021-03-15T15:22:42.206Z", "updated_at": "2021-03-15T15:22:42.206Z", "closed_at": null, "closed_by": null, "labels": ["bug"], "milestone": null, "assignees": [], "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "type": "ISSUE", "assignee": null, "user_notes_count": 0, "merge_requests_count": 0, "upvotes": 0, "downvotes": 0, "due_date": null, "confidential": false, "discussion_locked": null, "issue_type": "issue", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/issues/32", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "task_completion_status": {"count": 0, "completed_count": 0}, "blocking_issues_count": 0, "has_tasks": true, "task_status": "", "_links": {"self": "https://gitlab.com/api/v4/projects/25157276/issues/32", "notes": "https://gitlab.com/api/v4/projects/25157276/issues/32/notes", "award_emoji": "https://gitlab.com/api/v4/projects/25157276/issues/32/award_emoji", "project": "https://gitlab.com/api/v4/projects/25157276", "closed_as_duplicate_of": null}, "references": {"short": "#32", "relative": "#32", "full": "new-group-airbute/new-ci-test-project#32"}, "severity": "UNKNOWN", "moved_to_id": null, "service_desk_reply_to": null, "author_id": 8375961, "assignee_id": null, "closed_by_id": null, "milestone_id": null}, "emitted_at": 1696949354572} @@ -18,20 +18,20 @@ {"stream": "project_members", "data": {"access_level": 40, "created_at": "2021-03-15T15:08:36.746Z", "created_by": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "expires_at": null, "id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "membership_state": "active", "project_id": 25157276}, "emitted_at": 1696949674671} {"stream": "epics", "data": {"id": 1977226, "iid": 2, "color": "#1068bf", "text_color": "#FFFFFF", "group_id": 11266951, "parent_id": null, "parent_iid": null, "title": "Test epic", "description": null, "confidential": false, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "start_date": null, "start_date_is_fixed": false, "start_date_fixed": null, "start_date_from_inherited_source": null, "start_date_from_milestones": null, "end_date": null, "due_date": null, "due_date_is_fixed": false, "due_date_fixed": null, "due_date_from_inherited_source": null, "due_date_from_milestones": null, "state": "opened", "web_edit_url": "/groups/airbyte.io/-/epics/2", "web_url": "https://gitlab.com/groups/airbyte.io/-/epics/2", "references": {"short": "&2", "relative": "&2", "full": "airbyte.io&2"}, "created_at": "2023-10-10T10:37:36.529Z", "updated_at": "2023-10-10T11:44:50.107Z", "closed_at": null, "labels": [], "upvotes": 0, "downvotes": 0, "_links": {"self": "https://gitlab.com/api/v4/groups/11266951/epics/2", "epic_issues": "https://gitlab.com/api/v4/groups/11266951/epics/2/issues", "group": "https://gitlab.com/api/v4/groups/11266951", "parent": null}, "author_id": 8375961}, "emitted_at": 1696949906098} {"stream": "epics", "data": {"id": 678569, "iid": 1, "color": "#1068bf", "text_color": "#FFFFFF", "group_id": 11266951, "parent_id": null, "parent_iid": null, "title": "Source Gitlab: certify to Beta", "description": "Lorem ipsum", "confidential": false, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "start_date": "2022-12-11", "start_date_is_fixed": true, "start_date_fixed": "2022-12-11", "start_date_from_inherited_source": null, "start_date_from_milestones": null, "end_date": "2022-12-30", "due_date": "2022-12-30", "due_date_is_fixed": true, "due_date_fixed": "2022-12-30", "due_date_from_inherited_source": null, "due_date_from_milestones": null, "state": "opened", "web_edit_url": "/groups/airbyte.io/-/epics/1", "web_url": "https://gitlab.com/groups/airbyte.io/-/epics/1", "references": {"short": "&1", "relative": "&1", "full": "airbyte.io&1"}, "created_at": "2022-12-11T10:50:04.280Z", "updated_at": "2023-10-10T11:44:49.999Z", "closed_at": null, "labels": [], "upvotes": 1, "downvotes": 0, "_links": {"self": "https://gitlab.com/api/v4/groups/11266951/epics/1", "epic_issues": "https://gitlab.com/api/v4/groups/11266951/epics/1/issues", "group": "https://gitlab.com/api/v4/groups/11266951", "parent": null}, "author_id": 8375961}, "emitted_at": 1696949906100} -{"stream": "commits", "data": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98", "stats": {"additions": 14, "deletions": 0, "total": 14}, "project_id": 25157276}, "emitted_at": 1696950309747} -{"stream": "commits", "data": {"id": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "short_id": "028c02d9", "created_at": "2021-03-18T14:48:41.000+02:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef"], "title": "add fake CI config", "message": "add fake CI config\n", "author_name": "ykurochkin", "author_email": "zhenia.kurochkin@gmail.com", "authored_date": "2021-03-18T14:48:41.000+02:00", "committer_name": "ykurochkin", "committer_email": "zhenia.kurochkin@gmail.com", "committed_date": "2021-03-18T14:48:41.000+02:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/028c02d96f40afe9b4d1173c1d0f712dd6d07302", "stats": {"additions": 14, "deletions": 0, "total": 14}, "project_id": 25157276}, "emitted_at": 1696950309749} -{"stream": "commits", "data": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef", "stats": {"additions": 2, "deletions": 0, "total": 2}, "project_id": 25157276}, "emitted_at": 1696950309750} -{"stream": "jobs", "data": {"id": 1108959782, "status": "failed", "stage": "test", "name": "test-code-job2", "ref": "master", "tag": false, "coverage": null, "allow_failure": false, "created_at": "2021-03-18T12:51:06.294Z", "started_at": "2021-03-18T12:51:07.646Z", "finished_at": "2021-03-18T12:51:51.309Z", "erased_at": null, "duration": 43.662407, "queued_duration": 1.180926, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "created_at": "2021-03-10T17:13:46.589Z", "bio": "", "location": "", "public_email": "", "skype": "", "linkedin": "", "twitter": "", "discord": "", "website_url": "", "organization": "", "job_title": "", "pronouns": "", "bot": false, "work_information": null, "followers": 0, "following": 0, "local_time": "11:08 AM"}, "commit": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98"}, "pipeline": {"id": 272632767, "iid": 2, "project_id": 25157276, "sha": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "ref": "master", "status": "failed", "source": "push", "created_at": "2021-03-18T12:51:06.262Z", "updated_at": "2021-03-18T12:51:52.007Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272632767"}, "failure_reason": "script_failure", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/jobs/1108959782", "project": {"ci_job_token_scope_enabled": false}, "artifacts": [{"file_type": "trace", "size": 2200, "filename": "job.log", "file_format": null}], "runner": null, "artifacts_expire_at": null, "tag_list": [], "user_id": 8375961, "pipeline_id": 272632767, "runner_id": null, "commit_id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "project_id": 25157276}, "emitted_at": 1686568098000} -{"stream": "jobs", "data": {"id": 1108959779, "status": "failed", "stage": "test", "name": "test-code-job1", "ref": "master", "tag": false, "coverage": null, "allow_failure": false, "created_at": "2021-03-18T12:51:06.279Z", "started_at": "2021-03-18T12:51:07.943Z", "finished_at": "2021-03-18T12:51:50.943Z", "erased_at": null, "duration": 42.999853, "queued_duration": 1.349274, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "created_at": "2021-03-10T17:13:46.589Z", "bio": "", "location": "", "public_email": "", "skype": "", "linkedin": "", "twitter": "", "discord": "", "website_url": "", "organization": "", "job_title": "", "pronouns": "", "bot": false, "work_information": null, "followers": 0, "following": 0, "local_time": "11:08 AM"}, "commit": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98"}, "pipeline": {"id": 272632767, "iid": 2, "project_id": 25157276, "sha": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "ref": "master", "status": "failed", "source": "push", "created_at": "2021-03-18T12:51:06.262Z", "updated_at": "2021-03-18T12:51:52.007Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272632767"}, "failure_reason": "script_failure", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/jobs/1108959779", "project": {"ci_job_token_scope_enabled": false}, "artifacts": [{"file_type": "trace", "size": 2182, "filename": "job.log", "file_format": null}], "runner": null, "artifacts_expire_at": null, "tag_list": [], "user_id": 8375961, "pipeline_id": 272632767, "runner_id": null, "commit_id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "project_id": 25157276}, "emitted_at": 1686568098001} -{"stream": "jobs", "data": {"id": 1108952832, "status": "failed", "stage": "test", "name": "test-code-job2", "ref": "ykurochkin/add-fake-CI-config", "tag": false, "coverage": null, "allow_failure": false, "created_at": "2021-03-18T12:48:49.222Z", "started_at": "2021-03-18T12:48:50.732Z", "finished_at": "2021-03-18T12:49:37.961Z", "erased_at": null, "duration": 47.229034, "queued_duration": 1.422541, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "created_at": "2021-03-10T17:13:46.589Z", "bio": "", "location": "", "public_email": "", "skype": "", "linkedin": "", "twitter": "", "discord": "", "website_url": "", "organization": "", "job_title": "", "pronouns": "", "bot": false, "work_information": null, "followers": 0, "following": 0, "local_time": "11:08 AM"}, "commit": {"id": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "short_id": "028c02d9", "created_at": "2021-03-18T14:48:41.000+02:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef"], "title": "add fake CI config", "message": "add fake CI config\n", "author_name": "ykurochkin", "author_email": "zhenia.kurochkin@gmail.com", "authored_date": "2021-03-18T14:48:41.000+02:00", "committer_name": "ykurochkin", "committer_email": "zhenia.kurochkin@gmail.com", "committed_date": "2021-03-18T14:48:41.000+02:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/028c02d96f40afe9b4d1173c1d0f712dd6d07302"}, "pipeline": {"id": 272631271, "iid": 1, "project_id": 25157276, "sha": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "ref": "ykurochkin/add-fake-CI-config", "status": "failed", "source": "push", "created_at": "2021-03-18T12:48:49.174Z", "updated_at": "2021-03-18T12:49:38.092Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272631271"}, "failure_reason": "script_failure", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/jobs/1108952832", "project": {"ci_job_token_scope_enabled": false}, "artifacts": [{"file_type": "trace", "size": 2223, "filename": "job.log", "file_format": null}], "runner": null, "artifacts_expire_at": null, "tag_list": [], "user_id": 8375961, "pipeline_id": 272631271, "runner_id": null, "commit_id": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "project_id": 25157276}, "emitted_at": 1686568098411} +{"stream": "commits", "data": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98", "stats": {"additions": 14, "deletions": 0, "total": 14}, "project_id": 25157276}, "emitted_at": 1703256223650} +{"stream": "commits", "data": {"id": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "short_id": "028c02d9", "created_at": "2021-03-18T14:48:41.000+02:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef"], "title": "add fake CI config", "message": "add fake CI config\n", "author_name": "ykurochkin", "author_email": "zhenia.kurochkin@gmail.com", "authored_date": "2021-03-18T14:48:41.000+02:00", "committer_name": "ykurochkin", "committer_email": "zhenia.kurochkin@gmail.com", "committed_date": "2021-03-18T14:48:41.000+02:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/028c02d96f40afe9b4d1173c1d0f712dd6d07302", "stats": {"additions": 14, "deletions": 0, "total": 14}, "project_id": 25157276}, "emitted_at": 1703256223651} +{"stream": "commits", "data": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef", "stats": {"additions": 2, "deletions": 0, "total": 2}, "project_id": 25157276}, "emitted_at": 1703256223652} +{"stream": "jobs", "data": {"id": 1108959782, "status": "failed", "stage": "test", "name": "test-code-job2", "ref": "master", "tag": false, "coverage": null, "allow_failure": false, "created_at": "2021-03-18T12:51:06.294Z", "started_at": "2021-03-18T12:51:07.646Z", "finished_at": "2021-03-18T12:51:51.309Z", "erased_at": null, "duration": 43.662407, "queued_duration": 1.180926, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "created_at": "2021-03-10T17:13:46.589Z", "bio": "", "location": "", "public_email": "", "skype": "", "linkedin": "", "twitter": "", "discord": "", "website_url": "", "organization": "", "job_title": "", "pronouns": "", "bot": false, "work_information": null, "followers": 0, "following": 0, "local_time": "5:02 PM"}, "commit": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98"}, "pipeline": {"id": 272632767, "iid": 2, "project_id": 25157276, "sha": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "ref": "master", "status": "failed", "source": "push", "created_at": "2021-03-18T12:51:06.262Z", "updated_at": "2021-03-18T12:51:52.007Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272632767"}, "failure_reason": "script_failure", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/jobs/1108959782", "project": {"ci_job_token_scope_enabled": false}, "artifacts": [{"file_type": "trace", "size": 2200, "filename": "job.log", "file_format": null}], "runner": null, "artifacts_expire_at": null, "archived": false, "tag_list": [], "user_id": 8375961, "pipeline_id": 272632767, "runner_id": null, "commit_id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "project_id": 25157276}, "emitted_at": 1704733346934} +{"stream": "jobs", "data": {"id": 1108959779, "status": "failed", "stage": "test", "name": "test-code-job1", "ref": "master", "tag": false, "coverage": null, "allow_failure": false, "created_at": "2021-03-18T12:51:06.279Z", "started_at": "2021-03-18T12:51:07.943Z", "finished_at": "2021-03-18T12:51:50.943Z", "erased_at": null, "duration": 42.999853, "queued_duration": 1.349274, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "created_at": "2021-03-10T17:13:46.589Z", "bio": "", "location": "", "public_email": "", "skype": "", "linkedin": "", "twitter": "", "discord": "", "website_url": "", "organization": "", "job_title": "", "pronouns": "", "bot": false, "work_information": null, "followers": 0, "following": 0, "local_time": "5:02 PM"}, "commit": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98"}, "pipeline": {"id": 272632767, "iid": 2, "project_id": 25157276, "sha": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "ref": "master", "status": "failed", "source": "push", "created_at": "2021-03-18T12:51:06.262Z", "updated_at": "2021-03-18T12:51:52.007Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272632767"}, "failure_reason": "script_failure", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/jobs/1108959779", "project": {"ci_job_token_scope_enabled": false}, "artifacts": [{"file_type": "trace", "size": 2182, "filename": "job.log", "file_format": null}], "runner": null, "artifacts_expire_at": null, "archived": false, "tag_list": [], "user_id": 8375961, "pipeline_id": 272632767, "runner_id": null, "commit_id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "project_id": 25157276}, "emitted_at": 1704733346935} +{"stream": "jobs", "data": {"id": 1108952832, "status": "failed", "stage": "test", "name": "test-code-job2", "ref": "ykurochkin/add-fake-CI-config", "tag": false, "coverage": null, "allow_failure": false, "created_at": "2021-03-18T12:48:49.222Z", "started_at": "2021-03-18T12:48:50.732Z", "finished_at": "2021-03-18T12:49:37.961Z", "erased_at": null, "duration": 47.229034, "queued_duration": 1.422541, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "created_at": "2021-03-10T17:13:46.589Z", "bio": "", "location": "", "public_email": "", "skype": "", "linkedin": "", "twitter": "", "discord": "", "website_url": "", "organization": "", "job_title": "", "pronouns": "", "bot": false, "work_information": null, "followers": 0, "following": 0, "local_time": "5:02 PM"}, "commit": {"id": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "short_id": "028c02d9", "created_at": "2021-03-18T14:48:41.000+02:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef"], "title": "add fake CI config", "message": "add fake CI config\n", "author_name": "ykurochkin", "author_email": "zhenia.kurochkin@gmail.com", "authored_date": "2021-03-18T14:48:41.000+02:00", "committer_name": "ykurochkin", "committer_email": "zhenia.kurochkin@gmail.com", "committed_date": "2021-03-18T14:48:41.000+02:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/028c02d96f40afe9b4d1173c1d0f712dd6d07302"}, "pipeline": {"id": 272631271, "iid": 1, "project_id": 25157276, "sha": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "ref": "ykurochkin/add-fake-CI-config", "status": "failed", "source": "push", "created_at": "2021-03-18T12:48:49.174Z", "updated_at": "2021-03-18T12:49:38.092Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272631271"}, "failure_reason": "script_failure", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/jobs/1108952832", "project": {"ci_job_token_scope_enabled": false}, "artifacts": [{"file_type": "trace", "size": 2223, "filename": "job.log", "file_format": null}], "runner": null, "artifacts_expire_at": null, "archived": false, "tag_list": [], "user_id": 8375961, "pipeline_id": 272631271, "runner_id": null, "commit_id": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "project_id": 25157276}, "emitted_at": 1704733347322} {"stream": "project_labels", "data": {"id": 19116944, "name": "Label 1", "description": null, "description_html": "", "text_color": "#1F1E24", "color": "#ffff00", "subscribed": false, "priority": null, "is_project_label": true, "project_id": 25157276}, "emitted_at": 1696950582334} {"stream": "project_labels", "data": {"id": 19117004, "name": "Label 1", "description": null, "description_html": "", "text_color": "#FFFFFF", "color": "#008000", "subscribed": false, "priority": null, "is_project_label": false, "project_id": 25157276}, "emitted_at": 1696950582334} {"stream": "project_labels", "data": {"id": 19116954, "name": "Label 10", "description": null, "description_html": "", "text_color": "#FFFFFF", "color": "#ff00ff", "subscribed": false, "priority": null, "is_project_label": true, "project_id": 25157276}, "emitted_at": 1696950582334} -{"stream": "releases", "data": {"name": "First release", "tag_name": "fake-tag-6", "description": "Test Release", "created_at": "2021-03-18T12:44:12.497Z", "released_at": "2021-03-18T12:44:12.497Z", "upcoming_release": false, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "milestones": [1943704], "commit_path": "/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef", "tag_path": "/new-group-airbute/new-ci-test-project/-/tags/fake-tag-6", "assets": {"count": 4, "sources": [{"format": "zip", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.zip"}, {"format": "tar.gz", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.tar.gz"}, {"format": "tar.bz2", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.tar.bz2"}, {"format": "tar", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.tar"}], "links": []}, "evidences": [{"sha": "a616fdca9312ca5aa451bc1060ce91a672fd24cc0f4d", "filepath": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/releases/fake-tag-6/evidences/855895.json", "collected_at": "2021-03-18T12:44:12.650Z"}], "_links": {"closed_issues_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/issues?release_tag=fake-tag-6&scope=all&state=closed", "closed_merge_requests_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests?release_tag=fake-tag-6&scope=all&state=closed", "edit_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/releases/fake-tag-6/edit", "merged_merge_requests_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests?release_tag=fake-tag-6&scope=all&state=merged", "opened_issues_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/issues?release_tag=fake-tag-6&scope=all&state=opened", "opened_merge_requests_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests?release_tag=fake-tag-6&scope=all&state=opened", "self": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/releases/fake-tag-6"}, "author_id": 8375961, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1696950910144} -{"stream": "projects", "data": {"id": 25157276, "description": "", "name": "New CI Test Project ", "name_with_namespace": "New Group Airbute / New CI Test Project ", "path": "new-ci-test-project", "path_with_namespace": "new-group-airbute/new-ci-test-project", "created_at": "2021-03-15T15:08:36.498Z", "default_branch": "master", "tag_list": [], "topics": [], "ssh_url_to_repo": "git@gitlab.com:new-group-airbute/new-ci-test-project.git", "http_url_to_repo": "https://gitlab.com/new-group-airbute/new-ci-test-project.git", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project", "readme_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/blob/master/README.md", "forks_count": 0, "avatar_url": null, "star_count": 0, "last_activity_at": "2022-12-13T09:39:47.235Z", "namespace": {"id": 11329647, "name": "New Group Airbute", "path": "new-group-airbute", "kind": "group", "full_path": "new-group-airbute", "parent_id": null, "avatar_url": null, "web_url": "https://gitlab.com/groups/new-group-airbute"}, "container_registry_image_prefix": "registry.gitlab.com/new-group-airbute/new-ci-test-project", "_links": {"self": "https://gitlab.com/api/v4/projects/25157276", "issues": "https://gitlab.com/api/v4/projects/25157276/issues", "merge_requests": "https://gitlab.com/api/v4/projects/25157276/merge_requests", "repo_branches": "https://gitlab.com/api/v4/projects/25157276/repository/branches", "labels": "https://gitlab.com/api/v4/projects/25157276/labels", "events": "https://gitlab.com/api/v4/projects/25157276/events", "members": "https://gitlab.com/api/v4/projects/25157276/members", "cluster_agents": "https://gitlab.com/api/v4/projects/25157276/cluster_agents"}, "packages_enabled": true, "empty_repo": false, "archived": false, "visibility": "private", "resolve_outdated_diff_discussions": false, "container_expiration_policy": {"cadence": "1d", "enabled": false, "keep_n": 10, "older_than": "90d", "name_regex": ".*", "name_regex_keep": null, "next_run_at": "2021-03-16T15:08:36.518Z"}, "issues_enabled": true, "merge_requests_enabled": true, "wiki_enabled": true, "jobs_enabled": true, "snippets_enabled": true, "container_registry_enabled": true, "service_desk_enabled": true, "service_desk_address": "contact-project+new-group-airbute-new-ci-test-project-25157276-issue-@incoming.gitlab.com", "can_create_merge_request_in": true, "issues_access_level": "private", "repository_access_level": "private", "merge_requests_access_level": "private", "forking_access_level": "enabled", "wiki_access_level": "enabled", "builds_access_level": "private", "snippets_access_level": "enabled", "pages_access_level": "private", "analytics_access_level": "enabled", "container_registry_access_level": "enabled", "security_and_compliance_access_level": "private", "releases_access_level": "enabled", "environments_access_level": "enabled", "feature_flags_access_level": "enabled", "infrastructure_access_level": "enabled", "monitor_access_level": "enabled", "model_experiments_access_level": "enabled", "emails_disabled": false, "emails_enabled": true, "shared_runners_enabled": true, "lfs_enabled": true, "creator_id": 8375961, "import_url": null, "import_type": null, "import_status": "none", "import_error": null, "open_issues_count": 31, "description_html": "", "updated_at": "2023-05-23T12:12:18.623Z", "ci_default_git_depth": 50, "ci_forward_deployment_enabled": true, "ci_forward_deployment_rollback_allowed": true, "ci_job_token_scope_enabled": false, "ci_separated_caches": true, "ci_allow_fork_pipelines_to_run_in_parent_project": true, "build_git_strategy": "fetch", "keep_latest_artifact": true, "restrict_user_defined_variables": false, "runners_token": "GR1348941eMJgWDU69xyyshaNsaTZ", "runner_token_expiration_interval": null, "group_runners_enabled": true, "auto_cancel_pending_pipelines": "enabled", "build_timeout": 3600, "auto_devops_enabled": false, "auto_devops_deploy_strategy": "continuous", "ci_config_path": "", "public_jobs": true, "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "allow_merge_on_skipped_pipeline": null, "request_access_enabled": true, "only_allow_merge_if_all_discussions_are_resolved": false, "remove_source_branch_after_merge": true, "printing_merge_request_link_enabled": true, "merge_method": "merge", "squash_option": "default_off", "enforce_auth_checks_on_uploads": true, "suggestion_commit_message": null, "merge_commit_template": null, "squash_commit_template": null, "issue_branch_template": null, "statistics": {"commit_count": 3, "storage_size": 9061, "repository_size": 251, "wiki_size": 0, "lfs_objects_size": 0, "job_artifacts_size": 8810, "pipeline_artifacts_size": 0, "packages_size": 0, "snippets_size": 0, "uploads_size": 0}, "autoclose_referenced_issues": true, "external_authorization_classification_label": "", "requirements_enabled": false, "requirements_access_level": "enabled", "security_and_compliance_enabled": true, "compliance_frameworks": [], "permissions": {"project_access": {"access_level": 40, "notification_level": 3}, "group_access": {"access_level": 50, "notification_level": 3}}}, "emitted_at": 1696951654063} -{"stream": "branches", "data": {"name": "31-fake-issue-30", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "merged": true, "protected": false, "developers_can_push": false, "developers_can_merge": false, "can_push": true, "default": false, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/tree/31-fake-issue-30", "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1696951865405} -{"stream": "branches", "data": {"name": "master", "commit": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98"}, "merged": false, "protected": true, "developers_can_push": false, "developers_can_merge": false, "can_push": true, "default": true, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/tree/master", "commit_id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "project_id": 25157276}, "emitted_at": 1696951865406} -{"stream": "branches", "data": {"name": "new-test-branch", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "merged": true, "protected": false, "developers_can_push": false, "developers_can_merge": false, "can_push": true, "default": false, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/tree/new-test-branch", "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1696951865406} +{"stream": "releases", "data": {"name": "First release", "tag_name": "fake-tag-6", "description": "Test Release", "created_at": "2021-03-18T12:44:12.497Z", "released_at": "2021-03-18T12:44:12.497Z", "upcoming_release": false, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "milestones": [1943704], "commit_path": "/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef", "tag_path": "/new-group-airbute/new-ci-test-project/-/tags/fake-tag-6", "assets": {"count": 4, "sources": [{"format": "zip", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.zip"}, {"format": "tar.gz", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.tar.gz"}, {"format": "tar.bz2", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.tar.bz2"}, {"format": "tar", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.tar"}], "links": []}, "evidences": [{"sha": "a616fdca9312ca5aa451bc1060ce91a672fd24cc0f4d", "filepath": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/releases/fake-tag-6/evidences/855895.json", "collected_at": "2021-03-18T12:44:12.650Z"}], "_links": {"closed_issues_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/issues?release_tag=fake-tag-6&scope=all&state=closed", "closed_merge_requests_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests?release_tag=fake-tag-6&scope=all&state=closed", "edit_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/releases/fake-tag-6/edit", "merged_merge_requests_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests?release_tag=fake-tag-6&scope=all&state=merged", "opened_issues_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/issues?release_tag=fake-tag-6&scope=all&state=opened", "opened_merge_requests_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests?release_tag=fake-tag-6&scope=all&state=opened", "self": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/releases/fake-tag-6"}, "author_id": 8375961, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1703256897835} +{"stream": "projects", "data": {"id": 25157276, "description": "", "name": "New CI Test Project ", "name_with_namespace": "New Group Airbute / New CI Test Project ", "path": "new-ci-test-project", "path_with_namespace": "new-group-airbute/new-ci-test-project", "created_at": "2021-03-15T15:08:36.498Z", "default_branch": "master", "tag_list": [], "topics": [], "ssh_url_to_repo": "git@gitlab.com:new-group-airbute/new-ci-test-project.git", "http_url_to_repo": "https://gitlab.com/new-group-airbute/new-ci-test-project.git", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project", "readme_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/blob/master/README.md", "forks_count": 0, "avatar_url": null, "star_count": 0, "last_activity_at": "2022-12-13T09:39:47.235Z", "namespace": {"id": 11329647, "name": "New Group Airbute", "path": "new-group-airbute", "kind": "group", "full_path": "new-group-airbute", "parent_id": null, "avatar_url": null, "web_url": "https://gitlab.com/groups/new-group-airbute"}, "container_registry_image_prefix": "registry.gitlab.com/new-group-airbute/new-ci-test-project", "_links": {"self": "https://gitlab.com/api/v4/projects/25157276", "issues": "https://gitlab.com/api/v4/projects/25157276/issues", "merge_requests": "https://gitlab.com/api/v4/projects/25157276/merge_requests", "repo_branches": "https://gitlab.com/api/v4/projects/25157276/repository/branches", "labels": "https://gitlab.com/api/v4/projects/25157276/labels", "events": "https://gitlab.com/api/v4/projects/25157276/events", "members": "https://gitlab.com/api/v4/projects/25157276/members", "cluster_agents": "https://gitlab.com/api/v4/projects/25157276/cluster_agents"}, "code_suggestions": true, "packages_enabled": true, "empty_repo": false, "archived": false, "visibility": "private", "resolve_outdated_diff_discussions": false, "container_expiration_policy": {"cadence": "1d", "enabled": false, "keep_n": 10, "older_than": "90d", "name_regex": ".*", "name_regex_keep": null, "next_run_at": "2021-03-16T15:08:36.518Z"}, "issues_enabled": true, "merge_requests_enabled": true, "wiki_enabled": true, "jobs_enabled": true, "snippets_enabled": true, "container_registry_enabled": true, "service_desk_enabled": true, "service_desk_address": "contact-project+new-group-airbute-new-ci-test-project-25157276-issue-@incoming.gitlab.com", "can_create_merge_request_in": true, "issues_access_level": "private", "repository_access_level": "private", "merge_requests_access_level": "private", "forking_access_level": "enabled", "wiki_access_level": "enabled", "builds_access_level": "private", "snippets_access_level": "enabled", "pages_access_level": "private", "analytics_access_level": "enabled", "container_registry_access_level": "enabled", "security_and_compliance_access_level": "private", "releases_access_level": "enabled", "environments_access_level": "enabled", "feature_flags_access_level": "enabled", "infrastructure_access_level": "enabled", "monitor_access_level": "enabled", "model_experiments_access_level": "enabled", "model_registry_access_level": "enabled", "emails_disabled": false, "emails_enabled": true, "shared_runners_enabled": true, "lfs_enabled": true, "creator_id": 8375961, "import_url": null, "import_type": null, "import_status": "none", "import_error": null, "open_issues_count": 31, "description_html": "", "updated_at": "2023-05-23T12:12:18.623Z", "ci_default_git_depth": 50, "ci_forward_deployment_enabled": true, "ci_forward_deployment_rollback_allowed": true, "ci_job_token_scope_enabled": false, "ci_separated_caches": true, "ci_allow_fork_pipelines_to_run_in_parent_project": true, "build_git_strategy": "fetch", "keep_latest_artifact": true, "restrict_user_defined_variables": false, "runners_token": "GR1348941eMJgWDU69xyyshaNsaTZ", "runner_token_expiration_interval": null, "group_runners_enabled": true, "auto_cancel_pending_pipelines": "enabled", "build_timeout": 3600, "auto_devops_enabled": false, "auto_devops_deploy_strategy": "continuous", "ci_config_path": "", "public_jobs": true, "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "allow_merge_on_skipped_pipeline": null, "request_access_enabled": true, "only_allow_merge_if_all_discussions_are_resolved": false, "remove_source_branch_after_merge": true, "printing_merge_request_link_enabled": true, "merge_method": "merge", "squash_option": "default_off", "enforce_auth_checks_on_uploads": true, "suggestion_commit_message": null, "merge_commit_template": null, "squash_commit_template": null, "issue_branch_template": null, "statistics": {"commit_count": 3, "storage_size": 9061, "repository_size": 251, "wiki_size": 0, "lfs_objects_size": 0, "job_artifacts_size": 8810, "pipeline_artifacts_size": 0, "packages_size": 0, "snippets_size": 0, "uploads_size": 0}, "autoclose_referenced_issues": true, "external_authorization_classification_label": "", "requirements_enabled": false, "requirements_access_level": "enabled", "security_and_compliance_enabled": true, "compliance_frameworks": [], "permissions": {"project_access": {"access_level": 40, "notification_level": 3}, "group_access": {"access_level": 50, "notification_level": 3}}}, "emitted_at": 1703257466384} +{"stream": "branches", "data": {"name": "31-fake-issue-30", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "merged": true, "protected": false, "developers_can_push": false, "developers_can_merge": false, "can_push": true, "default": false, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/tree/31-fake-issue-30", "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1703257845052} +{"stream": "branches", "data": {"name": "master", "commit": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98"}, "merged": false, "protected": true, "developers_can_push": false, "developers_can_merge": false, "can_push": true, "default": true, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/tree/master", "commit_id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "project_id": 25157276}, "emitted_at": 1703257845053} +{"stream": "branches", "data": {"name": "new-test-branch", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "merged": true, "protected": false, "developers_can_push": false, "developers_can_merge": false, "can_push": true, "default": false, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/tree/new-test-branch", "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1703257845054} {"stream": "merge_request_commits", "data": {"id": 92594931, "iid": 3, "project_id": 25157276, "title": "add fake CI config", "description": "", "state": "merged", "created_at": "2021-03-18T12:49:13.091Z", "updated_at": "2021-03-18T12:51:06.319Z", "merged_by": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "merge_user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "merged_at": "2021-03-18T12:51:06.470Z", "closed_by": null, "closed_at": null, "target_branch": "master", "source_branch": "ykurochkin/add-fake-CI-config", "user_notes_count": 0, "upvotes": 0, "downvotes": 0, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "assignees": [], "assignee": null, "reviewers": [], "source_project_id": 25157276, "target_project_id": 25157276, "labels": [], "draft": false, "work_in_progress": false, "milestone": null, "merge_when_pipeline_succeeds": false, "merge_status": "can_be_merged", "detailed_merge_status": "not_open", "sha": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "merge_commit_sha": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "squash_commit_sha": null, "discussion_locked": null, "should_remove_source_branch": null, "force_remove_source_branch": true, "prepared_at": "2021-03-18T12:49:13.091Z", "reference": "!3", "references": {"short": "!3", "relative": "!3", "full": "new-group-airbute/new-ci-test-project!3"}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests/3", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "squash": false, "squash_on_merge": false, "task_completion_status": {"count": 0, "completed_count": 0}, "has_conflicts": false, "blocking_discussions_resolved": true, "approvals_before_merge": null, "subscribed": true, "changes_count": "1", "latest_build_started_at": null, "latest_build_finished_at": null, "first_deployed_to_production_at": null, "pipeline": null, "head_pipeline": {"id": 272631271, "iid": 1, "project_id": 25157276, "sha": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "ref": "ykurochkin/add-fake-CI-config", "status": "failed", "source": "push", "created_at": "2021-03-18T12:48:49.174Z", "updated_at": "2021-03-18T12:49:38.092Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272631271", "before_sha": "0000000000000000000000000000000000000000", "tag": false, "yaml_errors": null, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "started_at": "2021-03-18T12:48:50.166Z", "finished_at": "2021-03-18T12:49:38.084Z", "committed_at": null, "duration": 47, "queued_duration": null, "coverage": null, "detailed_status": {"icon": "status_failed", "text": "Failed", "label": "failed", "group": "failed", "tooltip": "failed", "has_details": true, "details_path": "/new-group-airbute/new-ci-test-project/-/pipelines/272631271", "illustration": null, "favicon": "/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"}}, "diff_refs": {"base_sha": "2831d897ba0214f8d3168647e8ad4232b83987ef", "head_sha": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "start_sha": "2831d897ba0214f8d3168647e8ad4232b83987ef"}, "merge_error": null, "first_contribution": false, "user": {"can_merge": true}, "merge_request_iid": 3}, "emitted_at": 1696952086155} {"stream": "merge_request_commits", "data": {"id": 92593913, "iid": 2, "project_id": 25157276, "title": "update readme.md", "description": "", "state": "opened", "created_at": "2021-03-18T12:42:30.200Z", "updated_at": "2021-03-18T12:42:30.200Z", "merged_by": null, "merge_user": null, "merged_at": null, "closed_by": null, "closed_at": null, "target_branch": "master", "source_branch": "ykurochkin/test-branch", "user_notes_count": 0, "upvotes": 0, "downvotes": 0, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "assignees": [], "assignee": null, "reviewers": [], "source_project_id": 25157276, "target_project_id": 25157276, "labels": [], "draft": false, "work_in_progress": false, "milestone": null, "merge_when_pipeline_succeeds": false, "merge_status": "can_be_merged", "detailed_merge_status": "mergeable", "sha": "9b0c5cf345f0ca1a3fb3ae253e74e0616abf8129", "merge_commit_sha": null, "squash_commit_sha": null, "discussion_locked": null, "should_remove_source_branch": null, "force_remove_source_branch": true, "prepared_at": "2021-03-18T12:42:30.200Z", "reference": "!2", "references": {"short": "!2", "relative": "!2", "full": "new-group-airbute/new-ci-test-project!2"}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests/2", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "squash": false, "squash_on_merge": false, "task_completion_status": {"count": 0, "completed_count": 0}, "has_conflicts": false, "blocking_discussions_resolved": true, "approvals_before_merge": null, "subscribed": true, "changes_count": "1", "latest_build_started_at": null, "latest_build_finished_at": null, "first_deployed_to_production_at": null, "pipeline": null, "head_pipeline": null, "diff_refs": {"base_sha": "2831d897ba0214f8d3168647e8ad4232b83987ef", "head_sha": "9b0c5cf345f0ca1a3fb3ae253e74e0616abf8129", "start_sha": "2831d897ba0214f8d3168647e8ad4232b83987ef"}, "merge_error": null, "first_contribution": false, "user": {"can_merge": true}, "merge_request_iid": 2}, "emitted_at": 1696952086460} {"stream": "merge_request_commits", "data": {"id": 92111504, "iid": 1, "project_id": 25157276, "title": "Draft: Resolve \"Fake Issue 30\"", "description": "Closes #31", "state": "opened", "created_at": "2021-03-15T16:08:05.071Z", "updated_at": "2021-03-15T16:08:05.071Z", "merged_by": null, "merge_user": null, "merged_at": null, "closed_by": null, "closed_at": null, "target_branch": "master", "source_branch": "31-fake-issue-30", "user_notes_count": 0, "upvotes": 0, "downvotes": 0, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "assignees": [{"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}], "assignee": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "reviewers": [], "source_project_id": 25157276, "target_project_id": 25157276, "labels": ["bug"], "draft": true, "work_in_progress": true, "milestone": null, "merge_when_pipeline_succeeds": false, "merge_status": "cannot_be_merged", "detailed_merge_status": "draft_status", "sha": "2831d897ba0214f8d3168647e8ad4232b83987ef", "merge_commit_sha": null, "squash_commit_sha": null, "discussion_locked": null, "should_remove_source_branch": null, "force_remove_source_branch": true, "prepared_at": "2021-03-15T16:08:05.071Z", "reference": "!1", "references": {"short": "!1", "relative": "!1", "full": "new-group-airbute/new-ci-test-project!1"}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests/1", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "squash": false, "squash_on_merge": false, "task_completion_status": {"count": 0, "completed_count": 0}, "has_conflicts": true, "blocking_discussions_resolved": true, "approvals_before_merge": null, "subscribed": true, "changes_count": null, "latest_build_started_at": null, "latest_build_finished_at": null, "first_deployed_to_production_at": null, "pipeline": null, "head_pipeline": null, "diff_refs": {"base_sha": "2831d897ba0214f8d3168647e8ad4232b83987ef", "head_sha": "2831d897ba0214f8d3168647e8ad4232b83987ef", "start_sha": "2831d897ba0214f8d3168647e8ad4232b83987ef"}, "merge_error": null, "first_contribution": false, "user": {"can_merge": true}, "merge_request_iid": 1}, "emitted_at": 1696952086890} @@ -48,7 +48,7 @@ {"stream": "group_members", "data": {"access_level": 50, "created_at": "2021-03-15T15:55:53.658Z", "expires_at": null, "id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "membership_state": "active", "group_id": 11329647}, "emitted_at": 1696952386414} {"stream": "group_members", "data": {"access_level": 30, "created_at": "2021-03-15T15:55:53.998Z", "created_by": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "expires_at": null, "id": 7904355, "username": "y.kurochkin", "name": "Yevhenii Kurochkin", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/760fcac88680c724a6b19c6bfd5b6718?s=80&d=identicon", "web_url": "https://gitlab.com/y.kurochkin", "membership_state": "active", "group_id": 11329647}, "emitted_at": 1696952386416} {"stream": "group_members", "data": {"access_level": 50, "created_at": "2022-12-02T08:46:22.834Z", "expires_at": null, "id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "membership_state": "active", "group_id": 61014882}, "emitted_at": 1696952387022} -{"stream": "tags", "data": {"name": "fake-tag-1", "message": "", "target": "2831d897ba0214f8d3168647e8ad4232b83987ef", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "release": null, "protected": false, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1686568185586} -{"stream": "tags", "data": {"name": "fake-tag-10", "message": "", "target": "2831d897ba0214f8d3168647e8ad4232b83987ef", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "release": null, "protected": false, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1686568185588} -{"stream": "tags", "data": {"name": "fake-tag-11", "message": "", "target": "2831d897ba0214f8d3168647e8ad4232b83987ef", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "release": null, "protected": false, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1686568185590} +{"stream": "tags", "data": {"name": "fake-tag-1", "message": "", "target": "2831d897ba0214f8d3168647e8ad4232b83987ef", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "release": null, "protected": false, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1703258228301} +{"stream": "tags", "data": {"name": "fake-tag-10", "message": "", "target": "2831d897ba0214f8d3168647e8ad4232b83987ef", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "release": null, "protected": false, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1703258228301} +{"stream": "tags", "data": {"name": "fake-tag-11", "message": "", "target": "2831d897ba0214f8d3168647e8ad4232b83987ef", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "release": null, "protected": false, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1703258228301} {"stream": "deployments", "data": {"id": 568087366, "iid": 1, "ref": "master", "sha": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "created_at": "2023-10-10T09:56:02.273Z", "updated_at": "2023-10-10T09:56:02.273Z", "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "environment": {"id": 17305239, "name": "dev", "slug": "dev", "external_url": null, "created_at": "2023-10-10T09:56:02.188Z", "updated_at": "2023-10-10T09:56:02.188Z"}, "deployable": null, "status": "failed", "user_id": 8375961, "environment_id": 17305239, "user_username": "airbyte", "user_full_name": "Airbyte Team", "environment_name": "dev", "project_id": 25157276}, "emitted_at": 1696931771902} diff --git a/airbyte-integrations/connectors/source-gitlab/integration_tests/expected_records_with_ids.jsonl b/airbyte-integrations/connectors/source-gitlab/integration_tests/expected_records_with_ids.jsonl index 320db38aa3a13..69c1ed5a15d0f 100644 --- a/airbyte-integrations/connectors/source-gitlab/integration_tests/expected_records_with_ids.jsonl +++ b/airbyte-integrations/connectors/source-gitlab/integration_tests/expected_records_with_ids.jsonl @@ -1,9 +1,9 @@ {"stream": "pipelines", "data": {"id": 272632767, "iid": 2, "project_id": 25157276, "sha": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "ref": "master", "status": "failed", "source": "push", "created_at": "2021-03-18T12:51:06.262Z", "updated_at": "2021-03-18T12:51:52.007Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272632767", "name": null}, "emitted_at": 1686567225920} {"stream": "pipelines", "data": {"id": 272631271, "iid": 1, "project_id": 25157276, "sha": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "ref": "ykurochkin/add-fake-CI-config", "status": "failed", "source": "push", "created_at": "2021-03-18T12:48:49.174Z", "updated_at": "2021-03-18T12:49:38.092Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272631271", "name": null}, "emitted_at": 1686567225922} -{"stream": "releases", "data": {"name": "First release", "tag_name": "fake-tag-6", "description": "Test Release", "created_at": "2021-03-18T12:44:12.497Z", "released_at": "2021-03-18T12:44:12.497Z", "upcoming_release": false, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "milestones": [1943704], "commit_path": "/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef", "tag_path": "/new-group-airbute/new-ci-test-project/-/tags/fake-tag-6", "assets": {"count": 4, "sources": [{"format": "zip", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.zip"}, {"format": "tar.gz", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.tar.gz"}, {"format": "tar.bz2", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.tar.bz2"}, {"format": "tar", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.tar"}], "links": []}, "evidences": [{"sha": "a616fdca9312ca5aa451bc1060ce91a672fd24cc0f4d", "filepath": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/releases/fake-tag-6/evidences/855895.json", "collected_at": "2021-03-18T12:44:12.650Z"}], "_links": {"closed_issues_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/issues?release_tag=fake-tag-6&scope=all&state=closed", "closed_merge_requests_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests?release_tag=fake-tag-6&scope=all&state=closed", "edit_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/releases/fake-tag-6/edit", "merged_merge_requests_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests?release_tag=fake-tag-6&scope=all&state=merged", "opened_issues_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/issues?release_tag=fake-tag-6&scope=all&state=opened", "opened_merge_requests_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests?release_tag=fake-tag-6&scope=all&state=opened", "self": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/releases/fake-tag-6"}, "author_id": 8375961, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1696947713101} -{"stream": "jobs", "data": {"id": 1108959782, "status": "failed", "stage": "test", "name": "test-code-job2", "ref": "master", "tag": false, "coverage": null, "allow_failure": false, "created_at": "2021-03-18T12:51:06.294Z", "started_at": "2021-03-18T12:51:07.646Z", "finished_at": "2021-03-18T12:51:51.309Z", "erased_at": null, "duration": 43.662407, "queued_duration": 1.180926, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "created_at": "2021-03-10T17:13:46.589Z", "bio": "", "location": "", "public_email": "", "skype": "", "linkedin": "", "twitter": "", "discord": "", "website_url": "", "organization": "", "job_title": "", "pronouns": "", "bot": false, "work_information": null, "followers": 0, "following": 0, "local_time": "10:53 AM"}, "commit": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98"}, "pipeline": {"id": 272632767, "iid": 2, "project_id": 25157276, "sha": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "ref": "master", "status": "failed", "source": "push", "created_at": "2021-03-18T12:51:06.262Z", "updated_at": "2021-03-18T12:51:52.007Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272632767"}, "failure_reason": "script_failure", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/jobs/1108959782", "project": {"ci_job_token_scope_enabled": false}, "artifacts": [{"file_type": "trace", "size": 2200, "filename": "job.log", "file_format": null}], "runner": null, "artifacts_expire_at": null, "tag_list": [], "user_id": 8375961, "pipeline_id": 272632767, "runner_id": null, "commit_id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "project_id": 25157276}, "emitted_at": 1686567192490} -{"stream": "jobs", "data": {"id": 1108959779, "status": "failed", "stage": "test", "name": "test-code-job1", "ref": "master", "tag": false, "coverage": null, "allow_failure": false, "created_at": "2021-03-18T12:51:06.279Z", "started_at": "2021-03-18T12:51:07.943Z", "finished_at": "2021-03-18T12:51:50.943Z", "erased_at": null, "duration": 42.999853, "queued_duration": 1.349274, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "created_at": "2021-03-10T17:13:46.589Z", "bio": "", "location": "", "public_email": "", "skype": "", "linkedin": "", "twitter": "", "discord": "", "website_url": "", "organization": "", "job_title": "", "pronouns": "", "bot": false, "work_information": null, "followers": 0, "following": 0, "local_time": "10:53 AM"}, "commit": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98"}, "pipeline": {"id": 272632767, "iid": 2, "project_id": 25157276, "sha": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "ref": "master", "status": "failed", "source": "push", "created_at": "2021-03-18T12:51:06.262Z", "updated_at": "2021-03-18T12:51:52.007Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272632767"}, "failure_reason": "script_failure", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/jobs/1108959779", "project": {"ci_job_token_scope_enabled": false}, "artifacts": [{"file_type": "trace", "size": 2182, "filename": "job.log", "file_format": null}], "runner": null, "artifacts_expire_at": null, "tag_list": [], "user_id": 8375961, "pipeline_id": 272632767, "runner_id": null, "commit_id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "project_id": 25157276}, "emitted_at": 1686567192491} -{"stream": "jobs", "data": {"id": 1108952832, "status": "failed", "stage": "test", "name": "test-code-job2", "ref": "ykurochkin/add-fake-CI-config", "tag": false, "coverage": null, "allow_failure": false, "created_at": "2021-03-18T12:48:49.222Z", "started_at": "2021-03-18T12:48:50.732Z", "finished_at": "2021-03-18T12:49:37.961Z", "erased_at": null, "duration": 47.229034, "queued_duration": 1.422541, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "created_at": "2021-03-10T17:13:46.589Z", "bio": "", "location": "", "public_email": "", "skype": "", "linkedin": "", "twitter": "", "discord": "", "website_url": "", "organization": "", "job_title": "", "pronouns": "", "bot": false, "work_information": null, "followers": 0, "following": 0, "local_time": "10:53 AM"}, "commit": {"id": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "short_id": "028c02d9", "created_at": "2021-03-18T14:48:41.000+02:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef"], "title": "add fake CI config", "message": "add fake CI config\n", "author_name": "ykurochkin", "author_email": "zhenia.kurochkin@gmail.com", "authored_date": "2021-03-18T14:48:41.000+02:00", "committer_name": "ykurochkin", "committer_email": "zhenia.kurochkin@gmail.com", "committed_date": "2021-03-18T14:48:41.000+02:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/028c02d96f40afe9b4d1173c1d0f712dd6d07302"}, "pipeline": {"id": 272631271, "iid": 1, "project_id": 25157276, "sha": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "ref": "ykurochkin/add-fake-CI-config", "status": "failed", "source": "push", "created_at": "2021-03-18T12:48:49.174Z", "updated_at": "2021-03-18T12:49:38.092Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272631271"}, "failure_reason": "script_failure", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/jobs/1108952832", "project": {"ci_job_token_scope_enabled": false}, "artifacts": [{"file_type": "trace", "size": 2223, "filename": "job.log", "file_format": null}], "runner": null, "artifacts_expire_at": null, "tag_list": [], "user_id": 8375961, "pipeline_id": 272631271, "runner_id": null, "commit_id": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "project_id": 25157276}, "emitted_at": 1686567192861} +{"stream": "releases", "data": {"name": "First release", "tag_name": "fake-tag-6", "description": "Test Release", "created_at": "2021-03-18T12:44:12.497Z", "released_at": "2021-03-18T12:44:12.497Z", "upcoming_release": false, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "milestones": [1943704], "commit_path": "/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef", "tag_path": "/new-group-airbute/new-ci-test-project/-/tags/fake-tag-6", "assets": {"count": 4, "sources": [{"format": "zip", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.zip"}, {"format": "tar.gz", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.tar.gz"}, {"format": "tar.bz2", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.tar.bz2"}, {"format": "tar", "url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/archive/fake-tag-6/new-ci-test-project-fake-tag-6.tar"}], "links": []}, "evidences": [{"sha": "a616fdca9312ca5aa451bc1060ce91a672fd24cc0f4d", "filepath": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/releases/fake-tag-6/evidences/855895.json", "collected_at": "2021-03-18T12:44:12.650Z"}], "_links": {"closed_issues_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/issues?release_tag=fake-tag-6&scope=all&state=closed", "closed_merge_requests_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests?release_tag=fake-tag-6&scope=all&state=closed", "edit_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/releases/fake-tag-6/edit", "merged_merge_requests_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests?release_tag=fake-tag-6&scope=all&state=merged", "opened_issues_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/issues?release_tag=fake-tag-6&scope=all&state=opened", "opened_merge_requests_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests?release_tag=fake-tag-6&scope=all&state=opened", "self": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/releases/fake-tag-6"}, "author_id": 8375961, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1696947713101} +{"stream": "jobs", "data": {"id": 1108959782, "status": "failed", "stage": "test", "name": "test-code-job2", "ref": "master", "tag": false, "coverage": null, "allow_failure": false, "created_at": "2021-03-18T12:51:06.294Z", "started_at": "2021-03-18T12:51:07.646Z", "finished_at": "2021-03-18T12:51:51.309Z", "erased_at": null, "duration": 43.662407, "queued_duration": 1.180926, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "created_at": "2021-03-10T17:13:46.589Z", "bio": "", "location": "", "public_email": "", "skype": "", "linkedin": "", "twitter": "", "discord": "", "website_url": "", "organization": "", "job_title": "", "pronouns": "", "bot": false, "work_information": null, "followers": 0, "following": 0, "local_time": "4:51 PM"}, "commit": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98"}, "pipeline": {"id": 272632767, "iid": 2, "project_id": 25157276, "sha": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "ref": "master", "status": "failed", "source": "push", "created_at": "2021-03-18T12:51:06.262Z", "updated_at": "2021-03-18T12:51:52.007Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272632767"}, "failure_reason": "script_failure", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/jobs/1108959782", "project": {"ci_job_token_scope_enabled": false}, "artifacts": [{"file_type": "trace", "size": 2200, "filename": "job.log", "file_format": null}], "runner": null, "artifacts_expire_at": null, "archived": false, "tag_list": [], "user_id": 8375961, "pipeline_id": 272632767, "runner_id": null, "commit_id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "project_id": 25157276}, "emitted_at": 1704732685403} +{"stream": "jobs", "data": {"id": 1108959779, "status": "failed", "stage": "test", "name": "test-code-job1", "ref": "master", "tag": false, "coverage": null, "allow_failure": false, "created_at": "2021-03-18T12:51:06.279Z", "started_at": "2021-03-18T12:51:07.943Z", "finished_at": "2021-03-18T12:51:50.943Z", "erased_at": null, "duration": 42.999853, "queued_duration": 1.349274, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "created_at": "2021-03-10T17:13:46.589Z", "bio": "", "location": "", "public_email": "", "skype": "", "linkedin": "", "twitter": "", "discord": "", "website_url": "", "organization": "", "job_title": "", "pronouns": "", "bot": false, "work_information": null, "followers": 0, "following": 0, "local_time": "4:51 PM"}, "commit": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98"}, "pipeline": {"id": 272632767, "iid": 2, "project_id": 25157276, "sha": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "ref": "master", "status": "failed", "source": "push", "created_at": "2021-03-18T12:51:06.262Z", "updated_at": "2021-03-18T12:51:52.007Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272632767"}, "failure_reason": "script_failure", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/jobs/1108959779", "project": {"ci_job_token_scope_enabled": false}, "artifacts": [{"file_type": "trace", "size": 2182, "filename": "job.log", "file_format": null}], "runner": null, "artifacts_expire_at": null, "archived": false, "tag_list": [], "user_id": 8375961, "pipeline_id": 272632767, "runner_id": null, "commit_id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "project_id": 25157276}, "emitted_at": 1704732685404} +{"stream": "jobs", "data": {"id": 1108952832, "status": "failed", "stage": "test", "name": "test-code-job2", "ref": "ykurochkin/add-fake-CI-config", "tag": false, "coverage": null, "allow_failure": false, "created_at": "2021-03-18T12:48:49.222Z", "started_at": "2021-03-18T12:48:50.732Z", "finished_at": "2021-03-18T12:49:37.961Z", "erased_at": null, "duration": 47.229034, "queued_duration": 1.422541, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "created_at": "2021-03-10T17:13:46.589Z", "bio": "", "location": "", "public_email": "", "skype": "", "linkedin": "", "twitter": "", "discord": "", "website_url": "", "organization": "", "job_title": "", "pronouns": "", "bot": false, "work_information": null, "followers": 0, "following": 0, "local_time": "4:51 PM"}, "commit": {"id": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "short_id": "028c02d9", "created_at": "2021-03-18T14:48:41.000+02:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef"], "title": "add fake CI config", "message": "add fake CI config\n", "author_name": "ykurochkin", "author_email": "zhenia.kurochkin@gmail.com", "authored_date": "2021-03-18T14:48:41.000+02:00", "committer_name": "ykurochkin", "committer_email": "zhenia.kurochkin@gmail.com", "committed_date": "2021-03-18T14:48:41.000+02:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/028c02d96f40afe9b4d1173c1d0f712dd6d07302"}, "pipeline": {"id": 272631271, "iid": 1, "project_id": 25157276, "sha": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "ref": "ykurochkin/add-fake-CI-config", "status": "failed", "source": "push", "created_at": "2021-03-18T12:48:49.174Z", "updated_at": "2021-03-18T12:49:38.092Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272631271"}, "failure_reason": "script_failure", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/jobs/1108952832", "project": {"ci_job_token_scope_enabled": false}, "artifacts": [{"file_type": "trace", "size": 2223, "filename": "job.log", "file_format": null}], "runner": null, "artifacts_expire_at": null, "archived": false, "tag_list": [], "user_id": 8375961, "pipeline_id": 272631271, "runner_id": null, "commit_id": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "project_id": 25157276}, "emitted_at": 1704732685727} {"stream": "merge_request_commits", "data": {"id": 92594931, "iid": 3, "project_id": 25157276, "title": "add fake CI config", "description": "", "state": "merged", "created_at": "2021-03-18T12:49:13.091Z", "updated_at": "2021-03-18T12:51:06.319Z", "merged_by": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "merge_user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "merged_at": "2021-03-18T12:51:06.470Z", "closed_by": null, "closed_at": null, "target_branch": "master", "source_branch": "ykurochkin/add-fake-CI-config", "user_notes_count": 0, "upvotes": 0, "downvotes": 0, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "assignees": [], "assignee": null, "reviewers": [], "source_project_id": 25157276, "target_project_id": 25157276, "labels": [], "draft": false, "work_in_progress": false, "milestone": null, "merge_when_pipeline_succeeds": false, "merge_status": "can_be_merged", "detailed_merge_status": "not_open", "sha": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "merge_commit_sha": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "squash_commit_sha": null, "discussion_locked": null, "should_remove_source_branch": null, "force_remove_source_branch": true, "prepared_at": "2021-03-18T12:49:13.091Z", "reference": "!3", "references": {"short": "!3", "relative": "!3", "full": "new-group-airbute/new-ci-test-project!3"}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests/3", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "squash": false, "squash_on_merge": false, "task_completion_status": {"count": 0, "completed_count": 0}, "has_conflicts": false, "blocking_discussions_resolved": true, "approvals_before_merge": null, "subscribed": true, "changes_count": "1", "latest_build_started_at": null, "latest_build_finished_at": null, "first_deployed_to_production_at": null, "pipeline": null, "head_pipeline": {"id": 272631271, "iid": 1, "project_id": 25157276, "sha": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "ref": "ykurochkin/add-fake-CI-config", "status": "failed", "source": "push", "created_at": "2021-03-18T12:48:49.174Z", "updated_at": "2021-03-18T12:49:38.092Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272631271", "before_sha": "0000000000000000000000000000000000000000", "tag": false, "yaml_errors": null, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "started_at": "2021-03-18T12:48:50.166Z", "finished_at": "2021-03-18T12:49:38.084Z", "committed_at": null, "duration": 47, "queued_duration": null, "coverage": null, "detailed_status": {"icon": "status_failed", "text": "Failed", "label": "failed", "group": "failed", "tooltip": "failed", "has_details": true, "details_path": "/new-group-airbute/new-ci-test-project/-/pipelines/272631271", "illustration": null, "favicon": "/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"}}, "diff_refs": {"base_sha": "2831d897ba0214f8d3168647e8ad4232b83987ef", "head_sha": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "start_sha": "2831d897ba0214f8d3168647e8ad4232b83987ef"}, "merge_error": null, "first_contribution": false, "user": {"can_merge": true}, "merge_request_iid": 3}, "emitted_at": 1696948297499} {"stream": "merge_request_commits", "data": {"id": 92593913, "iid": 2, "project_id": 25157276, "title": "update readme.md", "description": "", "state": "opened", "created_at": "2021-03-18T12:42:30.200Z", "updated_at": "2021-03-18T12:42:30.200Z", "merged_by": null, "merge_user": null, "merged_at": null, "closed_by": null, "closed_at": null, "target_branch": "master", "source_branch": "ykurochkin/test-branch", "user_notes_count": 0, "upvotes": 0, "downvotes": 0, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "assignees": [], "assignee": null, "reviewers": [], "source_project_id": 25157276, "target_project_id": 25157276, "labels": [], "draft": false, "work_in_progress": false, "milestone": null, "merge_when_pipeline_succeeds": false, "merge_status": "can_be_merged", "detailed_merge_status": "mergeable", "sha": "9b0c5cf345f0ca1a3fb3ae253e74e0616abf8129", "merge_commit_sha": null, "squash_commit_sha": null, "discussion_locked": null, "should_remove_source_branch": null, "force_remove_source_branch": true, "prepared_at": "2021-03-18T12:42:30.200Z", "reference": "!2", "references": {"short": "!2", "relative": "!2", "full": "new-group-airbute/new-ci-test-project!2"}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests/2", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "squash": false, "squash_on_merge": false, "task_completion_status": {"count": 0, "completed_count": 0}, "has_conflicts": false, "blocking_discussions_resolved": true, "approvals_before_merge": null, "subscribed": true, "changes_count": "1", "latest_build_started_at": null, "latest_build_finished_at": null, "first_deployed_to_production_at": null, "pipeline": null, "head_pipeline": null, "diff_refs": {"base_sha": "2831d897ba0214f8d3168647e8ad4232b83987ef", "head_sha": "9b0c5cf345f0ca1a3fb3ae253e74e0616abf8129", "start_sha": "2831d897ba0214f8d3168647e8ad4232b83987ef"}, "merge_error": null, "first_contribution": false, "user": {"can_merge": true}, "merge_request_iid": 2}, "emitted_at": 1696948297896} {"stream": "merge_request_commits", "data": {"id": 92111504, "iid": 1, "project_id": 25157276, "title": "Draft: Resolve \"Fake Issue 30\"", "description": "Closes #31", "state": "opened", "created_at": "2021-03-15T16:08:05.071Z", "updated_at": "2021-03-15T16:08:05.071Z", "merged_by": null, "merge_user": null, "merged_at": null, "closed_by": null, "closed_at": null, "target_branch": "master", "source_branch": "31-fake-issue-30", "user_notes_count": 0, "upvotes": 0, "downvotes": 0, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "assignees": [{"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}], "assignee": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "reviewers": [], "source_project_id": 25157276, "target_project_id": 25157276, "labels": ["bug"], "draft": true, "work_in_progress": true, "milestone": null, "merge_when_pipeline_succeeds": false, "merge_status": "cannot_be_merged", "detailed_merge_status": "draft_status", "sha": "2831d897ba0214f8d3168647e8ad4232b83987ef", "merge_commit_sha": null, "squash_commit_sha": null, "discussion_locked": null, "should_remove_source_branch": null, "force_remove_source_branch": true, "prepared_at": "2021-03-15T16:08:05.071Z", "reference": "!1", "references": {"short": "!1", "relative": "!1", "full": "new-group-airbute/new-ci-test-project!1"}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests/1", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "squash": false, "squash_on_merge": false, "task_completion_status": {"count": 0, "completed_count": 0}, "has_conflicts": true, "blocking_discussions_resolved": true, "approvals_before_merge": null, "subscribed": true, "changes_count": null, "latest_build_started_at": null, "latest_build_finished_at": null, "first_deployed_to_production_at": null, "pipeline": null, "head_pipeline": null, "diff_refs": {"base_sha": "2831d897ba0214f8d3168647e8ad4232b83987ef", "head_sha": "2831d897ba0214f8d3168647e8ad4232b83987ef", "start_sha": "2831d897ba0214f8d3168647e8ad4232b83987ef"}, "merge_error": null, "first_contribution": false, "user": {"can_merge": true}, "merge_request_iid": 1}, "emitted_at": 1696948298305} @@ -14,26 +14,26 @@ {"stream": "pipelines_extended", "data": {"id": 272631271, "iid": 1, "project_id": 25157276, "sha": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "ref": "ykurochkin/add-fake-CI-config", "status": "failed", "source": "push", "created_at": "2021-03-18T12:48:49.174Z", "updated_at": "2021-03-18T12:49:38.092Z", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/pipelines/272631271", "before_sha": "0000000000000000000000000000000000000000", "tag": false, "yaml_errors": null, "user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "started_at": "2021-03-18T12:48:50.166Z", "finished_at": "2021-03-18T12:49:38.084Z", "committed_at": null, "duration": 47, "queued_duration": null, "coverage": null, "detailed_status": {"icon": "status_failed", "text": "Failed", "label": "failed", "group": "failed", "tooltip": "failed", "has_details": false, "details_path": "/new-group-airbute/new-ci-test-project/-/pipelines/272631271", "illustration": null, "favicon": "/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"}, "name": null}, "emitted_at": 1696948628851} {"stream": "users", "data": {"id": 7904355, "username": "y.kurochkin", "name": "Yevhenii Kurochkin", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/760fcac88680c724a6b19c6bfd5b6718?s=80&d=identicon", "web_url": "https://gitlab.com/y.kurochkin"}, "emitted_at": 1696948873593} {"stream": "users", "data": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "emitted_at": 1696948873594} -{"stream": "groups", "data": {"id": 11329647, "web_url": "https://gitlab.com/groups/new-group-airbute", "name": "New Group Airbute", "path": "new-group-airbute", "description": "", "visibility": "public", "share_with_group_lock": false, "require_two_factor_authentication": false, "two_factor_grace_period": 48, "project_creation_level": "developer", "auto_devops_enabled": null, "subgroup_creation_level": "maintainer", "emails_disabled": null, "mentions_disabled": null, "lfs_enabled": true, "default_branch_protection": 2, "default_branch_protection_defaults": {"allowed_to_push": [{"access_level": 30}], "allow_force_push": true, "allowed_to_merge": [{"access_level": 30}]}, "avatar_url": null, "request_access_enabled": true, "full_name": "New Group Airbute", "full_path": "new-group-airbute", "created_at": "2021-03-15T15:55:53.613Z", "parent_id": null, "shared_runners_setting": "enabled", "ldap_cn": null, "ldap_access": null, "wiki_access_level": "enabled", "shared_with_groups": [], "runners_token": "GR1348941-PhosPap-Sf1UxL1g6m4", "prevent_sharing_groups_outside_hierarchy": false, "shared_projects": [], "shared_runners_minutes_limit": null, "extra_shared_runners_minutes_limit": null, "prevent_forking_outside_group": null, "membership_lock": false, "projects": [{"id": 25157276, "path_with_namespace": "new-group-airbute/new-ci-test-project"}]}, "emitted_at": 1696949138497} -{"stream": "groups", "data": {"id": 61014882, "web_url": "https://gitlab.com/groups/new-group-airbute/test-subgroup-airbyte/test-private-sg", "name": "Test Private SG", "path": "test-private-sg", "description": "", "visibility": "private", "share_with_group_lock": false, "require_two_factor_authentication": false, "two_factor_grace_period": 48, "project_creation_level": "developer", "auto_devops_enabled": null, "subgroup_creation_level": "maintainer", "emails_disabled": null, "mentions_disabled": null, "lfs_enabled": true, "default_branch_protection": 2, "default_branch_protection_defaults": {"allowed_to_push": [{"access_level": 30}], "allow_force_push": true, "allowed_to_merge": [{"access_level": 30}]}, "avatar_url": null, "request_access_enabled": true, "full_name": "New Group Airbute / Test Subgroup Airbyte / Test Private SG", "full_path": "new-group-airbute/test-subgroup-airbyte/test-private-sg", "created_at": "2022-12-02T08:46:22.648Z", "parent_id": 61014863, "shared_runners_setting": "enabled", "ldap_cn": null, "ldap_access": null, "wiki_access_level": "enabled", "shared_with_groups": [], "runners_token": "GR1348941bjUaJQy2zzar-JmNBjfq", "shared_projects": [], "shared_runners_minutes_limit": null, "extra_shared_runners_minutes_limit": null, "prevent_forking_outside_group": null, "membership_lock": false, "projects": []}, "emitted_at": 1696949138806} -{"stream": "groups", "data": {"id": 61015181, "web_url": "https://gitlab.com/groups/new-group-airbute/test-public-sg/test-sg-public-2/test-private-subsubg-1", "name": "Test Private SubSubG 1", "path": "test-private-subsubg-1", "description": "", "visibility": "private", "share_with_group_lock": false, "require_two_factor_authentication": false, "two_factor_grace_period": 48, "project_creation_level": "developer", "auto_devops_enabled": null, "subgroup_creation_level": "maintainer", "emails_disabled": null, "mentions_disabled": null, "lfs_enabled": true, "default_branch_protection": 2, "default_branch_protection_defaults": {"allowed_to_push": [{"access_level": 30}], "allow_force_push": true, "allowed_to_merge": [{"access_level": 30}]}, "avatar_url": null, "request_access_enabled": true, "full_name": "New Group Airbute / Test Public SG / Test SG Public 2 / Test Private SubSubG 1", "full_path": "new-group-airbute/test-public-sg/test-sg-public-2/test-private-subsubg-1", "created_at": "2022-12-02T08:54:42.252Z", "parent_id": 61014943, "shared_runners_setting": "enabled", "ldap_cn": null, "ldap_access": null, "wiki_access_level": "enabled", "shared_with_groups": [], "runners_token": "GR1348941x8xQf6K-UvnnyJ-bcut4", "shared_projects": [], "shared_runners_minutes_limit": null, "extra_shared_runners_minutes_limit": null, "prevent_forking_outside_group": null, "membership_lock": false, "projects": [{"id": 41551658, "path_with_namespace": "new-group-airbute/test-public-sg/test-sg-public-2/test-private-subsubg-1/test_project_in_nested_subgroup"}]}, "emitted_at": 1696949139214} +{"stream": "groups", "data": {"id": 11266951, "web_url": "https://gitlab.com/groups/airbyte.io", "name": "airbyte.io", "path": "airbyte.io", "description": "", "visibility": "private", "share_with_group_lock": false, "require_two_factor_authentication": false, "two_factor_grace_period": 48, "project_creation_level": "developer", "auto_devops_enabled": null, "subgroup_creation_level": "maintainer", "emails_disabled": false, "emails_enabled": true, "mentions_disabled": null, "lfs_enabled": true, "default_branch_protection": 2, "default_branch_protection_defaults": {"allowed_to_push": [{"access_level": 30}], "allow_force_push": true, "allowed_to_merge": [{"access_level": 30}]}, "avatar_url": null, "request_access_enabled": true, "full_name": "airbyte.io", "full_path": "airbyte.io", "created_at": "2021-03-10T17:16:37.549Z", "parent_id": null, "organization_id": 1, "shared_runners_setting": "enabled", "ldap_cn": null, "ldap_access": null, "marked_for_deletion_on": null, "wiki_access_level": "enabled", "shared_with_groups": [], "runners_token": "GR1348941bzmDjXx-Cz48snUcJfK8", "prevent_sharing_groups_outside_hierarchy": false, "shared_projects": [], "shared_runners_minutes_limit": 10000, "extra_shared_runners_minutes_limit": null, "prevent_forking_outside_group": false, "service_access_tokens_expiration_enforced": true, "membership_lock": false, "ip_restriction_ranges": null, "projects": [{"id": 25156633, "path_with_namespace": "airbyte.io/ci-test-project"}, {"id": 25032440, "path_with_namespace": "airbyte.io/learn-gitlab"}, {"id": 25032439, "path_with_namespace": "airbyte.io/documentation"}]}, "emitted_at": 1704733464696} +{"stream": "groups", "data": {"id": 11329647, "web_url": "https://gitlab.com/groups/new-group-airbute", "name": "New Group Airbute", "path": "new-group-airbute", "description": "", "visibility": "public", "share_with_group_lock": false, "require_two_factor_authentication": false, "two_factor_grace_period": 48, "project_creation_level": "developer", "auto_devops_enabled": null, "subgroup_creation_level": "maintainer", "emails_disabled": false, "emails_enabled": true, "mentions_disabled": null, "lfs_enabled": true, "default_branch_protection": 2, "default_branch_protection_defaults": {"allowed_to_push": [{"access_level": 30}], "allow_force_push": true, "allowed_to_merge": [{"access_level": 30}]}, "avatar_url": null, "request_access_enabled": true, "full_name": "New Group Airbute", "full_path": "new-group-airbute", "created_at": "2021-03-15T15:55:53.613Z", "parent_id": null, "organization_id": 1, "shared_runners_setting": "enabled", "ldap_cn": null, "ldap_access": null, "wiki_access_level": "enabled", "shared_with_groups": [], "runners_token": "GR1348941-PhosPap-Sf1UxL1g6m4", "prevent_sharing_groups_outside_hierarchy": false, "shared_projects": [], "shared_runners_minutes_limit": null, "extra_shared_runners_minutes_limit": null, "prevent_forking_outside_group": null, "membership_lock": false, "projects": [{"id": 25157276, "path_with_namespace": "new-group-airbute/new-ci-test-project"}]}, "emitted_at": 1704733465104} +{"stream": "groups", "data": {"id": 61014882, "web_url": "https://gitlab.com/groups/new-group-airbute/test-subgroup-airbyte/test-private-sg", "name": "Test Private SG", "path": "test-private-sg", "description": "", "visibility": "private", "share_with_group_lock": false, "require_two_factor_authentication": false, "two_factor_grace_period": 48, "project_creation_level": "developer", "auto_devops_enabled": null, "subgroup_creation_level": "maintainer", "emails_disabled": false, "emails_enabled": true, "mentions_disabled": null, "lfs_enabled": true, "default_branch_protection": 2, "default_branch_protection_defaults": {"allowed_to_push": [{"access_level": 30}], "allow_force_push": true, "allowed_to_merge": [{"access_level": 30}]}, "avatar_url": null, "request_access_enabled": true, "full_name": "New Group Airbute / Test Subgroup Airbyte / Test Private SG", "full_path": "new-group-airbute/test-subgroup-airbyte/test-private-sg", "created_at": "2022-12-02T08:46:22.648Z", "parent_id": 61014863, "organization_id": 1, "shared_runners_setting": "enabled", "ldap_cn": null, "ldap_access": null, "wiki_access_level": "enabled", "shared_with_groups": [], "runners_token": "GR1348941bjUaJQy2zzar-JmNBjfq", "shared_projects": [], "shared_runners_minutes_limit": null, "extra_shared_runners_minutes_limit": null, "prevent_forking_outside_group": null, "membership_lock": false, "projects": []}, "emitted_at": 1704733465409} {"stream": "group_labels", "data": {"id": 19117004, "name": "Label 1", "description": null, "description_html": "", "text_color": "#FFFFFF", "color": "#008000", "subscribed": false, "group_id": 11329647}, "emitted_at": 1696949435261} {"stream": "group_labels", "data": {"id": 19117017, "name": "Label 10", "description": null, "description_html": "", "text_color": "#FFFFFF", "color": "#000080", "subscribed": false, "group_id": 11329647}, "emitted_at": 1696949435263} {"stream": "group_labels", "data": {"id": 19117018, "name": "Label 11", "description": null, "description_html": "", "text_color": "#FFFFFF", "color": "#808080", "subscribed": false, "group_id": 11329647}, "emitted_at": 1696949435264} {"stream": "group_members", "data": {"access_level": 50, "created_at": "2021-03-15T15:55:53.658Z", "expires_at": null, "id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "membership_state": "active", "group_id": 11329647}, "emitted_at": 1696949993328} {"stream": "group_members", "data": {"access_level": 30, "created_at": "2021-03-15T15:55:53.998Z", "created_by": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "expires_at": null, "id": 7904355, "username": "y.kurochkin", "name": "Yevhenii Kurochkin", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/760fcac88680c724a6b19c6bfd5b6718?s=80&d=identicon", "web_url": "https://gitlab.com/y.kurochkin", "membership_state": "active", "group_id": 11329647}, "emitted_at": 1696949993329} {"stream": "group_members", "data": {"access_level": 50, "created_at": "2022-12-02T08:46:22.834Z", "expires_at": null, "id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte", "membership_state": "active", "group_id": 61014882}, "emitted_at": 1696949993941} -{"stream": "branches", "data": {"name": "31-fake-issue-30", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "merged": true, "protected": false, "developers_can_push": false, "developers_can_merge": false, "can_push": true, "default": false, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/tree/31-fake-issue-30", "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1686567183576} -{"stream": "branches", "data": {"name": "master", "commit": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98"}, "merged": false, "protected": true, "developers_can_push": false, "developers_can_merge": false, "can_push": true, "default": true, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/tree/master", "commit_id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "project_id": 25157276}, "emitted_at": 1686567183576} -{"stream": "branches", "data": {"name": "new-test-branch", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "merged": true, "protected": false, "developers_can_push": false, "developers_can_merge": false, "can_push": true, "default": false, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/tree/new-test-branch", "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1686567183577} -{"stream": "commits", "data": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98", "stats": {"additions": 14, "deletions": 0, "total": 14}, "project_id": 25157276}, "emitted_at": 1686567184540} -{"stream": "commits", "data": {"id": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "short_id": "028c02d9", "created_at": "2021-03-18T14:48:41.000+02:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef"], "title": "add fake CI config", "message": "add fake CI config\n", "author_name": "ykurochkin", "author_email": "zhenia.kurochkin@gmail.com", "authored_date": "2021-03-18T14:48:41.000+02:00", "committer_name": "ykurochkin", "committer_email": "zhenia.kurochkin@gmail.com", "committed_date": "2021-03-18T14:48:41.000+02:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/028c02d96f40afe9b4d1173c1d0f712dd6d07302", "stats": {"additions": 14, "deletions": 0, "total": 14}, "project_id": 25157276}, "emitted_at": 1686567184541} -{"stream": "commits", "data": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef", "stats": {"additions": 2, "deletions": 0, "total": 2}, "project_id": 25157276}, "emitted_at": 1686567184541} +{"stream": "branches", "data": {"name": "31-fake-issue-30", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "merged": true, "protected": false, "developers_can_push": false, "developers_can_merge": false, "can_push": true, "default": false, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/tree/31-fake-issue-30", "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1703257027266} +{"stream": "branches", "data": {"name": "master", "commit": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98"}, "merged": false, "protected": true, "developers_can_push": false, "developers_can_merge": false, "can_push": true, "default": true, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/tree/master", "commit_id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "project_id": 25157276}, "emitted_at": 1703257027267} +{"stream": "branches", "data": {"name": "new-test-branch", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "merged": true, "protected": false, "developers_can_push": false, "developers_can_merge": false, "can_push": true, "default": false, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/tree/new-test-branch", "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1703257027267} +{"stream": "commits", "data": {"id": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "short_id": "6ad3dd49", "created_at": "2021-03-18T12:51:05.000+00:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef", "028c02d96f40afe9b4d1173c1d0f712dd6d07302"], "title": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'", "message": "Merge branch 'ykurochkin/add-fake-CI-config' into 'master'\n\nadd fake CI config\n\nSee merge request new-group-airbute/new-ci-test-project!3", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-18T12:51:05.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-18T12:51:05.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/6ad3dd49539391774db738c9e7b7d69f2d872c98", "stats": {"additions": 14, "deletions": 0, "total": 14}, "project_id": 25157276}, "emitted_at": 1703257635545} +{"stream": "commits", "data": {"id": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "short_id": "028c02d9", "created_at": "2021-03-18T14:48:41.000+02:00", "parent_ids": ["2831d897ba0214f8d3168647e8ad4232b83987ef"], "title": "add fake CI config", "message": "add fake CI config\n", "author_name": "ykurochkin", "author_email": "zhenia.kurochkin@gmail.com", "authored_date": "2021-03-18T14:48:41.000+02:00", "committer_name": "ykurochkin", "committer_email": "zhenia.kurochkin@gmail.com", "committed_date": "2021-03-18T14:48:41.000+02:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/028c02d96f40afe9b4d1173c1d0f712dd6d07302", "stats": {"additions": 14, "deletions": 0, "total": 14}, "project_id": 25157276}, "emitted_at": 1703257635547} +{"stream": "commits", "data": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef", "stats": {"additions": 2, "deletions": 0, "total": 2}, "project_id": 25157276}, "emitted_at": 1703257635548} {"stream": "group_issue_boards", "data": {"id": 5099065, "name": "Development", "hide_backlog_list": false, "hide_closed_list": false, "project": null, "lists": [], "group": {"id": 11329647, "web_url": "https://gitlab.com/groups/new-group-airbute", "name": "New Group Airbute"}, "group_id": 11329647}, "emitted_at": 1686567186609} -{"stream": "projects", "data": {"id": 25157276, "description": "", "name": "New CI Test Project ", "name_with_namespace": "New Group Airbute / New CI Test Project ", "path": "new-ci-test-project", "path_with_namespace": "new-group-airbute/new-ci-test-project", "created_at": "2021-03-15T15:08:36.498Z", "default_branch": "master", "tag_list": [], "topics": [], "ssh_url_to_repo": "git@gitlab.com:new-group-airbute/new-ci-test-project.git", "http_url_to_repo": "https://gitlab.com/new-group-airbute/new-ci-test-project.git", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project", "readme_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/blob/master/README.md", "forks_count": 0, "avatar_url": null, "star_count": 0, "last_activity_at": "2022-12-13T09:39:47.235Z", "namespace": {"id": 11329647, "name": "New Group Airbute", "path": "new-group-airbute", "kind": "group", "full_path": "new-group-airbute", "parent_id": null, "avatar_url": null, "web_url": "https://gitlab.com/groups/new-group-airbute"}, "container_registry_image_prefix": "registry.gitlab.com/new-group-airbute/new-ci-test-project", "_links": {"self": "https://gitlab.com/api/v4/projects/25157276", "issues": "https://gitlab.com/api/v4/projects/25157276/issues", "merge_requests": "https://gitlab.com/api/v4/projects/25157276/merge_requests", "repo_branches": "https://gitlab.com/api/v4/projects/25157276/repository/branches", "labels": "https://gitlab.com/api/v4/projects/25157276/labels", "events": "https://gitlab.com/api/v4/projects/25157276/events", "members": "https://gitlab.com/api/v4/projects/25157276/members", "cluster_agents": "https://gitlab.com/api/v4/projects/25157276/cluster_agents"}, "packages_enabled": true, "empty_repo": false, "archived": false, "visibility": "private", "resolve_outdated_diff_discussions": false, "container_expiration_policy": {"cadence": "1d", "enabled": false, "keep_n": 10, "older_than": "90d", "name_regex": ".*", "name_regex_keep": null, "next_run_at": "2021-03-16T15:08:36.518Z"}, "issues_enabled": true, "merge_requests_enabled": true, "wiki_enabled": true, "jobs_enabled": true, "snippets_enabled": true, "container_registry_enabled": true, "service_desk_enabled": true, "service_desk_address": "contact-project+new-group-airbute-new-ci-test-project-25157276-issue-@incoming.gitlab.com", "can_create_merge_request_in": true, "issues_access_level": "private", "repository_access_level": "private", "merge_requests_access_level": "private", "forking_access_level": "enabled", "wiki_access_level": "enabled", "builds_access_level": "private", "snippets_access_level": "enabled", "pages_access_level": "private", "analytics_access_level": "enabled", "container_registry_access_level": "enabled", "security_and_compliance_access_level": "private", "releases_access_level": "enabled", "environments_access_level": "enabled", "feature_flags_access_level": "enabled", "infrastructure_access_level": "enabled", "monitor_access_level": "enabled", "model_experiments_access_level": "enabled", "emails_disabled": false, "emails_enabled": true, "shared_runners_enabled": true, "lfs_enabled": true, "creator_id": 8375961, "import_url": null, "import_type": null, "import_status": "none", "import_error": null, "open_issues_count": 31, "description_html": "", "updated_at": "2023-05-23T12:12:18.623Z", "ci_default_git_depth": 50, "ci_forward_deployment_enabled": true, "ci_forward_deployment_rollback_allowed": true, "ci_job_token_scope_enabled": false, "ci_separated_caches": true, "ci_allow_fork_pipelines_to_run_in_parent_project": true, "build_git_strategy": "fetch", "keep_latest_artifact": true, "restrict_user_defined_variables": false, "runners_token": "GR1348941eMJgWDU69xyyshaNsaTZ", "runner_token_expiration_interval": null, "group_runners_enabled": true, "auto_cancel_pending_pipelines": "enabled", "build_timeout": 3600, "auto_devops_enabled": false, "auto_devops_deploy_strategy": "continuous", "ci_config_path": "", "public_jobs": true, "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "allow_merge_on_skipped_pipeline": null, "request_access_enabled": true, "only_allow_merge_if_all_discussions_are_resolved": false, "remove_source_branch_after_merge": true, "printing_merge_request_link_enabled": true, "merge_method": "merge", "squash_option": "default_off", "enforce_auth_checks_on_uploads": true, "suggestion_commit_message": null, "merge_commit_template": null, "squash_commit_template": null, "issue_branch_template": null, "statistics": {"commit_count": 3, "storage_size": 9061, "repository_size": 251, "wiki_size": 0, "lfs_objects_size": 0, "job_artifacts_size": 8810, "pipeline_artifacts_size": 0, "packages_size": 0, "snippets_size": 0, "uploads_size": 0}, "autoclose_referenced_issues": true, "external_authorization_classification_label": "", "requirements_enabled": false, "requirements_access_level": "enabled", "security_and_compliance_enabled": true, "compliance_frameworks": [], "permissions": {"project_access": {"access_level": 40, "notification_level": 3}, "group_access": {"access_level": 50, "notification_level": 3}}}, "emitted_at": 1696950432789} -{"stream": "tags", "data": {"name": "fake-tag-1", "message": "", "target": "2831d897ba0214f8d3168647e8ad4232b83987ef", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "release": null, "protected": false, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1686567225240} -{"stream": "tags", "data": {"name": "fake-tag-10", "message": "", "target": "2831d897ba0214f8d3168647e8ad4232b83987ef", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "release": null, "protected": false, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1686567225242} -{"stream": "tags", "data": {"name": "fake-tag-11", "message": "", "target": "2831d897ba0214f8d3168647e8ad4232b83987ef", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "release": null, "protected": false, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1686567225243} +{"stream": "projects", "data": {"id": 25157276, "description": "", "name": "New CI Test Project ", "name_with_namespace": "New Group Airbute / New CI Test Project ", "path": "new-ci-test-project", "path_with_namespace": "new-group-airbute/new-ci-test-project", "created_at": "2021-03-15T15:08:36.498Z", "default_branch": "master", "tag_list": [], "topics": [], "ssh_url_to_repo": "git@gitlab.com:new-group-airbute/new-ci-test-project.git", "http_url_to_repo": "https://gitlab.com/new-group-airbute/new-ci-test-project.git", "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project", "readme_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/blob/master/README.md", "forks_count": 0, "avatar_url": null, "star_count": 0, "last_activity_at": "2022-12-13T09:39:47.235Z", "namespace": {"id": 11329647, "name": "New Group Airbute", "path": "new-group-airbute", "kind": "group", "full_path": "new-group-airbute", "parent_id": null, "avatar_url": null, "web_url": "https://gitlab.com/groups/new-group-airbute"}, "container_registry_image_prefix": "registry.gitlab.com/new-group-airbute/new-ci-test-project", "_links": {"self": "https://gitlab.com/api/v4/projects/25157276", "issues": "https://gitlab.com/api/v4/projects/25157276/issues", "merge_requests": "https://gitlab.com/api/v4/projects/25157276/merge_requests", "repo_branches": "https://gitlab.com/api/v4/projects/25157276/repository/branches", "labels": "https://gitlab.com/api/v4/projects/25157276/labels", "events": "https://gitlab.com/api/v4/projects/25157276/events", "members": "https://gitlab.com/api/v4/projects/25157276/members", "cluster_agents": "https://gitlab.com/api/v4/projects/25157276/cluster_agents"}, "code_suggestions": true, "packages_enabled": true, "empty_repo": false, "archived": false, "visibility": "private", "resolve_outdated_diff_discussions": false, "container_expiration_policy": {"cadence": "1d", "enabled": false, "keep_n": 10, "older_than": "90d", "name_regex": ".*", "name_regex_keep": null, "next_run_at": "2021-03-16T15:08:36.518Z"}, "issues_enabled": true, "merge_requests_enabled": true, "wiki_enabled": true, "jobs_enabled": true, "snippets_enabled": true, "container_registry_enabled": true, "service_desk_enabled": true, "service_desk_address": "contact-project+new-group-airbute-new-ci-test-project-25157276-issue-@incoming.gitlab.com", "can_create_merge_request_in": true, "issues_access_level": "private", "repository_access_level": "private", "merge_requests_access_level": "private", "forking_access_level": "enabled", "wiki_access_level": "enabled", "builds_access_level": "private", "snippets_access_level": "enabled", "pages_access_level": "private", "analytics_access_level": "enabled", "container_registry_access_level": "enabled", "security_and_compliance_access_level": "private", "releases_access_level": "enabled", "environments_access_level": "enabled", "feature_flags_access_level": "enabled", "infrastructure_access_level": "enabled", "monitor_access_level": "enabled", "model_experiments_access_level": "enabled", "model_registry_access_level": "enabled", "emails_disabled": false, "emails_enabled": true, "shared_runners_enabled": true, "lfs_enabled": true, "creator_id": 8375961, "import_url": null, "import_type": null, "import_status": "none", "import_error": null, "open_issues_count": 31, "description_html": "", "updated_at": "2023-05-23T12:12:18.623Z", "ci_default_git_depth": 50, "ci_forward_deployment_enabled": true, "ci_forward_deployment_rollback_allowed": true, "ci_job_token_scope_enabled": false, "ci_separated_caches": true, "ci_allow_fork_pipelines_to_run_in_parent_project": true, "build_git_strategy": "fetch", "keep_latest_artifact": true, "restrict_user_defined_variables": false, "runners_token": "GR1348941eMJgWDU69xyyshaNsaTZ", "runner_token_expiration_interval": null, "group_runners_enabled": true, "auto_cancel_pending_pipelines": "enabled", "build_timeout": 3600, "auto_devops_enabled": false, "auto_devops_deploy_strategy": "continuous", "ci_config_path": "", "public_jobs": true, "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "allow_merge_on_skipped_pipeline": null, "request_access_enabled": true, "only_allow_merge_if_all_discussions_are_resolved": false, "remove_source_branch_after_merge": true, "printing_merge_request_link_enabled": true, "merge_method": "merge", "squash_option": "default_off", "enforce_auth_checks_on_uploads": true, "suggestion_commit_message": null, "merge_commit_template": null, "squash_commit_template": null, "issue_branch_template": null, "statistics": {"commit_count": 3, "storage_size": 9061, "repository_size": 251, "wiki_size": 0, "lfs_objects_size": 0, "job_artifacts_size": 8810, "pipeline_artifacts_size": 0, "packages_size": 0, "snippets_size": 0, "uploads_size": 0}, "autoclose_referenced_issues": true, "external_authorization_classification_label": "", "requirements_enabled": false, "requirements_access_level": "enabled", "security_and_compliance_enabled": true, "compliance_frameworks": [], "permissions": {"project_access": {"access_level": 40, "notification_level": 3}, "group_access": {"access_level": 50, "notification_level": 3}}}, "emitted_at": 1703258006161} +{"stream": "tags", "data": {"name": "fake-tag-1", "message": "", "target": "2831d897ba0214f8d3168647e8ad4232b83987ef", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "release": null, "protected": false, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1703258326525} +{"stream": "tags", "data": {"name": "fake-tag-10", "message": "", "target": "2831d897ba0214f8d3168647e8ad4232b83987ef", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "release": null, "protected": false, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1703258326527} +{"stream": "tags", "data": {"name": "fake-tag-11", "message": "", "target": "2831d897ba0214f8d3168647e8ad4232b83987ef", "commit": {"id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "short_id": "2831d897", "created_at": "2021-03-15T15:08:36.000+00:00", "parent_ids": [], "title": "Initial commit", "message": "Initial commit", "author_name": "Alexander Arhipenko", "author_email": "integration-test@airbyte.io", "authored_date": "2021-03-15T15:08:36.000+00:00", "committer_name": "Alexander Arhipenko", "committer_email": "integration-test@airbyte.io", "committed_date": "2021-03-15T15:08:36.000+00:00", "trailers": {}, "extended_trailers": {}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/commit/2831d897ba0214f8d3168647e8ad4232b83987ef"}, "release": null, "protected": false, "commit_id": "2831d897ba0214f8d3168647e8ad4232b83987ef", "project_id": 25157276}, "emitted_at": 1703258326528} {"stream": "merge_requests", "data": {"id": 92594931, "iid": 3, "project_id": 25157276, "title": "add fake CI config", "description": "", "state": "merged", "created_at": "2021-03-18T12:49:13.091Z", "updated_at": "2021-03-18T12:51:06.319Z", "merged_by": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "merge_user": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "merged_at": "2021-03-18T12:51:06.470Z", "closed_by": null, "closed_at": null, "target_branch": "master", "source_branch": "ykurochkin/add-fake-CI-config", "user_notes_count": 0, "upvotes": 0, "downvotes": 0, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "assignees": [], "assignee": null, "reviewers": [], "source_project_id": 25157276, "target_project_id": 25157276, "labels": [], "draft": false, "work_in_progress": false, "milestone": null, "merge_when_pipeline_succeeds": false, "merge_status": "can_be_merged", "detailed_merge_status": "not_open", "sha": "028c02d96f40afe9b4d1173c1d0f712dd6d07302", "merge_commit_sha": "6ad3dd49539391774db738c9e7b7d69f2d872c98", "squash_commit_sha": null, "discussion_locked": null, "should_remove_source_branch": null, "force_remove_source_branch": true, "prepared_at": "2021-03-18T12:49:13.091Z", "reference": "!3", "references": {"short": "!3", "relative": "!3", "full": "new-group-airbute/new-ci-test-project!3"}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests/3", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "squash": false, "squash_on_merge": false, "task_completion_status": {"count": 0, "completed_count": 0}, "has_conflicts": false, "blocking_discussions_resolved": true, "approvals_before_merge": null, "author_id": 8375961, "assignee_id": null, "closed_by_id": null, "milestone_id": null, "merged_by_id": 8375961}, "emitted_at": 1696950689861} {"stream": "merge_requests", "data": {"id": 92593913, "iid": 2, "project_id": 25157276, "title": "update readme.md", "description": "", "state": "opened", "created_at": "2021-03-18T12:42:30.200Z", "updated_at": "2021-03-18T12:42:30.200Z", "merged_by": null, "merge_user": null, "merged_at": null, "closed_by": null, "closed_at": null, "target_branch": "master", "source_branch": "ykurochkin/test-branch", "user_notes_count": 0, "upvotes": 0, "downvotes": 0, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "assignees": [], "assignee": null, "reviewers": [], "source_project_id": 25157276, "target_project_id": 25157276, "labels": [], "draft": false, "work_in_progress": false, "milestone": null, "merge_when_pipeline_succeeds": false, "merge_status": "can_be_merged", "detailed_merge_status": "mergeable", "sha": "9b0c5cf345f0ca1a3fb3ae253e74e0616abf8129", "merge_commit_sha": null, "squash_commit_sha": null, "discussion_locked": null, "should_remove_source_branch": null, "force_remove_source_branch": true, "prepared_at": "2021-03-18T12:42:30.200Z", "reference": "!2", "references": {"short": "!2", "relative": "!2", "full": "new-group-airbute/new-ci-test-project!2"}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests/2", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "squash": false, "squash_on_merge": false, "task_completion_status": {"count": 0, "completed_count": 0}, "has_conflicts": false, "blocking_discussions_resolved": true, "approvals_before_merge": null, "author_id": 8375961, "assignee_id": null, "closed_by_id": null, "milestone_id": null, "merged_by_id": null}, "emitted_at": 1696950689864} {"stream": "merge_requests", "data": {"id": 92111504, "iid": 1, "project_id": 25157276, "title": "Draft: Resolve \"Fake Issue 30\"", "description": "Closes #31", "state": "opened", "created_at": "2021-03-15T16:08:05.071Z", "updated_at": "2021-03-15T16:08:05.071Z", "merged_by": null, "merge_user": null, "merged_at": null, "closed_by": null, "closed_at": null, "target_branch": "master", "source_branch": "31-fake-issue-30", "user_notes_count": 0, "upvotes": 0, "downvotes": 0, "author": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "assignees": [8375961], "assignee": {"id": 8375961, "username": "airbyte", "name": "Airbyte Team", "state": "active", "locked": false, "avatar_url": "https://secure.gravatar.com/avatar/0a7841feac7218131ce7b427283c24ef?s=80&d=identicon", "web_url": "https://gitlab.com/airbyte"}, "reviewers": [], "source_project_id": 25157276, "target_project_id": 25157276, "labels": ["bug"], "draft": true, "work_in_progress": true, "milestone": null, "merge_when_pipeline_succeeds": false, "merge_status": "cannot_be_merged", "detailed_merge_status": "draft_status", "sha": "2831d897ba0214f8d3168647e8ad4232b83987ef", "merge_commit_sha": null, "squash_commit_sha": null, "discussion_locked": null, "should_remove_source_branch": null, "force_remove_source_branch": true, "prepared_at": "2021-03-15T16:08:05.071Z", "reference": "!1", "references": {"short": "!1", "relative": "!1", "full": "new-group-airbute/new-ci-test-project!1"}, "web_url": "https://gitlab.com/new-group-airbute/new-ci-test-project/-/merge_requests/1", "time_stats": {"time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null}, "squash": false, "squash_on_merge": false, "task_completion_status": {"count": 0, "completed_count": 0}, "has_conflicts": true, "blocking_discussions_resolved": true, "approvals_before_merge": null, "author_id": 8375961, "assignee_id": 8375961, "closed_by_id": null, "milestone_id": null, "merged_by_id": null}, "emitted_at": 1696950689866} diff --git a/airbyte-integrations/connectors/source-gitlab/metadata.yaml b/airbyte-integrations/connectors/source-gitlab/metadata.yaml index c4015a62d8ee7..e79748b9025d6 100644 --- a/airbyte-integrations/connectors/source-gitlab/metadata.yaml +++ b/airbyte-integrations/connectors/source-gitlab/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: 5e6175e5-68e1-4c17-bff9-56103bbb0d80 - dockerImageTag: 2.0.0 + dockerImageTag: 2.1.0 dockerRepository: airbyte/source-gitlab documentationUrl: https://docs.airbyte.com/integrations/sources/gitlab githubIssueLabel: source-gitlab diff --git a/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/commits.json b/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/commits.json index 89a7d2f5ae313..55b6809a6683a 100644 --- a/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/commits.json +++ b/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/commits.json @@ -37,6 +37,17 @@ "type": ["null", "string"], "format": "date-time" }, + "extended_trailers": { + "type": ["null", "object"], + "properties": { + "Cc": { + "type": ["null", "array"], + "items": { + "type": ["null", "string"] + } + } + } + }, "committer_name": { "type": ["null", "string"] }, diff --git a/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/groups.json b/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/groups.json index be75d5230d672..b04cb5cbb15d0 100644 --- a/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/groups.json +++ b/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/groups.json @@ -19,6 +19,9 @@ "id": { "type": ["null", "integer"] }, + "organization_id": { + "type": ["null", "integer"] + }, "default_branch_protection_defaults": { "type": ["null", "object"], "properties": { @@ -74,6 +77,9 @@ "emails_disabled": { "type": ["null", "boolean"] }, + "emails_enabled": { + "type": ["null", "boolean"] + }, "mentions_disabled": { "type": ["null", "boolean"] }, @@ -144,6 +150,9 @@ }, "shared_runners_setting": { "type": ["null", "string"] + }, + "service_access_tokens_expiration_enforced": { + "type": ["null", "boolean"] } } } diff --git a/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/issues.json b/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/issues.json index 4ec4e6e0aace1..11583cdad76e7 100644 --- a/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/issues.json +++ b/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/issues.json @@ -235,20 +235,23 @@ } }, "epic": { - "id": { - "type": ["null", "integer"] - }, - "iid": { - "type": ["null", "integer"] - }, - "title": { - "type": ["null", "string"] - }, - "url": { - "type": ["null", "string"] - }, - "group_id": { - "type": ["null", "integer"] + "type": ["null", "object"], + "properties": { + "id": { + "type": ["null", "integer"] + }, + "iid": { + "type": ["null", "integer"] + }, + "title": { + "type": ["null", "string"] + }, + "url": { + "type": ["null", "string"] + }, + "group_id": { + "type": ["null", "integer"] + } } }, "epic_iid": { diff --git a/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/jobs.json b/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/jobs.json index 4c41e56b46a54..00fb3b5d6aada 100644 --- a/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/jobs.json +++ b/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/jobs.json @@ -11,6 +11,9 @@ "stage": { "type": ["null", "string"] }, + "archived": { + "type": ["null", "boolean"] + }, "name": { "type": ["null", "string"] }, diff --git a/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/projects.json b/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/projects.json index 21c5bc9abf8e5..e86a72556e05e 100644 --- a/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/projects.json +++ b/airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/projects.json @@ -483,6 +483,15 @@ }, "merge_trains_skip_train_allowed": { "type": ["null", "boolean"] + }, + "code_suggestions": { + "type": ["null", "boolean"] + }, + "model_registry_access_level": { + "type": ["null", "string"] + }, + "ci_restrict_pipeline_cancellation_role": { + "type": ["null", "string"] } } } diff --git a/airbyte-integrations/connectors/source-gitlab/source_gitlab/streams.py b/airbyte-integrations/connectors/source-gitlab/source_gitlab/streams.py index d561009f13bcc..d1f14ca59eef1 100644 --- a/airbyte-integrations/connectors/source-gitlab/source_gitlab/streams.py +++ b/airbyte-integrations/connectors/source-gitlab/source_gitlab/streams.py @@ -92,7 +92,7 @@ def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapp elif isinstance(response_data, dict): yield self.transform(response_data, **kwargs) else: - Exception(f"Unsupported type of response data for stream {self.name}") + self.logger.info(f"Unsupported type of response data for stream {self.name}") def transform(self, record: Dict[str, Any], stream_slice: Mapping[str, Any] = None, **kwargs): for key in self.flatten_id_keys: @@ -166,7 +166,7 @@ def get_updated_state(self, current_stream_state: MutableMapping[str, Any], late current_state = current_state.get(self.cursor_field) current_state_value = current_state or latest_cursor_value max_value = max(pendulum.parse(current_state_value), pendulum.parse(latest_cursor_value)) - current_stream_state[str(project_id)] = {self.cursor_field: str(max_value)} + current_stream_state[str(project_id)] = {self.cursor_field: max_value.to_iso8601_string()} return current_stream_state @staticmethod diff --git a/airbyte-integrations/connectors/source-gitlab/unit_tests/test_config.json b/airbyte-integrations/connectors/source-gitlab/unit_tests/test_config.json new file mode 100644 index 0000000000000..71f30753dc6e2 --- /dev/null +++ b/airbyte-integrations/connectors/source-gitlab/unit_tests/test_config.json @@ -0,0 +1 @@ +{ "groups": "a b c", "groups_list": ["a", "c", "b"] } diff --git a/airbyte-integrations/connectors/source-gitlab/unit_tests/test_config_migrations.py b/airbyte-integrations/connectors/source-gitlab/unit_tests/test_config_migrations.py new file mode 100644 index 0000000000000..b61fb232906d2 --- /dev/null +++ b/airbyte-integrations/connectors/source-gitlab/unit_tests/test_config_migrations.py @@ -0,0 +1,21 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import os + +from source_gitlab.config_migrations import MigrateGroups +from source_gitlab.source import SourceGitlab + +TEST_CONFIG_PATH = f"{os.path.dirname(__file__)}/test_config.json" + + +def test_should_migrate(): + assert MigrateGroups._should_migrate({"groups": "group group2 group3"}) is True + assert MigrateGroups._should_migrate({"groups_list": ["test", "group2", "group3"]}) is False + + +def test__modify_and_save(): + source = SourceGitlab() + expected = {"groups": "a b c", "groups_list": ["b", "c", "a"]} + modified_config = MigrateGroups._modify_and_save(config_path=TEST_CONFIG_PATH, source=source, config={"groups": "a b c"}) + assert modified_config["groups_list"].sort() == expected["groups_list"].sort() + assert modified_config.get("groups") diff --git a/airbyte-integrations/connectors/source-gitlab/unit_tests/test_source.py b/airbyte-integrations/connectors/source-gitlab/unit_tests/test_source.py index 8874e957c06ec..5454ee1d7d763 100644 --- a/airbyte-integrations/connectors/source-gitlab/unit_tests/test_source.py +++ b/airbyte-integrations/connectors/source-gitlab/unit_tests/test_source.py @@ -64,12 +64,39 @@ def test_connection_fail_due_to_api_error(errror_code, expected_status, config, assert msg.startswith("Unable to connect to Gitlab API with the provided Private Access Token") +def test_connection_fail_due_to_api_error_oauth(oauth_config, mocker, requests_mock): + mocker.patch("time.sleep") + test_response = { + "access_token": "new_access_token", + "expires_in": 7200, + "created_at": 1735689600, + # (7200 + 1735689600).timestamp().to_rfc3339_string() = "2025-01-01T02:00:00+00:00" + "refresh_token": "new_refresh_token", + } + requests_mock.post("https://gitlab.com/oauth/token", status_code=200, json=test_response) + requests_mock.get("/api/v4/groups", status_code=500) + source = SourceGitlab() + status, msg = source.check_connection(logging.getLogger(), oauth_config) + assert status is False + assert msg.startswith("Unable to connect to Gitlab API with the provided credentials") + + def test_connection_fail_due_to_expired_access_token_error(oauth_config, requests_mock): - expected = "Unable to refresh the `access_token`, please re-auth in Source > Settings." + expected = "Unable to refresh the `access_token`, please re-authenticate in Sources > Settings." requests_mock.post("https://gitlab.com/oauth/token", status_code=401) source = SourceGitlab() status, msg = source.check_connection(logging.getLogger("airbyte"), oauth_config) - assert status is False, expected in msg + assert status is False + assert expected in msg + + +def test_connection_refresh_access_token(oauth_config, requests_mock): + expected = "Unknown error occurred while checking the connection" + requests_mock.post("https://gitlab.com/oauth/token", status_code=200, json={"access_token": "new access token"}) + source = SourceGitlab() + status, msg = source.check_connection(logging.getLogger("airbyte"), oauth_config) + assert status is False + assert expected in msg def test_refresh_expired_access_token_on_error(oauth_config, requests_mock): @@ -108,3 +135,27 @@ def test_connection_fail_due_to_config_error(mocker, api_url, deployment_env, ex } status, msg = source.check_connection(logging.getLogger(), config) assert (status, msg) == (False, expected_message) + + +def test_try_refresh_access_token(oauth_config, requests_mock): + test_response = { + "access_token": "new_access_token", + "expires_in": 7200, + "created_at": 1735689600, + # (7200 + 1735689600).timestamp().to_rfc3339_string() = "2025-01-01T02:00:00+00:00" + "refresh_token": "new_refresh_token", + } + requests_mock.post("https://gitlab.com/oauth/token", status_code=200, json=test_response) + + expected = {"api_url": "gitlab.com", + "credentials": {"access_token": "new_access_token", + "auth_type": "oauth2.0", + "client_id": "client_id", + "client_secret": "client_secret", + "refresh_token": "new_refresh_token", + "token_expiry_date": "2025-01-01T02:00:00+00:00"}, + "start_date": "2021-01-01T00:00:00Z"} + + source = SourceGitlab() + source._auth_params(oauth_config) + assert source._try_refresh_access_token(logger=logging.getLogger(), config=oauth_config) == expected diff --git a/airbyte-integrations/connectors/source-gitlab/unit_tests/test_streams.py b/airbyte-integrations/connectors/source-gitlab/unit_tests/test_streams.py index 7fd342f45c479..283288168998d 100644 --- a/airbyte-integrations/connectors/source-gitlab/unit_tests/test_streams.py +++ b/airbyte-integrations/connectors/source-gitlab/unit_tests/test_streams.py @@ -3,8 +3,10 @@ # import datetime +from unittest.mock import MagicMock import pytest +from airbyte_cdk.models import SyncMode from airbyte_cdk.sources.streams.http.auth import NoAuth from source_gitlab.streams import ( Branches, @@ -276,3 +278,53 @@ def test_transform(requests_mock, stream, response_mocks, expected_records, requ def test_updated_state(stream, current_state, latest_record, new_state, request): stream = request.getfixturevalue(stream) assert stream.get_updated_state(current_state, latest_record) == new_state + + +def test_parse_response_unsuported_response_type(request, caplog): + stream = request.getfixturevalue("pipelines") + from unittest.mock import MagicMock + response = MagicMock() + response.status_code = 200 + response.json = MagicMock(return_value="") + list(stream.parse_response(response=response)) + assert "Unsupported type of response data for stream pipelines" in caplog.text + + +def test_stream_slices_child_stream(request, requests_mock): + commits = request.getfixturevalue("commits") + requests_mock.get("https://gitlab.com/api/v4/projects/p_1?per_page=50&statistics=1", + json=[{"id": 13082000, "description": "", "name": "New CI Test Project"}]) + + slices = list(commits.stream_slices(sync_mode=SyncMode.full_refresh, stream_state={"13082000": {""'created_at': "2021-03-10T23:58:1213"}})) + assert slices + + +def test_next_page_token(request): + response = MagicMock() + response.status_code = 200 + response.json = MagicMock(return_value=["some data"]) + commits = request.getfixturevalue("commits") + assert not commits.next_page_token(response) + data = ["some data" for x in range(0, 50)] + response.json = MagicMock(return_value=data) + assert commits.next_page_token(response) == {'page': 2} + response.json = MagicMock(return_value={"data": "some data"}) + assert not commits.next_page_token(response) + + +def test_availability_strategy(request): + commits = request.getfixturevalue("commits") + assert not commits.availability_strategy + + +def test_request_params(request): + commits = request.getfixturevalue("commits") + expected = {'per_page': 50, 'page': 2, 'with_stats': True} + assert commits.request_params(stream_slice={"updated_after": "2021-03-10T23:58:1213"}, next_page_token={'page': 2}) == expected + + +def test_chunk_date_range(request): + commits = request.getfixturevalue("commits") + # start point in future + start_point = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1) + assert not list(commits._chunk_date_range(start_point)) diff --git a/airbyte-integrations/connectors/source-gitlab/unit_tests/test_utils.py b/airbyte-integrations/connectors/source-gitlab/unit_tests/test_utils.py new file mode 100644 index 0000000000000..bd107e1a16dc5 --- /dev/null +++ b/airbyte-integrations/connectors/source-gitlab/unit_tests/test_utils.py @@ -0,0 +1,17 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import pytest +from source_gitlab.utils import parse_url + + +@pytest.mark.parametrize( + "url, expected", + ( + ("http://example.com", (True, "http", "example.com")), + ("http://example", (True, "http", "example")), + ("test://example.com", (False, "", "")), + ("https://example.com/test/test2", (False, "", "")), + ) +) +def test_parse_url(url, expected): + assert parse_url(url) == expected diff --git a/docs/integrations/sources/gitlab.md b/docs/integrations/sources/gitlab.md index d4e26685717fe..4e919fc6338d3 100644 --- a/docs/integrations/sources/gitlab.md +++ b/docs/integrations/sources/gitlab.md @@ -107,43 +107,44 @@ Gitlab has the [rate limits](https://docs.gitlab.com/ee/user/gitlab_com/index.ht ## Changelog -| Version | Date | Pull Request | Subject | -|:--------|:-----------|:---------------------------------------------------------|:-------------------------------------------------------------------------------------------| -| 2.0.0 | 2023-10-23 | [31700](https://github.com/airbytehq/airbyte/pull/31700) | Add correct date-time format for Deployments, Projects and Groups Members streams | -| 1.8.4 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | -| 1.8.3 | 2023-10-18 | [31547](https://github.com/airbytehq/airbyte/pull/31547) | Add validation for invalid `groups_list` and/or `projects_list` | -| 1.8.2 | 2023-10-17 | [31492](https://github.com/airbytehq/airbyte/pull/31492) | Expand list of possible error status codes when handling expired `access_token` | -| 1.8.1 | 2023-10-12 | [31375](https://github.com/airbytehq/airbyte/pull/31375) | Mark `start_date` as optional, migrate `groups` and `projects` to array | -| 1.8.0 | 2023-10-12 | [31339](https://github.com/airbytehq/airbyte/pull/31339) | Add undeclared fields to streams schemas, validate date/date-time format in stream schemas | -| 1.7.1 | 2023-10-10 | [31210](https://github.com/airbytehq/airbyte/pull/31210) | Added expired `access_token` handling, while checking the connection | -| 1.7.0 | 2023-08-08 | [27869](https://github.com/airbytehq/airbyte/pull/29203) | Add Deployments stream | -| 1.6.0 | 2023-06-30 | [27869](https://github.com/airbytehq/airbyte/pull/27869) | Add `shared_runners_setting` field to groups | -| 1.5.1 | 2023-06-24 | [27679](https://github.com/airbytehq/airbyte/pull/27679) | Fix formatting | -| 1.5.0 | 2023-06-15 | [27392](https://github.com/airbytehq/airbyte/pull/27392) | Make API URL an optional parameter in spec. | -| 1.4.2 | 2023-06-15 | [27346](https://github.com/airbytehq/airbyte/pull/27346) | Partially revert changes made in version 1.0.4, disallow http calls in cloud. | -| 1.4.1 | 2023-06-13 | [27351](https://github.com/airbytehq/airbyte/pull/27351) | Fix OAuth token expiry date. | -| 1.4.0 | 2023-06-12 | [27234](https://github.com/airbytehq/airbyte/pull/27234) | Skip stream slices on 403/404 errors, do not fail syncs. | -| 1.3.1 | 2023-06-08 | [27147](https://github.com/airbytehq/airbyte/pull/27147) | Improve connectivity check for connections with no projects/groups | -| 1.3.0 | 2023-06-08 | [27150](https://github.com/airbytehq/airbyte/pull/27150) | Update stream schemas | -| 1.2.1 | 2023-06-02 | [26947](https://github.com/airbytehq/airbyte/pull/26947) | New field `name` added to `Pipelines` and `PipelinesExtended` stream schema | -| 1.2.0 | 2023-05-17 | [22293](https://github.com/airbytehq/airbyte/pull/22293) | Preserve data in records with flattened keys | -| 1.1.1 | 2023-05-23 | [26422](https://github.com/airbytehq/airbyte/pull/26422) | Fix error `404 Repository Not Found` when syncing project with Repository feature disabled | -| 1.1.0 | 2023-05-10 | [25948](https://github.com/airbytehq/airbyte/pull/25948) | Introduce two new fields in the `Projects` stream schema | -| 1.0.4 | 2023-04-20 | [21373](https://github.com/airbytehq/airbyte/pull/21373) | Accept api_url with or without scheme | -| 1.0.3 | 2023-02-14 | [22992](https://github.com/airbytehq/airbyte/pull/22992) | Specified date formatting in specification | -| 1.0.2 | 2023-01-27 | [22001](https://github.com/airbytehq/airbyte/pull/22001) | Set `AvailabilityStrategy` for streams explicitly to `None` | -| 1.0.1 | 2023-01-23 | [21713](https://github.com/airbytehq/airbyte/pull/21713) | Fix missing data issue | -| 1.0.0 | 2022-12-05 | [7506](https://github.com/airbytehq/airbyte/pull/7506) | Add `OAuth2.0` authentication option | -| 0.1.12 | 2022-12-15 | [20542](https://github.com/airbytehq/airbyte/pull/20542) | Revert HttpAvailability changes, run on cdk 0.15.0 | -| 0.1.11 | 2022-12-14 | [20479](https://github.com/airbytehq/airbyte/pull/20479) | Use HttpAvailabilityStrategy + add unit tests | -| 0.1.10 | 2022-12-12 | [20384](https://github.com/airbytehq/airbyte/pull/20384) | Fetch groups along with their subgroups | -| 0.1.9 | 2022-12-11 | [20348](https://github.com/airbytehq/airbyte/pull/20348) | Fix 403 error when syncing `EpicIssues` stream | -| 0.1.8 | 2022-12-02 | [20023](https://github.com/airbytehq/airbyte/pull/20023) | Fix duplicated records issue for `Projects` stream | -| 0.1.7 | 2022-12-01 | [19986](https://github.com/airbytehq/airbyte/pull/19986) | Fix `GroupMilestones` stream schema | -| 0.1.6 | 2022-06-23 | [13252](https://github.com/airbytehq/airbyte/pull/13252) | Add GroupIssueBoards stream | -| 0.1.5 | 2022-05-02 | [11907](https://github.com/airbytehq/airbyte/pull/11907) | Fix null projects param and `container_expiration_policy` | -| 0.1.4 | 2022-03-23 | [11140](https://github.com/airbytehq/airbyte/pull/11140) | Ingest All Accessible Groups if not Specified in Config | -| 0.1.3 | 2021-12-21 | [8991](https://github.com/airbytehq/airbyte/pull/8991) | Update connector fields title/description | -| 0.1.2 | 2021-10-18 | [7108](https://github.com/airbytehq/airbyte/pull/7108) | Allow all domains to be used as `api_url` | -| 0.1.1 | 2021-10-12 | [6932](https://github.com/airbytehq/airbyte/pull/6932) | Fix pattern field in spec file, remove unused fields from config files, use cache from CDK | -| 0.1.0 | 2021-07-06 | [4174](https://github.com/airbytehq/airbyte/pull/4174) | Initial Release | \ No newline at end of file +| Version | Date | Pull Request | Subject | +|:--------|:-----------|:---------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 2.1.0 | 2023-12-20 | [33676](https://github.com/airbytehq/airbyte/pull/33676) | Add fields to Commits (extended_trailers), Groups (emails_enabled, service_access_tokens_expiration_enforced) and Projects (code_suggestions, model_registry_access_level) streams | +| 2.0.0 | 2023-10-23 | [31700](https://github.com/airbytehq/airbyte/pull/31700) | Add correct date-time format for Deployments, Projects and Groups Members streams | +| 1.8.4 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | +| 1.8.3 | 2023-10-18 | [31547](https://github.com/airbytehq/airbyte/pull/31547) | Add validation for invalid `groups_list` and/or `projects_list` | +| 1.8.2 | 2023-10-17 | [31492](https://github.com/airbytehq/airbyte/pull/31492) | Expand list of possible error status codes when handling expired `access_token` | +| 1.8.1 | 2023-10-12 | [31375](https://github.com/airbytehq/airbyte/pull/31375) | Mark `start_date` as optional, migrate `groups` and `projects` to array | +| 1.8.0 | 2023-10-12 | [31339](https://github.com/airbytehq/airbyte/pull/31339) | Add undeclared fields to streams schemas, validate date/date-time format in stream schemas | +| 1.7.1 | 2023-10-10 | [31210](https://github.com/airbytehq/airbyte/pull/31210) | Added expired `access_token` handling, while checking the connection | +| 1.7.0 | 2023-08-08 | [27869](https://github.com/airbytehq/airbyte/pull/29203) | Add Deployments stream | +| 1.6.0 | 2023-06-30 | [27869](https://github.com/airbytehq/airbyte/pull/27869) | Add `shared_runners_setting` field to groups | +| 1.5.1 | 2023-06-24 | [27679](https://github.com/airbytehq/airbyte/pull/27679) | Fix formatting | +| 1.5.0 | 2023-06-15 | [27392](https://github.com/airbytehq/airbyte/pull/27392) | Make API URL an optional parameter in spec. | +| 1.4.2 | 2023-06-15 | [27346](https://github.com/airbytehq/airbyte/pull/27346) | Partially revert changes made in version 1.0.4, disallow http calls in cloud. | +| 1.4.1 | 2023-06-13 | [27351](https://github.com/airbytehq/airbyte/pull/27351) | Fix OAuth token expiry date. | +| 1.4.0 | 2023-06-12 | [27234](https://github.com/airbytehq/airbyte/pull/27234) | Skip stream slices on 403/404 errors, do not fail syncs. | +| 1.3.1 | 2023-06-08 | [27147](https://github.com/airbytehq/airbyte/pull/27147) | Improve connectivity check for connections with no projects/groups | +| 1.3.0 | 2023-06-08 | [27150](https://github.com/airbytehq/airbyte/pull/27150) | Update stream schemas | +| 1.2.1 | 2023-06-02 | [26947](https://github.com/airbytehq/airbyte/pull/26947) | New field `name` added to `Pipelines` and `PipelinesExtended` stream schema | +| 1.2.0 | 2023-05-17 | [22293](https://github.com/airbytehq/airbyte/pull/22293) | Preserve data in records with flattened keys | +| 1.1.1 | 2023-05-23 | [26422](https://github.com/airbytehq/airbyte/pull/26422) | Fix error `404 Repository Not Found` when syncing project with Repository feature disabled | +| 1.1.0 | 2023-05-10 | [25948](https://github.com/airbytehq/airbyte/pull/25948) | Introduce two new fields in the `Projects` stream schema | +| 1.0.4 | 2023-04-20 | [21373](https://github.com/airbytehq/airbyte/pull/21373) | Accept api_url with or without scheme | +| 1.0.3 | 2023-02-14 | [22992](https://github.com/airbytehq/airbyte/pull/22992) | Specified date formatting in specification | +| 1.0.2 | 2023-01-27 | [22001](https://github.com/airbytehq/airbyte/pull/22001) | Set `AvailabilityStrategy` for streams explicitly to `None` | +| 1.0.1 | 2023-01-23 | [21713](https://github.com/airbytehq/airbyte/pull/21713) | Fix missing data issue | +| 1.0.0 | 2022-12-05 | [7506](https://github.com/airbytehq/airbyte/pull/7506) | Add `OAuth2.0` authentication option | +| 0.1.12 | 2022-12-15 | [20542](https://github.com/airbytehq/airbyte/pull/20542) | Revert HttpAvailability changes, run on cdk 0.15.0 | +| 0.1.11 | 2022-12-14 | [20479](https://github.com/airbytehq/airbyte/pull/20479) | Use HttpAvailabilityStrategy + add unit tests | +| 0.1.10 | 2022-12-12 | [20384](https://github.com/airbytehq/airbyte/pull/20384) | Fetch groups along with their subgroups | +| 0.1.9 | 2022-12-11 | [20348](https://github.com/airbytehq/airbyte/pull/20348) | Fix 403 error when syncing `EpicIssues` stream | +| 0.1.8 | 2022-12-02 | [20023](https://github.com/airbytehq/airbyte/pull/20023) | Fix duplicated records issue for `Projects` stream | +| 0.1.7 | 2022-12-01 | [19986](https://github.com/airbytehq/airbyte/pull/19986) | Fix `GroupMilestones` stream schema | +| 0.1.6 | 2022-06-23 | [13252](https://github.com/airbytehq/airbyte/pull/13252) | Add GroupIssueBoards stream | +| 0.1.5 | 2022-05-02 | [11907](https://github.com/airbytehq/airbyte/pull/11907) | Fix null projects param and `container_expiration_policy` | +| 0.1.4 | 2022-03-23 | [11140](https://github.com/airbytehq/airbyte/pull/11140) | Ingest All Accessible Groups if not Specified in Config | +| 0.1.3 | 2021-12-21 | [8991](https://github.com/airbytehq/airbyte/pull/8991) | Update connector fields title/description | +| 0.1.2 | 2021-10-18 | [7108](https://github.com/airbytehq/airbyte/pull/7108) | Allow all domains to be used as `api_url` | +| 0.1.1 | 2021-10-12 | [6932](https://github.com/airbytehq/airbyte/pull/6932) | Fix pattern field in spec file, remove unused fields from config files, use cache from CDK | +| 0.1.0 | 2021-07-06 | [4174](https://github.com/airbytehq/airbyte/pull/4174) | Initial Release | \ No newline at end of file From 12896252e9a20b5e9f69bb33c0accece410f1b7b Mon Sep 17 00:00:00 2001 From: Augustin Date: Tue, 9 Jan 2024 13:16:01 +0100 Subject: [PATCH 020/574] airbyte-ci: mitigate transient format failure (#34042) --- .github/workflows/format_check.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/format_check.yml b/.github/workflows/format_check.yml index 89e7223331015..51ee6194b5f62 100644 --- a/.github/workflows/format_check.yml +++ b/.github/workflows/format_check.yml @@ -6,7 +6,10 @@ on: airbyte-ci-binary-url: description: "URL to airbyte-ci binary" required: false - default: https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci + # Pin to a specific version of airbyte-ci to avoid transient failures + # Mentioned in issue https://github.com/airbytehq/airbyte/issues/34041 + default: https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/2.14.1/airbyte-ci + push: branches: - master @@ -14,7 +17,7 @@ on: jobs: format-check: - runs-on: "ci-runner-connector-format-medium-dagger-0-9-5" + runs-on: "ci-runner-connector-format-medium-dagger-0-6-4" # IMPORTANT: This name must match the require check name on the branch protection settings name: "Check for formatting errors" steps: @@ -55,6 +58,9 @@ jobs: github_token: ${{ secrets.GH_PAT_MAINTENANCE_OCTAVIA }} tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }} subcommand: "format check all" + # Pin to a specific version of airbyte-ci to avoid transient failures + # Mentioned in issue https://github.com/airbytehq/airbyte/issues/34041 + airbyte_ci_binary_url: https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/2.14.1/airbyte-ci - name: Run airbyte-ci format check [WORKFLOW DISPATCH] id: airbyte_ci_format_check_all_manual From 923331c4058b384198b3770abd9789e6f2055804 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 9 Jan 2024 13:43:28 +0100 Subject: [PATCH 021/574] airbyte-lib: Validation helper command (#34002) --- airbyte-lib/README.md | 12 +- airbyte-lib/airbyte_lib/validate.py | 113 ++++++++++++++++++ airbyte-lib/poetry.lock | 13 +- airbyte-lib/pyproject.toml | 6 +- .../fixtures/invalid_config.json | 1 + .../fixtures/source-test/metadata.yaml | 13 ++ .../fixtures/valid_config.json | 1 + .../integration_tests/test_validation.py | 15 +++ 8 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 airbyte-lib/airbyte_lib/validate.py create mode 100644 airbyte-lib/tests/integration_tests/fixtures/invalid_config.json create mode 100644 airbyte-lib/tests/integration_tests/fixtures/source-test/metadata.yaml create mode 100644 airbyte-lib/tests/integration_tests/fixtures/valid_config.json create mode 100644 airbyte-lib/tests/integration_tests/test_validation.py diff --git a/airbyte-lib/README.md b/airbyte-lib/README.md index addfb2ed49dcc..163fb16c85a1b 100644 --- a/airbyte-lib/README.md +++ b/airbyte-lib/README.md @@ -7,4 +7,14 @@ airbyte-lib is a library that allows to run Airbyte syncs embedded into any Pyth * Make sure [Poetry is installed](https://python-poetry.org/docs/#). * Run `poetry install` * For examples, check out the `examples` folder. They can be run via `poetry run python examples/` -* Unit tests and type checks can be run via `poetry run pytest` \ No newline at end of file +* Unit tests and type checks can be run via `poetry run pytest` + +## Validating source connectors + +To validate a source connector for compliance, the `airbyte-lib-validate-source` script can be used. It can be used like this: + +``` +airbyte-lib-validate-source —connector-dir . -—sample-config secrets/config.json +``` + +The script will install the python package in the provided directory, and run the connector against the provided config. The config should be a valid JSON file, with the same structure as the one that would be provided to the connector in Airbyte. The script will exit with a non-zero exit code if the connector fails to run. \ No newline at end of file diff --git a/airbyte-lib/airbyte_lib/validate.py b/airbyte-lib/airbyte_lib/validate.py new file mode 100644 index 0000000000000..58bae1b2466b2 --- /dev/null +++ b/airbyte-lib/airbyte_lib/validate.py @@ -0,0 +1,113 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +"""Defines the `airbyte-lib-validate-source` CLI, which checks if connectors are compatible with airbyte-lib.""" + +import argparse +import json +import os +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import List + +import airbyte_lib as ab +import yaml + + +def _parse_args(): + parser = argparse.ArgumentParser(description="Validate a connector") + parser.add_argument( + "--connector-dir", + type=str, + required=True, + help="Path to the connector directory", + ) + parser.add_argument( + "--sample-config", + type=str, + required=True, + help="Path to the sample config.json file", + ) + return parser.parse_args() + + +def _run_subprocess_and_raise_on_failure(args: List[str]): + result = subprocess.run(args) + if result.returncode != 0: + raise Exception(f"{args} exited with code {result.returncode}") + + +def tests(connector_name, sample_config): + print("Creating source and validating spec and version...") + source = ab.get_connector(connector_name, config=json.load(open(sample_config))) + + print("Running check...") + source.check() + + print("Fetching streams...") + streams = source.get_available_streams() + + # try to peek all streams - if one works, stop, if none works, throw exception + for stream in streams: + try: + print(f"Trying to read from stream {stream}...") + record = next(source.read_stream(stream)) + assert record, "No record returned" + break + except Exception as e: + print(f"Could not read from stream {stream}: {e}") + else: + raise Exception(f"Could not read from any stream from {streams}") + + +def run(): + """ + This is a CLI entrypoint for the `airbyte-lib-validate-source` command. + It's called like this: airbyte-lib-validate-source —connector-dir . -—sample-config secrets/config.json + It performs a basic smoke test to make sure the connector in question is airbyte-lib compliant: + * Can be installed into a venv + * Can be called via cli entrypoint + * Answers according to the Airbyte protocol when called with spec, check, discover and read + """ + + # parse args + args = _parse_args() + connector_dir = args.connector_dir + sample_config = args.sample_config + validate(connector_dir, sample_config) + + +def validate(connector_dir, sample_config): + # read metadata.yaml + metadata_path = Path(connector_dir) / "metadata.yaml" + with open(metadata_path, "r") as stream: + metadata = yaml.safe_load(stream)["data"] + + # TODO: Use remoteRegistries.pypi.packageName once set for connectors + connector_name = metadata["dockerRepository"].replace("airbyte/", "") + + # create a venv and install the connector + venv_name = f".venv-{connector_name}" + venv_path = Path(venv_name) + if not venv_path.exists(): + _run_subprocess_and_raise_on_failure([sys.executable, "-m", "venv", venv_name]) + + pip_path = os.path.join(venv_name, "bin", "pip") + + _run_subprocess_and_raise_on_failure([pip_path, "install", "-e", connector_dir]) + + # write basic registry to temp json file + registry = { + "sources": [ + { + "dockerRepository": f"airbyte/{connector_name}", + "dockerImageTag": "0.0.1", + } + ] + } + + with tempfile.NamedTemporaryFile(mode="w+t", delete=True) as temp_file: + temp_file.write(json.dumps(registry)) + temp_file.seek(0) + os.environ["AIRBYTE_LOCAL_REGISTRY"] = str(temp_file.name) + tests(connector_name, sample_config) diff --git a/airbyte-lib/poetry.lock b/airbyte-lib/poetry.lock index 8261c5142dfed..3ba1027f21dbe 100644 --- a/airbyte-lib/poetry.lock +++ b/airbyte-lib/poetry.lock @@ -640,6 +640,17 @@ files = [ [package.dependencies] referencing = "*" +[[package]] +name = "types-pyyaml" +version = "6.0.12.12" +description = "Typing stubs for PyYAML" +optional = false +python-versions = "*" +files = [ + {file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"}, + {file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"}, +] + [[package]] name = "types-requests" version = "2.31.0.10" @@ -684,4 +695,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "945526bad52aa7cbbcac56c9b34d024b3b03fd29c7e40bd4c22dce96f8e414d1" +content-hash = "c32ec0e9d78c52a1b9f59e5f40c900d3ddfad6f05208c7ccc46e6a3b42e8b442" diff --git a/airbyte-lib/pyproject.toml b/airbyte-lib/pyproject.toml index 5d1b88ecdc7b0..e54106d957dc4 100644 --- a/airbyte-lib/pyproject.toml +++ b/airbyte-lib/pyproject.toml @@ -10,6 +10,7 @@ python = "^3.10" jsonschema = "3.2.0" requests = "^2.31.0" airbyte-protocol-models = "^1.0.1" +types-pyyaml = "^6.0.12.12" [tool.poetry.group.dev.dependencies] pytest = "^7.4.3" @@ -26,4 +27,7 @@ build-backend = "poetry.core.masonry.api" ignore_missing_imports = true [tool.pytest.ini_options] -addopts = "--mypy" \ No newline at end of file +addopts = "--mypy" + +[tool.poetry.scripts] +airbyte-lib-validate-source = "airbyte_lib.validate:run" \ No newline at end of file diff --git a/airbyte-lib/tests/integration_tests/fixtures/invalid_config.json b/airbyte-lib/tests/integration_tests/fixtures/invalid_config.json new file mode 100644 index 0000000000000..3ce4b45a32097 --- /dev/null +++ b/airbyte-lib/tests/integration_tests/fixtures/invalid_config.json @@ -0,0 +1 @@ +{ "apiKey": "wrong" } diff --git a/airbyte-lib/tests/integration_tests/fixtures/source-test/metadata.yaml b/airbyte-lib/tests/integration_tests/fixtures/source-test/metadata.yaml new file mode 100644 index 0000000000000..a2ec10113aa8c --- /dev/null +++ b/airbyte-lib/tests/integration_tests/fixtures/source-test/metadata.yaml @@ -0,0 +1,13 @@ +data: + connectorSubtype: api + connectorType: source + definitionId: 47f17145-fe20-4ef5-a548-e29b048adf84 + dockerImageTag: 0.0.0 + dockerRepository: airbyte/source-test + githubIssueLabel: source-test + name: Test + releaseDate: 2023-08-25 + releaseStage: alpha + supportLevel: community + documentationUrl: https://docs.airbyte.com/integrations/sources/apify-dataset +metadataSpecVersion: "1.0" diff --git a/airbyte-lib/tests/integration_tests/fixtures/valid_config.json b/airbyte-lib/tests/integration_tests/fixtures/valid_config.json new file mode 100644 index 0000000000000..fbe094d80a449 --- /dev/null +++ b/airbyte-lib/tests/integration_tests/fixtures/valid_config.json @@ -0,0 +1 @@ +{ "apiKey": "test" } diff --git a/airbyte-lib/tests/integration_tests/test_validation.py b/airbyte-lib/tests/integration_tests/test_validation.py new file mode 100644 index 0000000000000..75a463592833e --- /dev/null +++ b/airbyte-lib/tests/integration_tests/test_validation.py @@ -0,0 +1,15 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import os +import shutil + +import pytest +from airbyte_lib.validate import validate + + +def test_validate_success(): + validate("./tests/integration_tests/fixtures/source-test", "./tests/integration_tests/fixtures/valid_config.json") + +def test_validate_failure(): + with pytest.raises(Exception): + validate("./tests/integration_tests/fixtures/source-test", "./tests/integration_tests/fixtures/invalid_config.json") From b4ddfb84543335777bc65f8c0e426116a193dc23 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 9 Jan 2024 15:29:40 +0100 Subject: [PATCH 022/574] airbyte-lib: Generate docs (#33997) --- airbyte-lib/.gitattributes | 2 + airbyte-lib/README.md | 8 +- airbyte-lib/docs.py | 30 ++ airbyte-lib/docs/frame.html.jinja2 | 14 + airbyte-lib/docs/generated/airbyte_lib.html | 333 ++++++++++++++ .../docs/generated/airbyte_lib/caches.html | 7 + .../generated/airbyte_lib/file_writers.html | 7 + airbyte-lib/docs/generated/index.html | 7 + airbyte-lib/poetry.lock | 411 +++++++++++------- airbyte-lib/pyproject.toml | 4 +- airbyte-lib/tests/docs_tests/__init__.py | 0 .../tests/docs_tests/test_docs_checked_in.py | 22 + 12 files changed, 693 insertions(+), 152 deletions(-) create mode 100644 airbyte-lib/.gitattributes create mode 100644 airbyte-lib/docs.py create mode 100644 airbyte-lib/docs/frame.html.jinja2 create mode 100644 airbyte-lib/docs/generated/airbyte_lib.html create mode 100644 airbyte-lib/docs/generated/airbyte_lib/caches.html create mode 100644 airbyte-lib/docs/generated/airbyte_lib/file_writers.html create mode 100644 airbyte-lib/docs/generated/index.html create mode 100644 airbyte-lib/tests/docs_tests/__init__.py create mode 100644 airbyte-lib/tests/docs_tests/test_docs_checked_in.py diff --git a/airbyte-lib/.gitattributes b/airbyte-lib/.gitattributes new file mode 100644 index 0000000000000..7af38cfbe1078 --- /dev/null +++ b/airbyte-lib/.gitattributes @@ -0,0 +1,2 @@ +# Hide diffs in auto-generated files +docs/generated/**/* linguist-generated=true diff --git a/airbyte-lib/README.md b/airbyte-lib/README.md index 163fb16c85a1b..b30ced523d93e 100644 --- a/airbyte-lib/README.md +++ b/airbyte-lib/README.md @@ -9,6 +9,12 @@ airbyte-lib is a library that allows to run Airbyte syncs embedded into any Pyth * For examples, check out the `examples` folder. They can be run via `poetry run python examples/` * Unit tests and type checks can be run via `poetry run pytest` +## Documentation + +Regular documentation lives in the `/docs` folder. Based on the doc strings of public methods, we generate API documentation using [pdoc](https://pdoc.dev). To generate the documentation, run `poetry run generate-docs`. The documentation will be generated in the `docs/generate` folder. This needs to be done manually when changing the public interface of the library. + +A unit test validates the documentation is up to date. + ## Validating source connectors To validate a source connector for compliance, the `airbyte-lib-validate-source` script can be used. It can be used like this: @@ -17,4 +23,4 @@ To validate a source connector for compliance, the `airbyte-lib-validate-source` airbyte-lib-validate-source —connector-dir . -—sample-config secrets/config.json ``` -The script will install the python package in the provided directory, and run the connector against the provided config. The config should be a valid JSON file, with the same structure as the one that would be provided to the connector in Airbyte. The script will exit with a non-zero exit code if the connector fails to run. \ No newline at end of file +The script will install the python package in the provided directory, and run the connector against the provided config. The config should be a valid JSON file, with the same structure as the one that would be provided to the connector in Airbyte. The script will exit with a non-zero exit code if the connector fails to run. diff --git a/airbyte-lib/docs.py b/airbyte-lib/docs.py new file mode 100644 index 0000000000000..a80ad6e4a8a12 --- /dev/null +++ b/airbyte-lib/docs.py @@ -0,0 +1,30 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import os +import pathlib +import shutil + +import pdoc + + +def run(): + """ + Generate docs for all public modules in airbyte_lib and save them to docs/generated. + Public modules are: + * The main airbyte_lib module + * All directory modules in airbyte_lib that don't start with an underscore + """ + public_modules = ["airbyte_lib"] + + # recursively delete the docs/generated folder if it exists + if os.path.exists("docs/generated"): + shutil.rmtree("docs/generated") + + # determine all folders in airbyte_lib that don't start with an underscore and add them to public_modules + for d in os.listdir("airbyte_lib"): + dir_path = pathlib.Path(f"airbyte_lib/{d}") + if dir_path.is_dir() and not d.startswith("_"): + public_modules.append(dir_path) + + pdoc.render.configure(template_directory="docs", show_source=False, search=False) + pdoc.pdoc(*public_modules, output_directory=pathlib.Path("docs/generated")) diff --git a/airbyte-lib/docs/frame.html.jinja2 b/airbyte-lib/docs/frame.html.jinja2 new file mode 100644 index 0000000000000..379ae376725f0 --- /dev/null +++ b/airbyte-lib/docs/frame.html.jinja2 @@ -0,0 +1,14 @@ + +
+ {% block module_contents %}{% endblock %} +
+ +{% filter minify_css %} + {% block style %} + {# The same CSS files as in pdoc's default template, except for layout.css. + You may leave out Bootstrap Reboot, which corrects inconsistences across browsers + but may conflict with you website's stylesheet. #} + + + {% endblock %} +{% endfilter %} diff --git a/airbyte-lib/docs/generated/airbyte_lib.html b/airbyte-lib/docs/generated/airbyte_lib.html new file mode 100644 index 0000000000000..75181e7e55a90 --- /dev/null +++ b/airbyte-lib/docs/generated/airbyte_lib.html @@ -0,0 +1,333 @@ + +
+
+
+ + def + get_connector( name: str, version: str = 'latest', config: Optional[Dict[str, Any]] = None, use_local_install: bool = False, install_if_missing: bool = False): + + +
+ + +

Get a connector by name and version.

+ +
Parameters
+ +
    +
  • name: connector name
  • +
  • version: connector version - if not provided, the most recent version will be used
  • +
  • config: connector config - if not provided, you need to set it later via the set_config method
  • +
  • use_local_install: whether to use a virtual environment to run the connector. If True, the connector is expected to be available on the path (e.g. installed via pip). If False, the connector will be installed automatically in a virtual environment.
  • +
  • install_if_missing: whether to install the connector if it is not available locally. This parameter is ignored if use_local_install is True.
  • +
+
+ + +
+
+
+ + def + get_in_memory_cache(): + + +
+ + + + +
+
+
+ + class + Dataset: + + +
+ + + + +
+
+ + Dataset(cache: airbyte_lib.cache.Cache, stream: str) + + +
+ + + + +
+
+
+ + def + to_pandas(self): + + +
+ + + + +
+
+
+ + def + to_sql_table(self): + + +
+ + + + +
+
+
+
+ + class + SyncResult: + + +
+ + + + +
+
+ + SyncResult(processed_records: int, cache: airbyte_lib.cache.Cache) + + +
+ + + + +
+
+
+ processed_records + + +
+ + + + +
+
+
+ + def + get_sql_engine(self) -> Any: + + +
+ + + + +
+
+
+
+ + class + Source: + + +
+ + +

This class is representing a source that can be called

+
+ + +
+
+ + Source( executor: airbyte_lib.executor.Executor, name: str, config: Optional[Dict[str, Any]] = None, streams: Optional[List[str]] = None) + + +
+ + + + +
+
+
+ executor + + +
+ + + + +
+
+
+ name + + +
+ + + + +
+
+
+ streams: Optional[List[str]] + + +
+ + + + +
+
+
+ config: Optional[Dict[str, Any]] + + +
+ + + + +
+
+
+ + def + set_streams(self, streams: List[str]): + + +
+ + + + +
+
+
+ + def + set_config(self, config: Dict[str, Any]): + + +
+ + + + +
+
+
+ + def + get_available_streams(self) -> List[str]: + + +
+ + +

Get the available streams from the spec.

+
+ + +
+
+
+ + def + read_stream(self, stream: str) -> Iterable[Dict[str, Any]]: + + +
+ + +

Read a stream from the connector.

+ +

This involves the following steps:

+ +
    +
  • Call discover to get the catalog
  • +
  • Generate a configured catalog that syncs the given stream in full_refresh mode
  • +
  • Write the configured catalog and the config to a temporary file
  • +
  • execute the connector with read --config --catalog
  • +
  • Listen to the messages and return the first AirbyteRecordMessages that come along.
  • +
  • Make sure the subprocess is killed when the function returns.
  • +
+
+ + +
+
+
+ + def + check(self): + + +
+ + +

Call check on the connector.

+ +

This involves the following steps:

+ +
    +
  • Write the config to a temporary file
  • +
  • execute the connector with check --config
  • +
  • Listen to the messages and return the first AirbyteCatalog that comes along.
  • +
  • Make sure the subprocess is killed when the function returns.
  • +
+
+ + +
+
+
+ + def + install(self): + + +
+ + + + +
+
+
+ + def + read_all( self, cache: Optional[airbyte_lib.cache.Cache] = None) -> SyncResult: + + +
+ + + + +
+
+
+ + + + \ No newline at end of file diff --git a/airbyte-lib/docs/generated/airbyte_lib/caches.html b/airbyte-lib/docs/generated/airbyte_lib/caches.html new file mode 100644 index 0000000000000..c0d27ca14eaa0 --- /dev/null +++ b/airbyte-lib/docs/generated/airbyte_lib/caches.html @@ -0,0 +1,7 @@ + +
+
+ + + + \ No newline at end of file diff --git a/airbyte-lib/docs/generated/airbyte_lib/file_writers.html b/airbyte-lib/docs/generated/airbyte_lib/file_writers.html new file mode 100644 index 0000000000000..c0d27ca14eaa0 --- /dev/null +++ b/airbyte-lib/docs/generated/airbyte_lib/file_writers.html @@ -0,0 +1,7 @@ + +
+
+ + + + \ No newline at end of file diff --git a/airbyte-lib/docs/generated/index.html b/airbyte-lib/docs/generated/index.html new file mode 100644 index 0000000000000..6dfc876b8f9c6 --- /dev/null +++ b/airbyte-lib/docs/generated/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/airbyte-lib/poetry.lock b/airbyte-lib/poetry.lock index 3ba1027f21dbe..4bb03217983c8 100644 --- a/airbyte-lib/poetry.lock +++ b/airbyte-lib/poetry.lock @@ -16,21 +16,22 @@ pydantic = ">=1.9.2,<1.10.0" [[package]] name = "attrs" -version = "23.1.0" +version = "23.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] [package.extras] cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] +dev = ["attrs[tests]", "pre-commit"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [[package]] name = "certifi" @@ -205,6 +206,23 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "jsonschema" version = "3.2.0" @@ -226,40 +244,99 @@ six = ">=1.11.0" format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] format-nongpl = ["idna", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "webcolors"] +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + [[package]] name = "mypy" -version = "1.7.1" +version = "1.8.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, - {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, - {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, - {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, - {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, - {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, - {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, - {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, - {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, - {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, - {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, - {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, - {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, - {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, - {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, - {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, - {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, - {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, - {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, - {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, - {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, - {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, - {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, - {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, - {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, - {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, - {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, + {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, + {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, + {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, + {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, + {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, + {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, + {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, + {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, + {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, + {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, + {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, + {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, + {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, + {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, + {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, + {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, + {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, ] [package.dependencies] @@ -295,6 +372,25 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "pdoc" +version = "14.3.0" +description = "API Documentation for Python Projects" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pdoc-14.3.0-py3-none-any.whl", hash = "sha256:9a8f9a48bda5a99c249367c2b99779dbdd9f4a56f905068c9c2d6868dbae6882"}, + {file = "pdoc-14.3.0.tar.gz", hash = "sha256:40bf8f092fcd91560d5e6cebb7c21b65df699f90a468c8ea316235c3368d5449"}, +] + +[package.dependencies] +Jinja2 = ">=2.11.0" +MarkupSafe = "*" +pygments = ">=2.12.0" + +[package.extras] +dev = ["hypothesis", "mypy", "pdoc-pyo3-sample-library (==1.0.11)", "pygments (>=2.14.0)", "pytest", "pytest-cov", "pytest-timeout", "ruff", "tox", "types-pygments"] + [[package]] name = "pluggy" version = "1.3.0" @@ -361,6 +457,21 @@ typing-extensions = ">=3.7.4.3" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pyrsistent" version = "0.20.0" @@ -404,13 +515,13 @@ files = [ [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -446,13 +557,13 @@ pytest = {version = ">=6.2", markers = "python_version >= \"3.10\""} [[package]] name = "referencing" -version = "0.32.0" +version = "0.32.1" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" files = [ - {file = "referencing-0.32.0-py3-none-any.whl", hash = "sha256:bdcd3efb936f82ff86f993093f6da7435c7de69a3b3a5a06678a6050184bee99"}, - {file = "referencing-0.32.0.tar.gz", hash = "sha256:689e64fe121843dcfd57b71933318ef1f91188ffb45367332700a86ac8fd6161"}, + {file = "referencing-0.32.1-py3-none-any.whl", hash = "sha256:7e4dc12271d8e15612bfe35792f5ea1c40970dadf8624602e33db2758f7ee554"}, + {file = "referencing-0.32.1.tar.gz", hash = "sha256:3c57da0513e9563eb7e203ebe9bb3a1b509b042016433bd1e45a2853466c3dd3"}, ] [package.dependencies] @@ -482,121 +593,121 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rpds-py" -version = "0.15.2" +version = "0.16.2" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" files = [ - {file = "rpds_py-0.15.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:337a8653fb11d2fbe7157c961cc78cb3c161d98cf44410ace9a3dc2db4fad882"}, - {file = "rpds_py-0.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:813a65f95bfcb7c8f2a70dd6add9b51e9accc3bdb3e03d0ff7a9e6a2d3e174bf"}, - {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:082e0e55d73690ffb4da4352d1b5bbe1b5c6034eb9dc8c91aa2a3ee15f70d3e2"}, - {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5595c80dd03d7e6c6afb73f3594bf3379a7d79fa57164b591d012d4b71d6ac4c"}, - {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb10bb720348fe1647a94eb605accb9ef6a9b1875d8845f9e763d9d71a706387"}, - {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53304cc14b1d94487d70086e1cb0cb4c29ec6da994d58ae84a4d7e78c6a6d04d"}, - {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d64a657de7aae8db2da60dc0c9e4638a0c3893b4d60101fd564a3362b2bfeb34"}, - {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee40206d1d6e95eaa2b7b919195e3689a5cf6ded730632de7f187f35a1b6052c"}, - {file = "rpds_py-0.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1607cda6129f815493a3c184492acb5ae4aa6ed61d3a1b3663aa9824ed26f7ac"}, - {file = "rpds_py-0.15.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f3e6e2e502c4043c52a99316d89dc49f416acda5b0c6886e0dd8ea7bb35859e8"}, - {file = "rpds_py-0.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:044f6f46d62444800402851afa3c3ae50141f12013060c1a3a0677e013310d6d"}, - {file = "rpds_py-0.15.2-cp310-none-win32.whl", hash = "sha256:c827a931c6b57f50f1bb5de400dcfb00bad8117e3753e80b96adb72d9d811514"}, - {file = "rpds_py-0.15.2-cp310-none-win_amd64.whl", hash = "sha256:3bbc89ce2a219662ea142f0abcf8d43f04a41d5b1880be17a794c39f0d609cb0"}, - {file = "rpds_py-0.15.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:1fd0f0b1ccd7d537b858a56355a250108df692102e08aa2036e1a094fd78b2dc"}, - {file = "rpds_py-0.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b414ef79f1f06fb90b5165db8aef77512c1a5e3ed1b4807da8476b7e2c853283"}, - {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31272c674f725dfe0f343d73b0abe8c878c646967ec1c6106122faae1efc15b"}, - {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6945c2d61c42bb7e818677f43638675b8c1c43e858b67a96df3eb2426a86c9d"}, - {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02744236ac1895d7be837878e707a5c35fb8edc5137602f253b63623d7ad5c8c"}, - {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2181e86d4e1cdf49a7320cb72a36c45efcb7670d0a88f09fd2d3a7967c0540fd"}, - {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a8ff8e809da81363bffca2b965cb6e4bf6056b495fc3f078467d1f8266fe27f"}, - {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97532802f14d383f37d603a56e226909f825a83ff298dc1b6697de00d2243999"}, - {file = "rpds_py-0.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:13716e53627ad97babf72ac9e01cf9a7d4af2f75dd5ed7b323a7a9520e948282"}, - {file = "rpds_py-0.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2f1f295a5c28cfa74a7d48c95acc1c8a7acd49d7d9072040d4b694fe11cd7166"}, - {file = "rpds_py-0.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8ec464f20fe803ae00419bd1610934e3bda963aeba1e6181dfc9033dc7e8940c"}, - {file = "rpds_py-0.15.2-cp311-none-win32.whl", hash = "sha256:b61d5096e75fd71018b25da50b82dd70ec39b5e15bb2134daf7eb7bbbc103644"}, - {file = "rpds_py-0.15.2-cp311-none-win_amd64.whl", hash = "sha256:9d41ebb471a6f064c0d1c873c4f7dded733d16ca5db7d551fb04ff3805d87802"}, - {file = "rpds_py-0.15.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:13ff62d3561a23c17341b4afc78e8fcfd799ab67c0b1ca32091d71383a98ba4b"}, - {file = "rpds_py-0.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b70b45a40ad0798b69748b34d508259ef2bdc84fb2aad4048bc7c9cafb68ddb3"}, - {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ecbba7efd82bd2a4bb88aab7f984eb5470991c1347bdd1f35fb34ea28dba6e"}, - {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d38494a8d21c246c535b41ecdb2d562c4b933cf3d68de03e8bc43a0d41be652"}, - {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13152dfe7d7c27c40df8b99ac6aab12b978b546716e99f67e8a67a1d441acbc3"}, - {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:164fcee32f15d04d61568c9cb0d919e37ff3195919cd604039ff3053ada0461b"}, - {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a5122b17a4faf5d7a6d91fa67b479736c0cacc7afe791ddebb7163a8550b799"}, - {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46b4f3d47d1033db569173be62365fbf7808c2bd3fb742314d251f130d90d44c"}, - {file = "rpds_py-0.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c61e42b4ceb9759727045765e87d51c1bb9f89987aca1fcc8a040232138cad1c"}, - {file = "rpds_py-0.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d2aa3ca9552f83b0b4fa6ca8c6ce08da6580f37e3e0ab7afac73a1cfdc230c0e"}, - {file = "rpds_py-0.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec19e823b4ccd87bd69e990879acbce9e961fc7aebe150156b8f4418d4b27b7f"}, - {file = "rpds_py-0.15.2-cp312-none-win32.whl", hash = "sha256:afeabb382c1256a7477b739820bce7fe782bb807d82927102cee73e79b41b38b"}, - {file = "rpds_py-0.15.2-cp312-none-win_amd64.whl", hash = "sha256:422b0901878a31ef167435c5ad46560362891816a76cc0d150683f3868a6f0d1"}, - {file = "rpds_py-0.15.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:baf744e5f9d5ee6531deea443be78b36ed1cd36c65a0b95ea4e8d69fa0102268"}, - {file = "rpds_py-0.15.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7e072f5da38d6428ba1fc1115d3cc0dae895df671cb04c70c019985e8c7606be"}, - {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f138f550b83554f5b344d6be35d3ed59348510edc3cb96f75309db6e9bfe8210"}, - {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b2a4cd924d0e2f4b1a68034abe4cadc73d69ad5f4cf02db6481c0d4d749f548f"}, - {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5eb05b654a41e0f81ab27a7c3e88b6590425eb3e934e1d533ecec5dc88a6ffff"}, - {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ee066a64f0d2ba45391cac15b3a70dcb549e968a117bd0500634754cfe0e5fc"}, - {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51a899792ee2c696072791e56b2020caff58b275abecbc9ae0cb71af0645c95"}, - {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac2ac84a4950d627d84b61f082eba61314373cfab4b3c264b62efab02ababe83"}, - {file = "rpds_py-0.15.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:62b292fff4739c6be89e6a0240c02bda5a9066a339d90ab191cf66e9fdbdc193"}, - {file = "rpds_py-0.15.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:98ee201a52a7f65608e5494518932e1473fd43535f12cade0a1b4ab32737fe28"}, - {file = "rpds_py-0.15.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3d40fb3ca22e3d40f494d577441b263026a3bd8c97ae6ce89b2d3c4b39ac9581"}, - {file = "rpds_py-0.15.2-cp38-none-win32.whl", hash = "sha256:30479a9f1fce47df56b07460b520f49fa2115ec2926d3b1303c85c81f8401ed1"}, - {file = "rpds_py-0.15.2-cp38-none-win_amd64.whl", hash = "sha256:2df3d07a16a3bef0917b28cd564778fbb31f3ffa5b5e33584470e2d1b0f248f0"}, - {file = "rpds_py-0.15.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:56b51ba29a18e5f5810224bcf00747ad931c0716e3c09a76b4a1edd3d4aba71f"}, - {file = "rpds_py-0.15.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c11bc5814554b018f6c5d6ae0969e43766f81e995000b53a5d8c8057055e886"}, - {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2faa97212b0dc465afeedf49045cdd077f97be1188285e646a9f689cb5dfff9e"}, - {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86c01299942b0f4b5b5f28c8701689181ad2eab852e65417172dbdd6c5b3ccc8"}, - {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd7d3608589072f63078b4063a6c536af832e76b0b3885f1bfe9e892abe6c207"}, - {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:938518a11780b39998179d07f31a4a468888123f9b00463842cd40f98191f4d3"}, - {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dccc623725d0b298f557d869a68496a2fd2a9e9c41107f234fa5f7a37d278ac"}, - {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d46ee458452727a147d7897bb33886981ae1235775e05decae5d5d07f537695a"}, - {file = "rpds_py-0.15.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d9d7ebcd11ea76ba0feaae98485cd8e31467c3d7985210fab46983278214736b"}, - {file = "rpds_py-0.15.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8a5f574b92b3ee7d254e56d56e37ec0e1416acb1ae357c4956d76a1788dc58fb"}, - {file = "rpds_py-0.15.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3db0c998c92b909d7c90b66c965590d4f3cd86157176a6cf14aa1f867b77b889"}, - {file = "rpds_py-0.15.2-cp39-none-win32.whl", hash = "sha256:bbc7421cbd28b4316d1d017db338039a7943f945c6f2bb15e1439b14b5682d28"}, - {file = "rpds_py-0.15.2-cp39-none-win_amd64.whl", hash = "sha256:1c24e30d720c0009b6fb2e1905b025da56103c70a8b31b99138e4ed1c2a6c5b0"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e6fcd0a0f62f2997107f758bb372397b8d5fd5f39cc6dcb86f7cb98a2172d6c"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d800a8e2ac62db1b9ea5d6d1724f1a93c53907ca061de4d05ed94e8dfa79050c"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e09d017e3f4d9bd7d17a30d3f59e4d6d9ba2d2ced280eec2425e84112cf623f"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b88c3ab98556bc351b36d6208a6089de8c8db14a7f6e1f57f82a334bd2c18f0b"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f333bfe782a2d05a67cfaa0cc9cd68b36b39ee6acfe099f980541ed973a7093"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b629db53fe17e6ce478a969d30bd1d0e8b53238c46e3a9c9db39e8b65a9ef973"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485fbdd23becb822804ed05622907ee5c8e8a5f43f6f43894a45f463b2217045"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:893e38d0f4319dfa70c0f36381a37cc418985c87b11d9784365b1fff4fa6973b"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8ffdeb7dbd0160d4e391e1f857477e4762d00aa2199c294eb95dfb9451aa1d9f"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fc33267d58dfbb2361baed52668c5d8c15d24bc0372cecbb79fed77339b55e0d"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2e7e5633577b3bd56bf3af2ef6ae3778bbafb83743989d57f0e7edbf6c0980e4"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8b9650f92251fdef843e74fc252cdfd6e3c700157ad686eeb0c6d7fdb2d11652"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:07a2e1d78d382f7181789713cdf0c16edbad4fe14fe1d115526cb6f0eef0daa3"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03f9c5875515820633bd7709a25c3e60c1ea9ad1c5d4030ce8a8c203309c36fd"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:580182fa5b269c2981e9ce9764367cb4edc81982ce289208d4607c203f44ffde"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa1e626c524d2c7972c0f3a8a575d654a3a9c008370dc2a97e46abd0eaa749b9"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae9d83a81b09ce3a817e2cbb23aabc07f86a3abc664c613cd283ce7a03541e95"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9235be95662559141934fced8197de6fee8c58870f36756b0584424b6d708393"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a72e00826a2b032dda3eb25aa3e3579c6d6773d22d8446089a57a123481cc46c"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ab095edf1d840a6a6a4307e1a5b907a299a94e7b90e75436ee770b8c35d22a25"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:3b79c63d29101cbaa53a517683557bb550462394fb91044cc5998dd2acff7340"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:911e600e798374c0d86235e7ef19109cf865d1336942d398ff313375a25a93ba"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3cd61e759c4075510052d1eca5cddbd297fe1164efec14ef1fce3f09b974dfe4"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9d2ae79f31da5143e020a8d4fc74e1f0cbcb8011bdf97453c140aa616db51406"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e99d6510c8557510c220b865d966b105464740dcbebf9b79ecd4fbab30a13d9"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c43e1b89099279cc03eb1c725c5de12af6edcd2f78e2f8a022569efa639ada3"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac7187bee72384b9cfedf09a29a3b2b6e8815cc64c095cdc8b5e6aec81e9fd5f"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3423007fc0661827e06f8a185a3792c73dda41f30f3421562f210cf0c9e49569"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2974e6dff38afafd5ccf8f41cb8fc94600b3f4fd9b0a98f6ece6e2219e3158d5"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:93c18a1696a8e0388ed84b024fe1a188a26ba999b61d1d9a371318cb89885a8c"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c7cd0841a586b7105513a7c8c3d5c276f3adc762a072d81ef7fae80632afad1e"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:709dc11af2f74ba89c68b1592368c6edcbccdb0a06ba77eb28c8fe08bb6997da"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:fc066395e6332da1e7525d605b4c96055669f8336600bef8ac569d5226a7c76f"}, - {file = "rpds_py-0.15.2.tar.gz", hash = "sha256:373b76eeb79e8c14f6d82cb1d4d5293f9e4059baec6c1b16dca7ad13b6131b39"}, + {file = "rpds_py-0.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:509b617ac787cd1149600e731db9274ebbef094503ca25158e6f23edaba1ca8f"}, + {file = "rpds_py-0.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:413b9c17388bbd0d87a329d8e30c1a4c6e44e2bb25457f43725a8e6fe4161e9e"}, + {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2946b120718eba9af2b4dd103affc1164a87b9e9ebff8c3e4c05d7b7a7e274e2"}, + {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:35ae5ece284cf36464eb160880018cf6088a9ac5ddc72292a6092b6ef3f4da53"}, + {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc6a7620ba7639a3db6213da61312cb4aa9ac0ca6e00dc1cbbdc21c2aa6eb57"}, + {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8cb6fe8ecdfffa0e711a75c931fb39f4ba382b4b3ccedeca43f18693864fe850"}, + {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dace7b26a13353e24613417ce2239491b40a6ad44e5776a18eaff7733488b44"}, + {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bdbc5fcb04a7309074de6b67fa9bc4b418ab3fc435fec1f2779a0eced688d04"}, + {file = "rpds_py-0.16.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f42e25c016927e2a6b1ce748112c3ab134261fc2ddc867e92d02006103e1b1b7"}, + {file = "rpds_py-0.16.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eab36eae3f3e8e24b05748ec9acc66286662f5d25c52ad70cadab544e034536b"}, + {file = "rpds_py-0.16.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0474df4ade9a3b4af96c3d36eb81856cb9462e4c6657d4caecfd840d2a13f3c9"}, + {file = "rpds_py-0.16.2-cp310-none-win32.whl", hash = "sha256:84c5a4d1f9dd7e2d2c44097fb09fffe728629bad31eb56caf97719e55575aa82"}, + {file = "rpds_py-0.16.2-cp310-none-win_amd64.whl", hash = "sha256:2bd82db36cd70b3628c0c57d81d2438e8dd4b7b32a6a9f25f24ab0e657cb6c4e"}, + {file = "rpds_py-0.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:adc0c3d6fc6ae35fee3e4917628983f6ce630d513cbaad575b4517d47e81b4bb"}, + {file = "rpds_py-0.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ec23fcad480e77ede06cf4127a25fc440f7489922e17fc058f426b5256ee0edb"}, + {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07aab64e2808c3ebac2a44f67e9dc0543812b715126dfd6fe4264df527556cb6"}, + {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a4ebb8b20bd09c5ce7884c8f0388801100f5e75e7f733b1b6613c713371feefc"}, + {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3d7e2ea25d3517c6d7e5a1cc3702cffa6bd18d9ef8d08d9af6717fc1c700eed"}, + {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f28ac0e8e7242d140f99402a903a2c596ab71550272ae9247ad78f9a932b5698"}, + {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19f00f57fdd38db4bb5ad09f9ead1b535332dbf624200e9029a45f1f35527ebb"}, + {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3da5a4c56953bdbf6d04447c3410309616c54433146ccdb4a277b9cb499bc10e"}, + {file = "rpds_py-0.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec2e1cf025b2c0f48ec17ff3e642661da7ee332d326f2e6619366ce8e221f018"}, + {file = "rpds_py-0.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e0441fb4fdd39a230477b2ca9be90868af64425bfe7b122b57e61e45737a653b"}, + {file = "rpds_py-0.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9f0350ef2fba5f34eb0c9000ea328e51b9572b403d2f7f3b19f24085f6f598e8"}, + {file = "rpds_py-0.16.2-cp311-none-win32.whl", hash = "sha256:5a80e2f83391ad0808b4646732af2a7b67550b98f0cae056cb3b40622a83dbb3"}, + {file = "rpds_py-0.16.2-cp311-none-win_amd64.whl", hash = "sha256:e04e56b4ca7a770593633556e8e9e46579d66ec2ada846b401252a2bdcf70a6d"}, + {file = "rpds_py-0.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:5e6caa3809e50690bd92fa490f5c38caa86082c8c3315aa438bce43786d5e90d"}, + {file = "rpds_py-0.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e53b9b25cac9065328901713a7e9e3b12e4f57ef4280b370fbbf6fef2052eef"}, + {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af27423662f32d7501a00c5e7342f7dbd1e4a718aea7a239781357d15d437133"}, + {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d4dd5fb16eb3825742bad8339d454054261ab59fed2fbac84e1d84d5aae7ba"}, + {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e061de3b745fe611e23cd7318aec2c8b0e4153939c25c9202a5811ca911fd733"}, + {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b811d182ad17ea294f2ec63c0621e7be92a1141e1012383461872cead87468f"}, + {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5552f328eaef1a75ff129d4d0c437bf44e43f9436d3996e8eab623ea0f5fcf73"}, + {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dcbe1f8dd179e4d69b70b1f1d9bb6fd1e7e1bdc9c9aad345cdeb332e29d40748"}, + {file = "rpds_py-0.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8aad80645a011abae487d356e0ceb359f4938dfb6f7bcc410027ed7ae4f7bb8b"}, + {file = "rpds_py-0.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6f5549d6ed1da9bfe3631ca9483ae906f21410be2445b73443fa9f017601c6f"}, + {file = "rpds_py-0.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d452817e0d9c749c431a1121d56a777bd7099b720b3d1c820f1725cb40928f58"}, + {file = "rpds_py-0.16.2-cp312-none-win32.whl", hash = "sha256:888a97002e986eca10d8546e3c8b97da1d47ad8b69726dcfeb3e56348ebb28a3"}, + {file = "rpds_py-0.16.2-cp312-none-win_amd64.whl", hash = "sha256:d8dda2a806dfa4a9b795950c4f5cc56d6d6159f7d68080aedaff3bdc9b5032f5"}, + {file = "rpds_py-0.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:071980663c273bf3d388fe5c794c547e6f35ba3335477072c713a3176bf14a60"}, + {file = "rpds_py-0.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:726ac36e8a3bb8daef2fd482534cabc5e17334052447008405daca7ca04a3108"}, + {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9e557db6a177470316c82f023e5d571811c9a4422b5ea084c85da9aa3c035fc"}, + {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90123853fc8b1747f80b0d354be3d122b4365a93e50fc3aacc9fb4c2488845d6"}, + {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a61f659665a39a4d17d699ab3593d7116d66e1e2e3f03ef3fb8f484e91908808"}, + {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc97f0640e91d7776530f06e6836c546c1c752a52de158720c4224c9e8053cad"}, + {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a54e99a2b9693a37ebf245937fd6e9228b4cbd64b9cc961e1f3391ec6c7391"}, + {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd4b677d929cf1f6bac07ad76e0f2d5de367e6373351c01a9c0a39f6b21b4a8b"}, + {file = "rpds_py-0.16.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5ef00873303d678aaf8b0627e111fd434925ca01c657dbb2641410f1cdaef261"}, + {file = "rpds_py-0.16.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:349cb40897fd529ca15317c22c0eab67f5ac5178b5bd2c6adc86172045210acc"}, + {file = "rpds_py-0.16.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2ddef620e70eaffebed5932ce754d539c0930f676aae6212f8e16cd9743dd365"}, + {file = "rpds_py-0.16.2-cp38-none-win32.whl", hash = "sha256:882ce6e25e585949c3d9f9abd29202367175e0aab3aba0c58c9abbb37d4982ff"}, + {file = "rpds_py-0.16.2-cp38-none-win_amd64.whl", hash = "sha256:f4bd4578e44f26997e9e56c96dedc5f1af43cc9d16c4daa29c771a00b2a26851"}, + {file = "rpds_py-0.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:69ac7ea9897ec201ce68b48582f3eb34a3f9924488a5432a93f177bf76a82a7e"}, + {file = "rpds_py-0.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a9880b4656efe36ccad41edc66789e191e5ee19a1ea8811e0aed6f69851a82f4"}, + {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee94cb58c0ba2c62ee108c2b7c9131b2c66a29e82746e8fa3aa1a1effbd3dcf1"}, + {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24f7a2eb3866a9e91f4599851e0c8d39878a470044875c49bd528d2b9b88361c"}, + {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca57468da2d9a660bcf8961637c85f2fbb2aa64d9bc3f9484e30c3f9f67b1dd7"}, + {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccd4e400309e1f34a5095bf9249d371f0fd60f8a3a5c4a791cad7b99ce1fd38d"}, + {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80443fe2f7b3ea3934c5d75fb0e04a5dbb4a8e943e5ff2de0dec059202b70a8b"}, + {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d6a9f052e72d493efd92a77f861e45bab2f6be63e37fa8ecf0c6fd1a58fedb0"}, + {file = "rpds_py-0.16.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:35953f4f2b3216421af86fd236b7c0c65935936a94ea83ddbd4904ba60757773"}, + {file = "rpds_py-0.16.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:981d135c7cdaf6cd8eadae1c950de43b976de8f09d8e800feed307140d3d6d00"}, + {file = "rpds_py-0.16.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d0dd7ed2f16df2e129496e7fbe59a34bc2d7fc8db443a606644d069eb69cbd45"}, + {file = "rpds_py-0.16.2-cp39-none-win32.whl", hash = "sha256:703d95c75a72e902544fda08e965885525e297578317989fd15a6ce58414b41d"}, + {file = "rpds_py-0.16.2-cp39-none-win_amd64.whl", hash = "sha256:e93ec1b300acf89730cf27975ef574396bc04edecc358e9bd116fb387a123239"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:44627b6ca7308680a70766454db5249105fa6344853af6762eaad4158a2feebe"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3f91df8e6dbb7360e176d1affd5fb0246d2b88d16aa5ebc7db94fd66b68b61da"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d904c5693e08bad240f16d79305edba78276be87061c872a4a15e2c301fa2c0"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:290a81cfbe4673285cdf140ec5cd1658ffbf63ab359f2b352ebe172e7cfa5bf0"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b634c5ec0103c5cbebc24ebac4872b045cccb9456fc59efdcf6fe39775365bd2"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a297a4d08cc67c7466c873c78039d87840fb50d05473db0ec1b7b03d179bf322"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2e75e17bd0bb66ee34a707da677e47c14ee51ccef78ed6a263a4cc965a072a1"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f1b9d9260e06ea017feb7172976ab261e011c1dc2f8883c7c274f6b2aabfe01a"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:162d7cd9cd311c1b0ff1c55a024b8f38bd8aad1876b648821da08adc40e95734"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:9b32f742ce5b57201305f19c2ef7a184b52f6f9ba6871cc042c2a61f0d6b49b8"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac08472f41ea77cd6a5dae36ae7d4ed3951d6602833af87532b556c1b4601d63"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:495a14b72bbe217f2695dcd9b5ab14d4f8066a00f5d209ed94f0aca307f85f6e"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:8d6b6937ae9eac6d6c0ca3c42774d89fa311f55adff3970fb364b34abde6ed3d"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a61226465bda9283686db8f17d02569a98e4b13c637be5a26d44aa1f1e361c2"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5cf6af100ffb5c195beec11ffaa8cf8523057f123afa2944e6571d54da84cdc9"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6df15846ee3fb2e6397fe25d7ca6624af9f89587f3f259d177b556fed6bebe2c"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1be2f033df1b8be8c3167ba3c29d5dca425592ee31e35eac52050623afba5772"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96f957d6ab25a78b9e7fc9749d754b98eac825a112b4e666525ce89afcbd9ed5"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:088396c7c70e59872f67462fcac3ecbded5233385797021976a09ebd55961dfe"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4c46ad6356e1561f2a54f08367d1d2e70a0a1bb2db2282d2c1972c1d38eafc3b"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:47713dc4fce213f5c74ca8a1f6a59b622fc1b90868deb8e8e4d993e421b4b39d"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f811771019f063bbd0aa7bb72c8a934bc13ebacb4672d712fc1639cfd314cccc"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f19afcfc0dd0dca35694df441e9b0f95bc231b512f51bded3c3d8ca32153ec19"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a4b682c5775d6a3d21e314c10124599976809455ee67020e8e72df1769b87bc3"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c647ca87fc0ebe808a41de912e9a1bfef9acb85257e5d63691364ac16b81c1f0"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:302bd4983bbd47063e452c38be66153760112f6d3635c7eeefc094299fa400a9"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf721ede3eb7b829e4a9b8142bd55db0bdc82902720548a703f7e601ee13bdc3"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:358dafc89ce3894c7f486c615ba914609f38277ef67f566abc4c854d23b997fa"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cad0f59ee3dc35526039f4bc23642d52d5f6616b5f687d846bfc6d0d6d486db0"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cffa76b385dfe1e38527662a302b19ffb0e7f5cf7dd5e89186d2c94a22dd9d0c"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:83640a5d7cd3bff694747d50436b8b541b5b9b9782b0c8c1688931d6ee1a1f2d"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:ed99b4f7179d2111702020fd7d156e88acd533f5a7d3971353e568b6051d5c97"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4022b9dc620e14f30201a8a73898a873c8e910cb642bcd2f3411123bc527f6ac"}, + {file = "rpds_py-0.16.2.tar.gz", hash = "sha256:781ef8bfc091b19960fc0142a23aedadafa826bc32b433fdfe6fd7f964d7ef44"}, ] [[package]] name = "setuptools" -version = "69.0.2" +version = "69.0.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, - {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, ] [package.extras] @@ -628,13 +739,13 @@ files = [ [[package]] name = "types-jsonschema" -version = "4.20.0.0" +version = "4.20.0.20240105" description = "Typing stubs for jsonschema" optional = false python-versions = ">=3.8" files = [ - {file = "types-jsonschema-4.20.0.0.tar.gz", hash = "sha256:0de1032d243f1d3dba8b745ad84efe8c1af71665a9deb1827636ac535dcb79c1"}, - {file = "types_jsonschema-4.20.0.0-py3-none-any.whl", hash = "sha256:e6d5df18aaca4412f0aae246a294761a92040e93d7bc840f002b7329a8b72d26"}, + {file = "types-jsonschema-4.20.0.20240105.tar.gz", hash = "sha256:4a71af7e904498e7ad055149f6dc1eee04153b59a99ad7dd17aa3769c9bc5982"}, + {file = "types_jsonschema-4.20.0.20240105-py3-none-any.whl", hash = "sha256:26706cd70a273e59e718074c4e756608a25ba61327a7f9a4493ebd11941e5ad4"}, ] [package.dependencies] @@ -653,13 +764,13 @@ files = [ [[package]] name = "types-requests" -version = "2.31.0.10" +version = "2.31.0.20240106" description = "Typing stubs for requests" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "types-requests-2.31.0.10.tar.gz", hash = "sha256:dc5852a76f1eaf60eafa81a2e50aefa3d1f015c34cf0cba130930866b1b22a92"}, - {file = "types_requests-2.31.0.10-py3-none-any.whl", hash = "sha256:b32b9a86beffa876c0c3ac99a4cd3b8b51e973fb8e3bd4e0a6bb32c7efad80fc"}, + {file = "types-requests-2.31.0.20240106.tar.gz", hash = "sha256:0e1c731c17f33618ec58e022b614a1a2ecc25f7dc86800b36ef341380402c612"}, + {file = "types_requests-2.31.0.20240106-py3-none-any.whl", hash = "sha256:da997b3b6a72cc08d09f4dba9802fdbabc89104b35fe24ee588e674037689354"}, ] [package.dependencies] @@ -695,4 +806,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c32ec0e9d78c52a1b9f59e5f40c900d3ddfad6f05208c7ccc46e6a3b42e8b442" +content-hash = "d1e2a54bb61025323657af12ba1e9d4c43c3eac85e706a2e70ac55ec2854c44d" diff --git a/airbyte-lib/pyproject.toml b/airbyte-lib/pyproject.toml index e54106d957dc4..8868b5ae376fa 100644 --- a/airbyte-lib/pyproject.toml +++ b/airbyte-lib/pyproject.toml @@ -18,6 +18,7 @@ mypy = "^1.7.1" types-requests = "^2.31.0.10" types-jsonschema = "^4.20.0.0" pytest-mypy = "^0.10.3" +pdoc = "^14.3.0" [build-system] requires = ["poetry-core"] @@ -30,4 +31,5 @@ ignore_missing_imports = true addopts = "--mypy" [tool.poetry.scripts] -airbyte-lib-validate-source = "airbyte_lib.validate:run" \ No newline at end of file +generate-docs = "docs:run" +airbyte-lib-validate-source = "airbyte_lib.validate:run" diff --git a/airbyte-lib/tests/docs_tests/__init__.py b/airbyte-lib/tests/docs_tests/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/airbyte-lib/tests/docs_tests/test_docs_checked_in.py b/airbyte-lib/tests/docs_tests/test_docs_checked_in.py new file mode 100644 index 0000000000000..54614c7cd6210 --- /dev/null +++ b/airbyte-lib/tests/docs_tests/test_docs_checked_in.py @@ -0,0 +1,22 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import os + +import docs + + +def test_docs_checked_in(): + """ + Docs need to be generated via `poetry run generate-docs` and checked in to the repo. + + This test runs the docs generation and compares the output with the checked in docs. + It will fail if there are any differences. + """ + + docs.run() + + # compare the generated docs with the checked in docs + diff = os.system("git diff --exit-code docs/generated") + + # if there is a diff, fail the test + assert diff == 0, "Docs are out of date. Please run `poetry run generate-docs` and commit the changes." From 99488ddcc50fd16b8753019864e80b37b947a76d Mon Sep 17 00:00:00 2001 From: Augustin Date: Tue, 9 Jan 2024 16:30:49 +0100 Subject: [PATCH 023/574] airbyte-ci: fix assertion error on report existence (#33979) --- airbyte-ci/connectors/pipelines/README.md | 1 + .../pipelines/airbyte_ci/connectors/publish/context.py | 1 + .../pipelines/pipelines/airbyte_ci/metadata/pipeline.py | 9 ++++++--- .../pipelines/models/contexts/pipeline_context.py | 3 +-- airbyte-ci/connectors/pipelines/pyproject.toml | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/airbyte-ci/connectors/pipelines/README.md b/airbyte-ci/connectors/pipelines/README.md index ad890b75397d2..0eb970821c0b1 100644 --- a/airbyte-ci/connectors/pipelines/README.md +++ b/airbyte-ci/connectors/pipelines/README.md @@ -521,6 +521,7 @@ E.G.: running `pytest` on a specific test folder: | Version | PR | Description | | ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| 3.1.1 | [#33979](https://github.com/airbytehq/airbyte/pull/33979) | Fix AssertionError on report existence again | | 3.1.0 | [#33994](https://github.com/airbytehq/airbyte/pull/33994) | Log more context information in CI. | | 3.0.2 | [#33987](https://github.com/airbytehq/airbyte/pull/33987) | Fix type checking issue when running --help | | 3.0.1 | [#33981](https://github.com/airbytehq/airbyte/pull/33981) | Fix issues with deploying dagster, pin pendulum version in dagster-cli install | diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/publish/context.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/publish/context.py index 5bd5a29f91754..a4471bac7ecaf 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/publish/context.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/publish/context.py @@ -120,6 +120,7 @@ def create_slack_message(self) -> str: message += "🔴" message += f" {self.state.value['description']}\n" if self.state is ContextState.SUCCESSFUL: + assert self.report is not None, "Report should be set when state is successful" message += f"⏲️ Run duration: {format_duration(self.report.run_duration)}\n" if self.state is ContextState.FAILURE: message += "\ncc. " # @dev-connector-ops diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/pipeline.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/pipeline.py index 30a84d3337e25..eae516a8db79f 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/pipeline.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/pipeline.py @@ -164,6 +164,8 @@ async def run_metadata_orchestrator_deploy_pipeline( pipeline_start_timestamp: Optional[int], ci_context: Optional[str], ) -> bool: + success: bool = False + metadata_pipeline_context = PipelineContext( pipeline_name="Metadata Service Orchestrator Unit Test Pipeline", is_local=is_local, @@ -175,7 +177,6 @@ async def run_metadata_orchestrator_deploy_pipeline( pipeline_start_timestamp=pipeline_start_timestamp, ci_context=ci_context, ) - async with dagger.Connection(DAGGER_CONFIG) as dagger_client: metadata_pipeline_context.dagger_client = dagger_client.pipeline(metadata_pipeline_context.pipeline_name) @@ -196,9 +197,11 @@ async def run_metadata_orchestrator_deploy_pipeline( ], ] steps_results = await run_steps(steps) - metadata_pipeline_context.report = Report( + report = Report( pipeline_context=metadata_pipeline_context, steps_results=list(steps_results.values()), name="METADATA ORCHESTRATOR DEPLOY RESULTS", ) - return metadata_pipeline_context.report.success + metadata_pipeline_context.report = report + success = report.success + return success diff --git a/airbyte-ci/connectors/pipelines/pipelines/models/contexts/pipeline_context.py b/airbyte-ci/connectors/pipelines/pipelines/models/contexts/pipeline_context.py index 0495bfb01c695..83b1cb8e4b6d7 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/models/contexts/pipeline_context.py +++ b/airbyte-ci/connectors/pipelines/pipelines/models/contexts/pipeline_context.py @@ -149,8 +149,7 @@ def repo(self) -> GitRepository: return self.dagger_client.git(AIRBYTE_REPO_URL, keep_git_dir=True) @property - def report(self) -> Report | ConnectorReport: - assert self._report is not None, "The report was not set on this PipelineContext." + def report(self) -> Report | ConnectorReport | None: return self._report @report.setter diff --git a/airbyte-ci/connectors/pipelines/pyproject.toml b/airbyte-ci/connectors/pipelines/pyproject.toml index faf30e9fc8add..d370f0a8e71bc 100644 --- a/airbyte-ci/connectors/pipelines/pyproject.toml +++ b/airbyte-ci/connectors/pipelines/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pipelines" -version = "3.1.0" +version = "3.1.1" description = "Packaged maintained by the connector operations team to perform CI for connectors' pipelines" authors = ["Airbyte "] From 07646783e29b29160a83820dd6454cf3c1df0a90 Mon Sep 17 00:00:00 2001 From: Daryna Ishchenko <80129833+darynaishchenko@users.noreply.github.com> Date: Tue, 9 Jan 2024 18:35:19 +0200 Subject: [PATCH 024/574] :bug: Source Google Ads: added handling for 401 error while parsing response. added metrics.cost_micros to ad_groups stream (#33494) --- .../acceptance-test-config.yml | 49 +++++++++++++++++++ .../integration_tests/expected_records.jsonl | 6 +-- .../expected_records_click.jsonl | 14 +++--- .../source-google-ads/metadata.yaml | 2 +- .../source_google_ads/schemas/ad_group.json | 3 ++ .../source_google_ads/streams.py | 4 +- .../source_google_ads/utils.py | 10 +++- .../unit_tests/test_streams.py | 22 ++++++++- docs/integrations/sources/google-ads.md | 1 + 9 files changed, 95 insertions(+), 16 deletions(-) diff --git a/airbyte-integrations/connectors/source-google-ads/acceptance-test-config.yml b/airbyte-integrations/connectors/source-google-ads/acceptance-test-config.yml index 0b639314c717a..10b39b2b95465 100644 --- a/airbyte-integrations/connectors/source-google-ads/acceptance-test-config.yml +++ b/airbyte-integrations/connectors/source-google-ads/acceptance-test-config.yml @@ -67,9 +67,58 @@ acceptance_tests: campaign_budget: - name: campaign_budget.recommended_budget_estimated_change_weekly_interactions bypass_reason: "Value can be updated by Google Ads" + - name: metrics.all_conversions + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.all_conversions_from_interactions_rate + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.all_conversions_value + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.conversions + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.conversions_from_interactions_rate + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.conversions_value + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.cost_per_all_conversions + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.cost_per_conversion + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.value_per_all_conversions + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.value_per_conversion + bypass_reason: "Value can be updated by Google Ads" campaign: - name: campaign.optimization_score bypass_reason: "Value can be updated by Google Ads" + ad_group_ad_legacy: + - name: metrics.all_conversions_from_interactions_rate + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.all_conversions_value + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.all_conversions + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.conversions_from_interactions_rate + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.conversions_value + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.conversions + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.cost_per_all_conversions + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.cost_per_conversion + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.cost_per_current_model_attributed_conversion + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.current_model_attributed_conversions_value + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.current_model_attributed_conversions + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.value_per_all_conversions + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.value_per_conversion + bypass_reason: "Value can be updated by Google Ads" + - name: metrics.value_per_current_model_attributed_conversion + bypass_reason: "Value can be updated by Google Ads" full_refresh: tests: - config_path: "secrets/config.json" diff --git a/airbyte-integrations/connectors/source-google-ads/integration_tests/expected_records.jsonl b/airbyte-integrations/connectors/source-google-ads/integration_tests/expected_records.jsonl index 547a452b09d80..875167ce8290b 100644 --- a/airbyte-integrations/connectors/source-google-ads/integration_tests/expected_records.jsonl +++ b/airbyte-integrations/connectors/source-google-ads/integration_tests/expected_records.jsonl @@ -20,9 +20,9 @@ {"stream": "ad_group_ad", "data": {"ad_group.id": 137020701042, "ad_group_ad.ad.added_by_google_ads": false, "ad_group_ad.ad.app_ad.descriptions": [], "ad_group_ad.ad.app_ad.headlines": [], "ad_group_ad.ad.app_ad.html5_media_bundles": [], "ad_group_ad.ad.app_ad.images": [], "ad_group_ad.ad.app_ad.mandatory_ad_text": "", "ad_group_ad.ad.app_ad.youtube_videos": [], "ad_group_ad.ad.app_engagement_ad.descriptions": [], "ad_group_ad.ad.app_engagement_ad.headlines": [], "ad_group_ad.ad.app_engagement_ad.images": [], "ad_group_ad.ad.app_engagement_ad.videos": [], "ad_group_ad.ad.call_ad.business_name": "", "ad_group_ad.ad.call_ad.call_tracked": false, "ad_group_ad.ad.call_ad.conversion_action": "", "ad_group_ad.ad.call_ad.conversion_reporting_state": "UNSPECIFIED", "ad_group_ad.ad.call_ad.country_code": "", "ad_group_ad.ad.call_ad.description1": "", "ad_group_ad.ad.call_ad.description2": "", "ad_group_ad.ad.call_ad.disable_call_conversion": false, "ad_group_ad.ad.call_ad.headline1": "", "ad_group_ad.ad.call_ad.headline2": "", "ad_group_ad.ad.call_ad.path1": "", "ad_group_ad.ad.call_ad.path2": "", "ad_group_ad.ad.call_ad.phone_number": "", "ad_group_ad.ad.call_ad.phone_number_verification_url": "", "ad_group_ad.ad.device_preference": "UNSPECIFIED", "ad_group_ad.ad.display_upload_ad.display_upload_product_type": "UNSPECIFIED", "ad_group_ad.ad.display_upload_ad.media_bundle": "", "ad_group_ad.ad.display_url": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.description": "", "ad_group_ad.ad.expanded_text_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.headline_part1": "", "ad_group_ad.ad.expanded_text_ad.headline_part2": "", "ad_group_ad.ad.expanded_text_ad.headline_part3": "", "ad_group_ad.ad.expanded_text_ad.path1": "", "ad_group_ad.ad.expanded_text_ad.path2": "", "ad_group_ad.ad.final_app_urls": [], "ad_group_ad.ad.final_mobile_urls": [], "ad_group_ad.ad.final_url_suffix": "", "ad_group_ad.ad.final_urls": ["https://airbyte.com"], "ad_group_ad.ad.hotel_ad": "", "ad_group_ad.ad.id": 592078631218, "ad_group_ad.ad.image_ad.image_url": "", "ad_group_ad.ad.image_ad.mime_type": "UNSPECIFIED", "ad_group_ad.ad.image_ad.name": "", "ad_group_ad.ad.image_ad.pixel_height": 0, "ad_group_ad.ad.image_ad.pixel_width": 0, "ad_group_ad.ad.image_ad.preview_image_url": "", "ad_group_ad.ad.image_ad.preview_pixel_height": 0, "ad_group_ad.ad.image_ad.preview_pixel_width": 0, "ad_group_ad.ad.legacy_app_install_ad": "", "ad_group_ad.ad.legacy_responsive_display_ad.accent_color": "", "ad_group_ad.ad.legacy_responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.legacy_responsive_display_ad.business_name": "", "ad_group_ad.ad.legacy_responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.legacy_responsive_display_ad.description": "", "ad_group_ad.ad.legacy_responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.legacy_responsive_display_ad.logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.long_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.main_color": "", "ad_group_ad.ad.legacy_responsive_display_ad.marketing_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.price_prefix": "", "ad_group_ad.ad.legacy_responsive_display_ad.promo_text": "", "ad_group_ad.ad.legacy_responsive_display_ad.short_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_marketing_image": "", "ad_group_ad.ad.local_ad.call_to_actions": [], "ad_group_ad.ad.local_ad.descriptions": [], "ad_group_ad.ad.local_ad.headlines": [], "ad_group_ad.ad.local_ad.logo_images": [], "ad_group_ad.ad.local_ad.marketing_images": [], "ad_group_ad.ad.local_ad.path1": "", "ad_group_ad.ad.local_ad.path2": "", "ad_group_ad.ad.local_ad.videos": [], "ad_group_ad.ad.name": "", "ad_group_ad.ad.resource_name": "customers/4651612872/ads/592078631218", "ad_group_ad.ad.responsive_display_ad.accent_color": "", "ad_group_ad.ad.responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.responsive_display_ad.business_name": "", "ad_group_ad.ad.responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.responsive_display_ad.control_spec.enable_asset_enhancements": false, "ad_group_ad.ad.responsive_display_ad.control_spec.enable_autogen_video": false, "ad_group_ad.ad.responsive_display_ad.descriptions": [], "ad_group_ad.ad.responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.responsive_display_ad.headlines": [], "ad_group_ad.ad.responsive_display_ad.logo_images": [], "ad_group_ad.ad.responsive_display_ad.long_headline": "", "ad_group_ad.ad.responsive_display_ad.main_color": "", "ad_group_ad.ad.responsive_display_ad.marketing_images": [], "ad_group_ad.ad.responsive_display_ad.price_prefix": "", "ad_group_ad.ad.responsive_display_ad.promo_text": "", "ad_group_ad.ad.responsive_display_ad.square_logo_images": [], "ad_group_ad.ad.responsive_display_ad.square_marketing_images": [], "ad_group_ad.ad.responsive_display_ad.youtube_videos": [], "ad_group_ad.ad.responsive_search_ad.descriptions": ["text: \"Behind The Scenes: Testing The Airbyte Maintainer Program\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Airbyte | Open-Source Data Integration Platform | ELT tool\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Upgrading Our Discourse And Slack To Support Our Community Growth\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Consolidate your data in your data warehouses, lakes and databases\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.headlines": ["text: \"Airbyte\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"ELT tool\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open-source Data Integration\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.path1": "", "ad_group_ad.ad.responsive_search_ad.path2": "", "ad_group_ad.ad.shopping_comparison_listing_ad.headline": "", "ad_group_ad.ad.shopping_product_ad": "", "ad_group_ad.ad.shopping_smart_ad": "", "ad_group_ad.ad.smart_campaign_ad.descriptions": [], "ad_group_ad.ad.smart_campaign_ad.headlines": [], "ad_group_ad.ad.system_managed_resource_source": "UNSPECIFIED", "ad_group_ad.ad.text_ad.description1": "", "ad_group_ad.ad.text_ad.description2": "", "ad_group_ad.ad.text_ad.headline": "", "ad_group_ad.ad.tracking_url_template": "", "ad_group_ad.ad.type": "RESPONSIVE_SEARCH_AD", "ad_group_ad.ad.url_collections": [], "ad_group_ad.ad.url_custom_parameters": [], "ad_group_ad.ad.video_ad.in_feed.description1": "", "ad_group_ad.ad.video_ad.in_feed.description2": "", "ad_group_ad.ad.video_ad.in_feed.headline": "", "ad_group_ad.ad.video_ad.in_stream.action_button_label": "", "ad_group_ad.ad.video_ad.in_stream.action_headline": "", "ad_group_ad.ad.video_ad.out_stream.description": "", "ad_group_ad.ad.video_ad.out_stream.headline": "", "ad_group_ad.ad.video_responsive_ad.call_to_actions": [], "ad_group_ad.ad.video_responsive_ad.companion_banners": [], "ad_group_ad.ad.video_responsive_ad.descriptions": [], "ad_group_ad.ad.video_responsive_ad.headlines": [], "ad_group_ad.ad.video_responsive_ad.long_headlines": [], "ad_group_ad.ad.video_responsive_ad.videos": [], "ad_group_ad.ad_group": "customers/4651612872/adGroups/137020701042", "ad_group_ad.ad_strength": "POOR", "ad_group_ad.labels": ["customers/4651612872/labels/21906377810"], "ad_group_ad.policy_summary.approval_status": "APPROVED", "ad_group_ad.policy_summary.policy_topic_entries": [], "ad_group_ad.policy_summary.review_status": "REVIEWED", "ad_group_ad.resource_name": "customers/4651612872/adGroupAds/137020701042~592078631218", "ad_group_ad.status": "ENABLED", "segments.date": "2022-05-18"}, "emitted_at": 1704407765438} {"stream": "ad_group_ad", "data": {"ad_group.id": 137020701042, "ad_group_ad.ad.added_by_google_ads": false, "ad_group_ad.ad.app_ad.descriptions": [], "ad_group_ad.ad.app_ad.headlines": [], "ad_group_ad.ad.app_ad.html5_media_bundles": [], "ad_group_ad.ad.app_ad.images": [], "ad_group_ad.ad.app_ad.mandatory_ad_text": "", "ad_group_ad.ad.app_ad.youtube_videos": [], "ad_group_ad.ad.app_engagement_ad.descriptions": [], "ad_group_ad.ad.app_engagement_ad.headlines": [], "ad_group_ad.ad.app_engagement_ad.images": [], "ad_group_ad.ad.app_engagement_ad.videos": [], "ad_group_ad.ad.call_ad.business_name": "", "ad_group_ad.ad.call_ad.call_tracked": false, "ad_group_ad.ad.call_ad.conversion_action": "", "ad_group_ad.ad.call_ad.conversion_reporting_state": "UNSPECIFIED", "ad_group_ad.ad.call_ad.country_code": "", "ad_group_ad.ad.call_ad.description1": "", "ad_group_ad.ad.call_ad.description2": "", "ad_group_ad.ad.call_ad.disable_call_conversion": false, "ad_group_ad.ad.call_ad.headline1": "", "ad_group_ad.ad.call_ad.headline2": "", "ad_group_ad.ad.call_ad.path1": "", "ad_group_ad.ad.call_ad.path2": "", "ad_group_ad.ad.call_ad.phone_number": "", "ad_group_ad.ad.call_ad.phone_number_verification_url": "", "ad_group_ad.ad.device_preference": "UNSPECIFIED", "ad_group_ad.ad.display_upload_ad.display_upload_product_type": "UNSPECIFIED", "ad_group_ad.ad.display_upload_ad.media_bundle": "", "ad_group_ad.ad.display_url": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.description": "", "ad_group_ad.ad.expanded_text_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.headline_part1": "", "ad_group_ad.ad.expanded_text_ad.headline_part2": "", "ad_group_ad.ad.expanded_text_ad.headline_part3": "", "ad_group_ad.ad.expanded_text_ad.path1": "", "ad_group_ad.ad.expanded_text_ad.path2": "", "ad_group_ad.ad.final_app_urls": [], "ad_group_ad.ad.final_mobile_urls": [], "ad_group_ad.ad.final_url_suffix": "", "ad_group_ad.ad.final_urls": ["https://airbyte.com"], "ad_group_ad.ad.hotel_ad": "", "ad_group_ad.ad.id": 592078631218, "ad_group_ad.ad.image_ad.image_url": "", "ad_group_ad.ad.image_ad.mime_type": "UNSPECIFIED", "ad_group_ad.ad.image_ad.name": "", "ad_group_ad.ad.image_ad.pixel_height": 0, "ad_group_ad.ad.image_ad.pixel_width": 0, "ad_group_ad.ad.image_ad.preview_image_url": "", "ad_group_ad.ad.image_ad.preview_pixel_height": 0, "ad_group_ad.ad.image_ad.preview_pixel_width": 0, "ad_group_ad.ad.legacy_app_install_ad": "", "ad_group_ad.ad.legacy_responsive_display_ad.accent_color": "", "ad_group_ad.ad.legacy_responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.legacy_responsive_display_ad.business_name": "", "ad_group_ad.ad.legacy_responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.legacy_responsive_display_ad.description": "", "ad_group_ad.ad.legacy_responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.legacy_responsive_display_ad.logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.long_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.main_color": "", "ad_group_ad.ad.legacy_responsive_display_ad.marketing_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.price_prefix": "", "ad_group_ad.ad.legacy_responsive_display_ad.promo_text": "", "ad_group_ad.ad.legacy_responsive_display_ad.short_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_marketing_image": "", "ad_group_ad.ad.local_ad.call_to_actions": [], "ad_group_ad.ad.local_ad.descriptions": [], "ad_group_ad.ad.local_ad.headlines": [], "ad_group_ad.ad.local_ad.logo_images": [], "ad_group_ad.ad.local_ad.marketing_images": [], "ad_group_ad.ad.local_ad.path1": "", "ad_group_ad.ad.local_ad.path2": "", "ad_group_ad.ad.local_ad.videos": [], "ad_group_ad.ad.name": "", "ad_group_ad.ad.resource_name": "customers/4651612872/ads/592078631218", "ad_group_ad.ad.responsive_display_ad.accent_color": "", "ad_group_ad.ad.responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.responsive_display_ad.business_name": "", "ad_group_ad.ad.responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.responsive_display_ad.control_spec.enable_asset_enhancements": false, "ad_group_ad.ad.responsive_display_ad.control_spec.enable_autogen_video": false, "ad_group_ad.ad.responsive_display_ad.descriptions": [], "ad_group_ad.ad.responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.responsive_display_ad.headlines": [], "ad_group_ad.ad.responsive_display_ad.logo_images": [], "ad_group_ad.ad.responsive_display_ad.long_headline": "", "ad_group_ad.ad.responsive_display_ad.main_color": "", "ad_group_ad.ad.responsive_display_ad.marketing_images": [], "ad_group_ad.ad.responsive_display_ad.price_prefix": "", "ad_group_ad.ad.responsive_display_ad.promo_text": "", "ad_group_ad.ad.responsive_display_ad.square_logo_images": [], "ad_group_ad.ad.responsive_display_ad.square_marketing_images": [], "ad_group_ad.ad.responsive_display_ad.youtube_videos": [], "ad_group_ad.ad.responsive_search_ad.descriptions": ["text: \"Behind The Scenes: Testing The Airbyte Maintainer Program\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Airbyte | Open-Source Data Integration Platform | ELT tool\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Upgrading Our Discourse And Slack To Support Our Community Growth\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Consolidate your data in your data warehouses, lakes and databases\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.headlines": ["text: \"Airbyte\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"ELT tool\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open-source Data Integration\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.path1": "", "ad_group_ad.ad.responsive_search_ad.path2": "", "ad_group_ad.ad.shopping_comparison_listing_ad.headline": "", "ad_group_ad.ad.shopping_product_ad": "", "ad_group_ad.ad.shopping_smart_ad": "", "ad_group_ad.ad.smart_campaign_ad.descriptions": [], "ad_group_ad.ad.smart_campaign_ad.headlines": [], "ad_group_ad.ad.system_managed_resource_source": "UNSPECIFIED", "ad_group_ad.ad.text_ad.description1": "", "ad_group_ad.ad.text_ad.description2": "", "ad_group_ad.ad.text_ad.headline": "", "ad_group_ad.ad.tracking_url_template": "", "ad_group_ad.ad.type": "RESPONSIVE_SEARCH_AD", "ad_group_ad.ad.url_collections": [], "ad_group_ad.ad.url_custom_parameters": [], "ad_group_ad.ad.video_ad.in_feed.description1": "", "ad_group_ad.ad.video_ad.in_feed.description2": "", "ad_group_ad.ad.video_ad.in_feed.headline": "", "ad_group_ad.ad.video_ad.in_stream.action_button_label": "", "ad_group_ad.ad.video_ad.in_stream.action_headline": "", "ad_group_ad.ad.video_ad.out_stream.description": "", "ad_group_ad.ad.video_ad.out_stream.headline": "", "ad_group_ad.ad.video_responsive_ad.call_to_actions": [], "ad_group_ad.ad.video_responsive_ad.companion_banners": [], "ad_group_ad.ad.video_responsive_ad.descriptions": [], "ad_group_ad.ad.video_responsive_ad.headlines": [], "ad_group_ad.ad.video_responsive_ad.long_headlines": [], "ad_group_ad.ad.video_responsive_ad.videos": [], "ad_group_ad.ad_group": "customers/4651612872/adGroups/137020701042", "ad_group_ad.ad_strength": "POOR", "ad_group_ad.labels": ["customers/4651612872/labels/21906377810"], "ad_group_ad.policy_summary.approval_status": "APPROVED", "ad_group_ad.policy_summary.policy_topic_entries": [], "ad_group_ad.policy_summary.review_status": "REVIEWED", "ad_group_ad.resource_name": "customers/4651612872/adGroupAds/137020701042~592078631218", "ad_group_ad.status": "ENABLED", "segments.date": "2022-05-19"}, "emitted_at": 1704407765455} {"stream": "ad_group_ad", "data": {"ad_group.id": 137020701042, "ad_group_ad.ad.added_by_google_ads": false, "ad_group_ad.ad.app_ad.descriptions": [], "ad_group_ad.ad.app_ad.headlines": [], "ad_group_ad.ad.app_ad.html5_media_bundles": [], "ad_group_ad.ad.app_ad.images": [], "ad_group_ad.ad.app_ad.mandatory_ad_text": "", "ad_group_ad.ad.app_ad.youtube_videos": [], "ad_group_ad.ad.app_engagement_ad.descriptions": [], "ad_group_ad.ad.app_engagement_ad.headlines": [], "ad_group_ad.ad.app_engagement_ad.images": [], "ad_group_ad.ad.app_engagement_ad.videos": [], "ad_group_ad.ad.call_ad.business_name": "", "ad_group_ad.ad.call_ad.call_tracked": false, "ad_group_ad.ad.call_ad.conversion_action": "", "ad_group_ad.ad.call_ad.conversion_reporting_state": "UNSPECIFIED", "ad_group_ad.ad.call_ad.country_code": "", "ad_group_ad.ad.call_ad.description1": "", "ad_group_ad.ad.call_ad.description2": "", "ad_group_ad.ad.call_ad.disable_call_conversion": false, "ad_group_ad.ad.call_ad.headline1": "", "ad_group_ad.ad.call_ad.headline2": "", "ad_group_ad.ad.call_ad.path1": "", "ad_group_ad.ad.call_ad.path2": "", "ad_group_ad.ad.call_ad.phone_number": "", "ad_group_ad.ad.call_ad.phone_number_verification_url": "", "ad_group_ad.ad.device_preference": "UNSPECIFIED", "ad_group_ad.ad.display_upload_ad.display_upload_product_type": "UNSPECIFIED", "ad_group_ad.ad.display_upload_ad.media_bundle": "", "ad_group_ad.ad.display_url": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.description": "", "ad_group_ad.ad.expanded_text_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.headline_part1": "", "ad_group_ad.ad.expanded_text_ad.headline_part2": "", "ad_group_ad.ad.expanded_text_ad.headline_part3": "", "ad_group_ad.ad.expanded_text_ad.path1": "", "ad_group_ad.ad.expanded_text_ad.path2": "", "ad_group_ad.ad.final_app_urls": [], "ad_group_ad.ad.final_mobile_urls": [], "ad_group_ad.ad.final_url_suffix": "", "ad_group_ad.ad.final_urls": ["https://airbyte.com"], "ad_group_ad.ad.hotel_ad": "", "ad_group_ad.ad.id": 592078631218, "ad_group_ad.ad.image_ad.image_url": "", "ad_group_ad.ad.image_ad.mime_type": "UNSPECIFIED", "ad_group_ad.ad.image_ad.name": "", "ad_group_ad.ad.image_ad.pixel_height": 0, "ad_group_ad.ad.image_ad.pixel_width": 0, "ad_group_ad.ad.image_ad.preview_image_url": "", "ad_group_ad.ad.image_ad.preview_pixel_height": 0, "ad_group_ad.ad.image_ad.preview_pixel_width": 0, "ad_group_ad.ad.legacy_app_install_ad": "", "ad_group_ad.ad.legacy_responsive_display_ad.accent_color": "", "ad_group_ad.ad.legacy_responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.legacy_responsive_display_ad.business_name": "", "ad_group_ad.ad.legacy_responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.legacy_responsive_display_ad.description": "", "ad_group_ad.ad.legacy_responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.legacy_responsive_display_ad.logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.long_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.main_color": "", "ad_group_ad.ad.legacy_responsive_display_ad.marketing_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.price_prefix": "", "ad_group_ad.ad.legacy_responsive_display_ad.promo_text": "", "ad_group_ad.ad.legacy_responsive_display_ad.short_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_marketing_image": "", "ad_group_ad.ad.local_ad.call_to_actions": [], "ad_group_ad.ad.local_ad.descriptions": [], "ad_group_ad.ad.local_ad.headlines": [], "ad_group_ad.ad.local_ad.logo_images": [], "ad_group_ad.ad.local_ad.marketing_images": [], "ad_group_ad.ad.local_ad.path1": "", "ad_group_ad.ad.local_ad.path2": "", "ad_group_ad.ad.local_ad.videos": [], "ad_group_ad.ad.name": "", "ad_group_ad.ad.resource_name": "customers/4651612872/ads/592078631218", "ad_group_ad.ad.responsive_display_ad.accent_color": "", "ad_group_ad.ad.responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.responsive_display_ad.business_name": "", "ad_group_ad.ad.responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.responsive_display_ad.control_spec.enable_asset_enhancements": false, "ad_group_ad.ad.responsive_display_ad.control_spec.enable_autogen_video": false, "ad_group_ad.ad.responsive_display_ad.descriptions": [], "ad_group_ad.ad.responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.responsive_display_ad.headlines": [], "ad_group_ad.ad.responsive_display_ad.logo_images": [], "ad_group_ad.ad.responsive_display_ad.long_headline": "", "ad_group_ad.ad.responsive_display_ad.main_color": "", "ad_group_ad.ad.responsive_display_ad.marketing_images": [], "ad_group_ad.ad.responsive_display_ad.price_prefix": "", "ad_group_ad.ad.responsive_display_ad.promo_text": "", "ad_group_ad.ad.responsive_display_ad.square_logo_images": [], "ad_group_ad.ad.responsive_display_ad.square_marketing_images": [], "ad_group_ad.ad.responsive_display_ad.youtube_videos": [], "ad_group_ad.ad.responsive_search_ad.descriptions": ["text: \"Behind The Scenes: Testing The Airbyte Maintainer Program\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Airbyte | Open-Source Data Integration Platform | ELT tool\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Upgrading Our Discourse And Slack To Support Our Community Growth\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Consolidate your data in your data warehouses, lakes and databases\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.headlines": ["text: \"Airbyte\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"ELT tool\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open-source Data Integration\"\nasset_performance_label: PENDING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.path1": "", "ad_group_ad.ad.responsive_search_ad.path2": "", "ad_group_ad.ad.shopping_comparison_listing_ad.headline": "", "ad_group_ad.ad.shopping_product_ad": "", "ad_group_ad.ad.shopping_smart_ad": "", "ad_group_ad.ad.smart_campaign_ad.descriptions": [], "ad_group_ad.ad.smart_campaign_ad.headlines": [], "ad_group_ad.ad.system_managed_resource_source": "UNSPECIFIED", "ad_group_ad.ad.text_ad.description1": "", "ad_group_ad.ad.text_ad.description2": "", "ad_group_ad.ad.text_ad.headline": "", "ad_group_ad.ad.tracking_url_template": "", "ad_group_ad.ad.type": "RESPONSIVE_SEARCH_AD", "ad_group_ad.ad.url_collections": [], "ad_group_ad.ad.url_custom_parameters": [], "ad_group_ad.ad.video_ad.in_feed.description1": "", "ad_group_ad.ad.video_ad.in_feed.description2": "", "ad_group_ad.ad.video_ad.in_feed.headline": "", "ad_group_ad.ad.video_ad.in_stream.action_button_label": "", "ad_group_ad.ad.video_ad.in_stream.action_headline": "", "ad_group_ad.ad.video_ad.out_stream.description": "", "ad_group_ad.ad.video_ad.out_stream.headline": "", "ad_group_ad.ad.video_responsive_ad.call_to_actions": [], "ad_group_ad.ad.video_responsive_ad.companion_banners": [], "ad_group_ad.ad.video_responsive_ad.descriptions": [], "ad_group_ad.ad.video_responsive_ad.headlines": [], "ad_group_ad.ad.video_responsive_ad.long_headlines": [], "ad_group_ad.ad.video_responsive_ad.videos": [], "ad_group_ad.ad_group": "customers/4651612872/adGroups/137020701042", "ad_group_ad.ad_strength": "POOR", "ad_group_ad.labels": ["customers/4651612872/labels/21906377810"], "ad_group_ad.policy_summary.approval_status": "APPROVED", "ad_group_ad.policy_summary.policy_topic_entries": [], "ad_group_ad.policy_summary.review_status": "REVIEWED", "ad_group_ad.resource_name": "customers/4651612872/adGroupAds/137020701042~592078631218", "ad_group_ad.status": "ENABLED", "segments.date": "2022-05-20"}, "emitted_at": 1704407765456} -{"stream": "ad_group", "data": {"campaign.id": 16820250687, "ad_group.ad_rotation_mode": "UNSPECIFIED", "ad_group.base_ad_group": "customers/4651612872/adGroups/137020701042", "ad_group.campaign": "customers/4651612872/campaigns/16820250687", "ad_group.cpc_bid_micros": 10000, "ad_group.cpm_bid_micros": 10000, "ad_group.cpv_bid_micros": 0, "ad_group.display_custom_bid_dimension": "UNSPECIFIED", "ad_group.effective_target_cpa_micros": 0, "ad_group.effective_target_cpa_source": "UNSPECIFIED", "ad_group.effective_target_roas": 0.0, "ad_group.effective_target_roas_source": "UNSPECIFIED", "ad_group.excluded_parent_asset_field_types": [], "ad_group.optimized_targeting_enabled": false, "ad_group.final_url_suffix": "", "ad_group.id": 137020701042, "ad_group.labels": ["customers/4651612872/labels/21906377810"], "ad_group.name": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0439\u00a02", "ad_group.percent_cpc_bid_micros": 0, "ad_group.resource_name": "customers/4651612872/adGroups/137020701042", "ad_group.status": "ENABLED", "ad_group.target_cpa_micros": 0, "ad_group.target_cpm_micros": 10000, "ad_group.target_roas": 0.0, "ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AUDIENCE\nbid_only: true\n", "targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "ad_group.tracking_url_template": "", "ad_group.type": "SEARCH_STANDARD", "ad_group.url_custom_parameters": [], "segments.date": "2022-05-18"}, "emitted_at": 1704407767210} -{"stream": "ad_group", "data": {"campaign.id": 16820250687, "ad_group.ad_rotation_mode": "UNSPECIFIED", "ad_group.base_ad_group": "customers/4651612872/adGroups/137020701042", "ad_group.campaign": "customers/4651612872/campaigns/16820250687", "ad_group.cpc_bid_micros": 10000, "ad_group.cpm_bid_micros": 10000, "ad_group.cpv_bid_micros": 0, "ad_group.display_custom_bid_dimension": "UNSPECIFIED", "ad_group.effective_target_cpa_micros": 0, "ad_group.effective_target_cpa_source": "UNSPECIFIED", "ad_group.effective_target_roas": 0.0, "ad_group.effective_target_roas_source": "UNSPECIFIED", "ad_group.excluded_parent_asset_field_types": [], "ad_group.optimized_targeting_enabled": false, "ad_group.final_url_suffix": "", "ad_group.id": 137020701042, "ad_group.labels": ["customers/4651612872/labels/21906377810"], "ad_group.name": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0439\u00a02", "ad_group.percent_cpc_bid_micros": 0, "ad_group.resource_name": "customers/4651612872/adGroups/137020701042", "ad_group.status": "ENABLED", "ad_group.target_cpa_micros": 0, "ad_group.target_cpm_micros": 10000, "ad_group.target_roas": 0.0, "ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AUDIENCE\nbid_only: true\n", "targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "ad_group.tracking_url_template": "", "ad_group.type": "SEARCH_STANDARD", "ad_group.url_custom_parameters": [], "segments.date": "2022-05-19"}, "emitted_at": 1704407767216} -{"stream": "ad_group", "data": {"campaign.id": 16820250687, "ad_group.ad_rotation_mode": "UNSPECIFIED", "ad_group.base_ad_group": "customers/4651612872/adGroups/137020701042", "ad_group.campaign": "customers/4651612872/campaigns/16820250687", "ad_group.cpc_bid_micros": 10000, "ad_group.cpm_bid_micros": 10000, "ad_group.cpv_bid_micros": 0, "ad_group.display_custom_bid_dimension": "UNSPECIFIED", "ad_group.effective_target_cpa_micros": 0, "ad_group.effective_target_cpa_source": "UNSPECIFIED", "ad_group.effective_target_roas": 0.0, "ad_group.effective_target_roas_source": "UNSPECIFIED", "ad_group.excluded_parent_asset_field_types": [], "ad_group.optimized_targeting_enabled": false, "ad_group.final_url_suffix": "", "ad_group.id": 137020701042, "ad_group.labels": ["customers/4651612872/labels/21906377810"], "ad_group.name": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0439\u00a02", "ad_group.percent_cpc_bid_micros": 0, "ad_group.resource_name": "customers/4651612872/adGroups/137020701042", "ad_group.status": "ENABLED", "ad_group.target_cpa_micros": 0, "ad_group.target_cpm_micros": 10000, "ad_group.target_roas": 0.0, "ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AUDIENCE\nbid_only: true\n", "targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "ad_group.tracking_url_template": "", "ad_group.type": "SEARCH_STANDARD", "ad_group.url_custom_parameters": [], "segments.date": "2022-05-20"}, "emitted_at": 1704407767216} +{"stream": "ad_group", "data": {"campaign.id": 16820250687, "ad_group.ad_rotation_mode": "UNSPECIFIED", "ad_group.base_ad_group": "customers/4651612872/adGroups/137020701042", "ad_group.campaign": "customers/4651612872/campaigns/16820250687", "metrics.cost_micros": 790000, "ad_group.cpc_bid_micros": 10000, "ad_group.cpm_bid_micros": 10000, "ad_group.cpv_bid_micros": 0, "ad_group.display_custom_bid_dimension": "UNSPECIFIED", "ad_group.effective_target_cpa_micros": 0, "ad_group.effective_target_cpa_source": "UNSPECIFIED", "ad_group.effective_target_roas": 0.0, "ad_group.effective_target_roas_source": "UNSPECIFIED", "ad_group.excluded_parent_asset_field_types": [], "ad_group.optimized_targeting_enabled": false, "ad_group.final_url_suffix": "", "ad_group.id": 137020701042, "ad_group.labels": ["customers/4651612872/labels/21906377810"], "ad_group.name": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0439\u00a02", "ad_group.percent_cpc_bid_micros": 0, "ad_group.resource_name": "customers/4651612872/adGroups/137020701042", "ad_group.status": "ENABLED", "ad_group.target_cpa_micros": 0, "ad_group.target_cpm_micros": 10000, "ad_group.target_roas": 0.0, "ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AUDIENCE\nbid_only: true\n", "targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "ad_group.tracking_url_template": "", "ad_group.type": "SEARCH_STANDARD", "ad_group.url_custom_parameters": [], "segments.date": "2022-05-18"}, "emitted_at": 1704715893659} +{"stream": "ad_group", "data": {"campaign.id": 16820250687, "ad_group.ad_rotation_mode": "UNSPECIFIED", "ad_group.base_ad_group": "customers/4651612872/adGroups/137020701042", "ad_group.campaign": "customers/4651612872/campaigns/16820250687", "metrics.cost_micros": 860000, "ad_group.cpc_bid_micros": 10000, "ad_group.cpm_bid_micros": 10000, "ad_group.cpv_bid_micros": 0, "ad_group.display_custom_bid_dimension": "UNSPECIFIED", "ad_group.effective_target_cpa_micros": 0, "ad_group.effective_target_cpa_source": "UNSPECIFIED", "ad_group.effective_target_roas": 0.0, "ad_group.effective_target_roas_source": "UNSPECIFIED", "ad_group.excluded_parent_asset_field_types": [], "ad_group.optimized_targeting_enabled": false, "ad_group.final_url_suffix": "", "ad_group.id": 137020701042, "ad_group.labels": ["customers/4651612872/labels/21906377810"], "ad_group.name": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0439\u00a02", "ad_group.percent_cpc_bid_micros": 0, "ad_group.resource_name": "customers/4651612872/adGroups/137020701042", "ad_group.status": "ENABLED", "ad_group.target_cpa_micros": 0, "ad_group.target_cpm_micros": 10000, "ad_group.target_roas": 0.0, "ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AUDIENCE\nbid_only: true\n", "targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "ad_group.tracking_url_template": "", "ad_group.type": "SEARCH_STANDARD", "ad_group.url_custom_parameters": [], "segments.date": "2022-05-19"}, "emitted_at": 1704715893662} +{"stream": "ad_group", "data": {"campaign.id": 16820250687, "ad_group.ad_rotation_mode": "UNSPECIFIED", "ad_group.base_ad_group": "customers/4651612872/adGroups/137020701042", "ad_group.campaign": "customers/4651612872/campaigns/16820250687", "metrics.cost_micros": 430000, "ad_group.cpc_bid_micros": 10000, "ad_group.cpm_bid_micros": 10000, "ad_group.cpv_bid_micros": 0, "ad_group.display_custom_bid_dimension": "UNSPECIFIED", "ad_group.effective_target_cpa_micros": 0, "ad_group.effective_target_cpa_source": "UNSPECIFIED", "ad_group.effective_target_roas": 0.0, "ad_group.effective_target_roas_source": "UNSPECIFIED", "ad_group.excluded_parent_asset_field_types": [], "ad_group.optimized_targeting_enabled": false, "ad_group.final_url_suffix": "", "ad_group.id": 137020701042, "ad_group.labels": ["customers/4651612872/labels/21906377810"], "ad_group.name": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0439\u00a02", "ad_group.percent_cpc_bid_micros": 0, "ad_group.resource_name": "customers/4651612872/adGroups/137020701042", "ad_group.status": "ENABLED", "ad_group.target_cpa_micros": 0, "ad_group.target_cpm_micros": 10000, "ad_group.target_roas": 0.0, "ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AUDIENCE\nbid_only: true\n", "targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "ad_group.tracking_url_template": "", "ad_group.type": "SEARCH_STANDARD", "ad_group.url_custom_parameters": [], "segments.date": "2022-05-20"}, "emitted_at": 1704715893662} {"stream": "customer", "data": {"customer.auto_tagging_enabled": true, "customer.call_reporting_setting.call_conversion_action": "customers/4651612872/conversionActions/179", "customer.call_reporting_setting.call_conversion_reporting_enabled": true, "customer.call_reporting_setting.call_reporting_enabled": true, "customer.conversion_tracking_setting.conversion_tracking_id": 657981234, "customer.conversion_tracking_setting.cross_account_conversion_tracking_id": 0, "customer.currency_code": "USD", "customer.descriptive_name": "Airbyte", "customer.final_url_suffix": "", "customer.has_partners_badge": false, "customer.id": 4651612872, "customer.manager": false, "customer.optimization_score": 0.7609283000000001, "customer.optimization_score_weight": 3182.4700059999996, "customer.pay_per_conversion_eligibility_failure_reasons": [], "customer.remarketing_setting.google_global_site_tag": "\n\n\n", "customer.resource_name": "customers/4651612872", "customer.test_account": false, "customer.time_zone": "America/Los_Angeles", "customer.tracking_url_template": "{lpurl}?utm_term={keyword}&utm_campaign={_utmcampaign}&utm_source=adwords&utm_medium=ppc&hsa_acc=4651612872&hsa_cam={campaignid}&hsa_grp={adgroupid}&hsa_ad={creative}&hsa_src={network}&hsa_tgt={targetid}&hsa_kw={keyword}&hsa_mt={matchtype}&hsa_net=adwords&hsa_ver=3", "segments.date": "2022-05-18"}, "emitted_at": 1704407768194} {"stream": "customer", "data": {"customer.auto_tagging_enabled": true, "customer.call_reporting_setting.call_conversion_action": "customers/4651612872/conversionActions/179", "customer.call_reporting_setting.call_conversion_reporting_enabled": true, "customer.call_reporting_setting.call_reporting_enabled": true, "customer.conversion_tracking_setting.conversion_tracking_id": 657981234, "customer.conversion_tracking_setting.cross_account_conversion_tracking_id": 0, "customer.currency_code": "USD", "customer.descriptive_name": "Airbyte", "customer.final_url_suffix": "", "customer.has_partners_badge": false, "customer.id": 4651612872, "customer.manager": false, "customer.optimization_score": 0.7609283000000001, "customer.optimization_score_weight": 3182.4700059999996, "customer.pay_per_conversion_eligibility_failure_reasons": [], "customer.remarketing_setting.google_global_site_tag": "\n\n\n", "customer.resource_name": "customers/4651612872", "customer.test_account": false, "customer.time_zone": "America/Los_Angeles", "customer.tracking_url_template": "{lpurl}?utm_term={keyword}&utm_campaign={_utmcampaign}&utm_source=adwords&utm_medium=ppc&hsa_acc=4651612872&hsa_cam={campaignid}&hsa_grp={adgroupid}&hsa_ad={creative}&hsa_src={network}&hsa_tgt={targetid}&hsa_kw={keyword}&hsa_mt={matchtype}&hsa_net=adwords&hsa_ver=3", "segments.date": "2022-05-19"}, "emitted_at": 1704407768194} {"stream": "customer", "data": {"customer.auto_tagging_enabled": true, "customer.call_reporting_setting.call_conversion_action": "customers/4651612872/conversionActions/179", "customer.call_reporting_setting.call_conversion_reporting_enabled": true, "customer.call_reporting_setting.call_reporting_enabled": true, "customer.conversion_tracking_setting.conversion_tracking_id": 657981234, "customer.conversion_tracking_setting.cross_account_conversion_tracking_id": 0, "customer.currency_code": "USD", "customer.descriptive_name": "Airbyte", "customer.final_url_suffix": "", "customer.has_partners_badge": false, "customer.id": 4651612872, "customer.manager": false, "customer.optimization_score": 0.7609283000000001, "customer.optimization_score_weight": 3182.4700059999996, "customer.pay_per_conversion_eligibility_failure_reasons": [], "customer.remarketing_setting.google_global_site_tag": "\n\n\n", "customer.resource_name": "customers/4651612872", "customer.test_account": false, "customer.time_zone": "America/Los_Angeles", "customer.tracking_url_template": "{lpurl}?utm_term={keyword}&utm_campaign={_utmcampaign}&utm_source=adwords&utm_medium=ppc&hsa_acc=4651612872&hsa_cam={campaignid}&hsa_grp={adgroupid}&hsa_ad={creative}&hsa_src={network}&hsa_tgt={targetid}&hsa_kw={keyword}&hsa_mt={matchtype}&hsa_net=adwords&hsa_ver=3", "segments.date": "2022-05-20"}, "emitted_at": 1704407768195} diff --git a/airbyte-integrations/connectors/source-google-ads/integration_tests/expected_records_click.jsonl b/airbyte-integrations/connectors/source-google-ads/integration_tests/expected_records_click.jsonl index 2e8a74f1a7e1e..07fd5caa07a11 100644 --- a/airbyte-integrations/connectors/source-google-ads/integration_tests/expected_records_click.jsonl +++ b/airbyte-integrations/connectors/source-google-ads/integration_tests/expected_records_click.jsonl @@ -1,7 +1,7 @@ -{"stream": "ad_group_ad_legacy", "data": {"ad_group_ad.ad.legacy_responsive_display_ad.accent_color": "", "ad_group.id": 155311392438, "customer.currency_code": "USD", "customer.descriptive_name": "Airbyte", "customer.time_zone": "America/Los_Angeles", "metrics.active_view_cpm": 0.0, "metrics.active_view_ctr": 0.0, "metrics.active_view_impressions": 0, "metrics.active_view_measurability": 0.0, "metrics.active_view_measurable_cost_micros": 0, "metrics.active_view_measurable_impressions": 0, "metrics.active_view_viewability": 0.0, "ad_group_ad.ad_group": "customers/4651612872/adGroups/155311392438", "ad_group.name": "Airbyte", "ad_group.status": "ENABLED", "segments.ad_network_type": "SEARCH", "ad_group_ad.ad_strength": "EXCELLENT", "ad_group_ad.ad.type": "RESPONSIVE_SEARCH_AD", "metrics.all_conversions_from_interactions_rate": 4.440321324324325, "metrics.all_conversions_value": 1240.049854, "metrics.all_conversions": 164.291889, "ad_group_ad.ad.legacy_responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.added_by_google_ads": false, "metrics.average_cost": 8890000.0, "metrics.average_cpc": 8890000.0, "metrics.average_cpe": 0.0, "metrics.average_cpm": 1279883268.4824903, "metrics.average_cpv": 0.0, "metrics.average_page_views": 0.0, "metrics.average_time_on_site": 0.0, "ad_group.base_ad_group": "customers/4651612872/adGroups/155311392438", "campaign.base_campaign": "customers/4651612872/campaigns/20643300404", "metrics.bounce_rate": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.business_name": "", "ad_group_ad.ad.legacy_responsive_display_ad.call_to_action_text": "", "campaign.id": 20643300404, "campaign.name": "mm_search_brand", "campaign.status": "PAUSED", "metrics.clicks": 37, "ad_group_ad.policy_summary.approval_status": "APPROVED", "metrics.conversions_from_interactions_rate": 0.34582527027027027, "metrics.conversions_value": 1079.5535, "metrics.conversions": 12.795535, "metrics.cost_micros": 328930000, "metrics.cost_per_all_conversions": 2002107.3590553214, "metrics.cost_per_conversion": 25706623.443255793, "metrics.cost_per_current_model_attributed_conversion": 25706623.443255793, "ad_group_ad.ad.final_mobile_urls": [], "ad_group_ad.ad.final_urls": ["https://airbyte.com/"], "ad_group_ad.ad.tracking_url_template": "", "ad_group_ad.ad.url_custom_parameters": [], "metrics.cross_device_conversions": 2.0, "metrics.ctr": 0.14396887159533073, "metrics.current_model_attributed_conversions_value": 1079.5535, "metrics.current_model_attributed_conversions": 12.795535, "segments.date": "2024-01-01", "segments.day_of_week": "MONDAY", "ad_group_ad.ad.expanded_text_ad.description": "", "ad_group_ad.ad.text_ad.description1": "", "ad_group_ad.ad.text_ad.description2": "", "ad_group_ad.ad.device_preference": "UNSPECIFIED", "ad_group_ad.ad.display_url": "", "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "ad_group_ad.ad.legacy_responsive_display_ad.logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.marketing_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_marketing_image": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description": "", "ad_group_ad.ad.expanded_text_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.headline_part3": "", "customer.id": 4651612872, "ad_group_ad.ad.legacy_responsive_display_ad.format_setting": "UNSPECIFIED", "metrics.gmail_forwards": 0, "metrics.gmail_saves": 0, "metrics.gmail_secondary_clicks": 0, "ad_group_ad.ad.text_ad.headline": "", "ad_group_ad.ad.expanded_text_ad.headline_part1": "", "ad_group_ad.ad.expanded_text_ad.headline_part2": "", "ad_group_ad.ad.id": 676665180945, "ad_group_ad.ad.image_ad.image_url": "", "ad_group_ad.ad.image_ad.pixel_height": 0, "ad_group_ad.ad.image_ad.pixel_width": 0, "ad_group_ad.ad.image_ad.mime_type": "UNSPECIFIED", "ad_group_ad.ad.image_ad.name": "", "metrics.impressions": 257, "metrics.interaction_rate": 0.14396887159533073, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interactions": 37, "ad_group_ad.ad.legacy_responsive_display_ad.long_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.main_color": "", "segments.month": "2024-01-01", "ad_group_ad.ad.responsive_display_ad.accent_color": "", "ad_group_ad.ad.responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.responsive_display_ad.business_name": "", "ad_group_ad.ad.responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.responsive_display_ad.descriptions": [], "ad_group_ad.ad.responsive_display_ad.price_prefix": "", "ad_group_ad.ad.responsive_display_ad.promo_text": "", "ad_group_ad.ad.responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.responsive_display_ad.headlines": [], "ad_group_ad.ad.responsive_display_ad.logo_images": [], "ad_group_ad.ad.responsive_display_ad.square_logo_images": [], "ad_group_ad.ad.responsive_display_ad.long_headline": "", "ad_group_ad.ad.responsive_display_ad.main_color": "", "ad_group_ad.ad.responsive_display_ad.marketing_images": [], "ad_group_ad.ad.responsive_display_ad.square_marketing_images": [], "ad_group_ad.ad.responsive_display_ad.youtube_videos": [], "ad_group_ad.ad.expanded_text_ad.path1": "", "ad_group_ad.ad.expanded_text_ad.path2": "", "metrics.percent_new_visitors": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.price_prefix": "", "ad_group_ad.ad.legacy_responsive_display_ad.promo_text": "", "segments.quarter": "2024-01-01", "ad_group_ad.ad.responsive_search_ad.descriptions": ["text: \"The most comprehensive catalog of connectors, trusted by 40,000K engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"A high-performing and scalable data integration platform with advanced features.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte for free! Connect Any Data, Any User, & Any Application Effortlessly.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build custom connectors in 10 min with our no-code connector builder.\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.headlines": ["text: \"Airbyte\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"The only ETL tool you need\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build ELT Pipelines In Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No code, ELT Tool\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Replicate Data in Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open Source Integration\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte Cloud Free\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Browse Our Catalog\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Trusted by over 40K Engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"14 Day Free Trial\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"300+ off-the-shelf connectors\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No-Code Connector Builder\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Get started in minutes\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Extract, Load & Transform\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Streamlined Data Pipeline\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.path1": "", "ad_group_ad.ad.responsive_search_ad.path2": "", "ad_group_ad.ad.legacy_responsive_display_ad.short_headline": "", "ad_group_ad.status": "ENABLED", "ad_group_ad.ad.system_managed_resource_source": "UNSPECIFIED", "metrics.top_impression_percentage": 0.9688715953307393, "ad_group_ad.ad.app_ad.descriptions": [], "ad_group_ad.ad.app_ad.headlines": [], "ad_group_ad.ad.app_ad.html5_media_bundles": [], "ad_group_ad.ad.app_ad.images": [], "ad_group_ad.ad.app_ad.mandatory_ad_text": "", "ad_group_ad.ad.app_ad.youtube_videos": [], "metrics.value_per_all_conversions": 7.547845858659524, "metrics.value_per_conversion": 84.36954765861685, "metrics.value_per_current_model_attributed_conversion": 84.36954765861685, "metrics.video_quartile_p100_rate": 0.0, "metrics.video_quartile_p25_rate": 0.0, "metrics.video_quartile_p50_rate": 0.0, "metrics.video_quartile_p75_rate": 0.0, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0, "segments.week": "2024-01-01", "segments.year": 2024}, "emitted_at": 1704411456522} -{"stream": "ad_group_ad_legacy", "data": {"ad_group_ad.ad.legacy_responsive_display_ad.accent_color": "", "ad_group.id": 152406853857, "customer.currency_code": "USD", "customer.descriptive_name": "Airbyte", "customer.time_zone": "America/Los_Angeles", "metrics.active_view_cpm": 0.0, "metrics.active_view_ctr": 0.0, "metrics.active_view_impressions": 0, "metrics.active_view_measurability": 0.0, "metrics.active_view_measurable_cost_micros": 0, "metrics.active_view_measurable_impressions": 0, "metrics.active_view_viewability": 0.0, "ad_group_ad.ad_group": "customers/4651612872/adGroups/152406853857", "ad_group.name": "Fivetran", "ad_group.status": "ENABLED", "segments.ad_network_type": "SEARCH", "ad_group_ad.ad_strength": "EXCELLENT", "ad_group_ad.ad.type": "RESPONSIVE_SEARCH_AD", "metrics.all_conversions_from_interactions_rate": 0.0, "metrics.all_conversions_value": 0.0, "metrics.all_conversions": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.added_by_google_ads": false, "metrics.average_cost": 0.0, "metrics.average_cpc": 0.0, "metrics.average_cpe": 0.0, "metrics.average_cpm": 0.0, "metrics.average_cpv": 0.0, "metrics.average_page_views": 0.0, "metrics.average_time_on_site": 0.0, "ad_group.base_ad_group": "customers/4651612872/adGroups/152406853857", "campaign.base_campaign": "customers/4651612872/campaigns/20655886237", "metrics.bounce_rate": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.business_name": "", "ad_group_ad.ad.legacy_responsive_display_ad.call_to_action_text": "", "campaign.id": 20655886237, "campaign.name": "mm_search_competitors", "campaign.status": "ENABLED", "metrics.clicks": 0, "ad_group_ad.policy_summary.approval_status": "APPROVED", "metrics.conversions_from_interactions_rate": 0.0, "metrics.conversions_value": 0.0, "metrics.conversions": 0.0, "metrics.cost_micros": 0, "metrics.cost_per_all_conversions": 0.0, "metrics.cost_per_conversion": 0.0, "metrics.cost_per_current_model_attributed_conversion": 0.0, "ad_group_ad.ad.final_mobile_urls": [], "ad_group_ad.ad.final_urls": ["https://airbyte.com/etl-tools/fivetran-alternative-airbyte"], "ad_group_ad.ad.tracking_url_template": "", "ad_group_ad.ad.url_custom_parameters": [], "metrics.cross_device_conversions": 0.0, "metrics.ctr": 0.0, "metrics.current_model_attributed_conversions_value": 0.0, "metrics.current_model_attributed_conversions": 0.0, "segments.date": "2024-01-02", "segments.day_of_week": "TUESDAY", "ad_group_ad.ad.expanded_text_ad.description": "", "ad_group_ad.ad.text_ad.description1": "", "ad_group_ad.ad.text_ad.description2": "", "ad_group_ad.ad.device_preference": "UNSPECIFIED", "ad_group_ad.ad.display_url": "", "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "ad_group_ad.ad.legacy_responsive_display_ad.logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.marketing_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_marketing_image": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description": "", "ad_group_ad.ad.expanded_text_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.headline_part3": "", "customer.id": 4651612872, "ad_group_ad.ad.legacy_responsive_display_ad.format_setting": "UNSPECIFIED", "metrics.gmail_forwards": 0, "metrics.gmail_saves": 0, "metrics.gmail_secondary_clicks": 0, "ad_group_ad.ad.text_ad.headline": "", "ad_group_ad.ad.expanded_text_ad.headline_part1": "", "ad_group_ad.ad.expanded_text_ad.headline_part2": "", "ad_group_ad.ad.id": 677024318384, "ad_group_ad.ad.image_ad.image_url": "", "ad_group_ad.ad.image_ad.pixel_height": 0, "ad_group_ad.ad.image_ad.pixel_width": 0, "ad_group_ad.ad.image_ad.mime_type": "UNSPECIFIED", "ad_group_ad.ad.image_ad.name": "", "metrics.impressions": 38, "metrics.interaction_rate": 0.0, "metrics.interaction_event_types": [], "metrics.interactions": 0, "ad_group_ad.ad.legacy_responsive_display_ad.long_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.main_color": "", "segments.month": "2024-01-01", "ad_group_ad.ad.responsive_display_ad.accent_color": "", "ad_group_ad.ad.responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.responsive_display_ad.business_name": "", "ad_group_ad.ad.responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.responsive_display_ad.descriptions": [], "ad_group_ad.ad.responsive_display_ad.price_prefix": "", "ad_group_ad.ad.responsive_display_ad.promo_text": "", "ad_group_ad.ad.responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.responsive_display_ad.headlines": [], "ad_group_ad.ad.responsive_display_ad.logo_images": [], "ad_group_ad.ad.responsive_display_ad.square_logo_images": [], "ad_group_ad.ad.responsive_display_ad.long_headline": "", "ad_group_ad.ad.responsive_display_ad.main_color": "", "ad_group_ad.ad.responsive_display_ad.marketing_images": [], "ad_group_ad.ad.responsive_display_ad.square_marketing_images": [], "ad_group_ad.ad.responsive_display_ad.youtube_videos": [], "ad_group_ad.ad.expanded_text_ad.path1": "", "ad_group_ad.ad.expanded_text_ad.path2": "", "metrics.percent_new_visitors": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.price_prefix": "", "ad_group_ad.ad.legacy_responsive_display_ad.promo_text": "", "segments.quarter": "2024-01-01", "ad_group_ad.ad.responsive_search_ad.descriptions": ["text: \"Airbyte vs. Fivetran - Comparison | Pros & Cons (2023)\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Airbyte is an open-source data integration / ETL alternative to Fivetran\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Self-Service for Airbyte Cloud. Open-Source Edition Deployable in Minutes. Volume-Based\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Compare data sources and destinations, features, pricing and more.\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.headlines": ["text: \"Airbyte vs. Fivetran\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Fivetran Alternative\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Need a Fivetran alternative?\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Power Up Your Data Integration\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"ELT tool\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open Source Data Integration\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Airbyte: Top Open-Source ELT\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Choose the Leading ELT tool\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"ELT Alternative?\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Looking for a Better ELT Tool?\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Automated ETL Pipeline\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Airbyte Connectors\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open-Source ELT Tool\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Hundreds of connectors\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Streamlined Data Pipeline\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.path1": "", "ad_group_ad.ad.responsive_search_ad.path2": "", "ad_group_ad.ad.legacy_responsive_display_ad.short_headline": "", "ad_group_ad.status": "ENABLED", "ad_group_ad.ad.system_managed_resource_source": "UNSPECIFIED", "metrics.top_impression_percentage": 0.9736842105263158, "ad_group_ad.ad.app_ad.descriptions": [], "ad_group_ad.ad.app_ad.headlines": [], "ad_group_ad.ad.app_ad.html5_media_bundles": [], "ad_group_ad.ad.app_ad.images": [], "ad_group_ad.ad.app_ad.mandatory_ad_text": "", "ad_group_ad.ad.app_ad.youtube_videos": [], "metrics.value_per_all_conversions": 0.0, "metrics.value_per_conversion": 0.0, "metrics.value_per_current_model_attributed_conversion": 0.0, "metrics.video_quartile_p100_rate": 0.0, "metrics.video_quartile_p25_rate": 0.0, "metrics.video_quartile_p50_rate": 0.0, "metrics.video_quartile_p75_rate": 0.0, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0, "segments.week": "2024-01-01", "segments.year": 2024}, "emitted_at": 1704411456523} -{"stream": "campaign_budget", "data": {"customer.id": 4651612872, "campaign.id": 20643300404, "campaign_budget.aligned_bidding_strategy_id": 0, "campaign_budget.amount_micros": 330000000, "campaign_budget.delivery_method": "STANDARD", "campaign_budget.explicitly_shared": false, "campaign_budget.has_recommended_budget": false, "campaign_budget.id": 13022493317, "campaign_budget.name": "mm_search_brand", "campaign_budget.period": "DAILY", "campaign_budget.recommended_budget_amount_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_clicks": 0, "campaign_budget.recommended_budget_estimated_change_weekly_cost_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_interactions": 0, "campaign_budget.recommended_budget_estimated_change_weekly_views": 0, "campaign_budget.reference_count": 1, "campaign_budget.resource_name": "customers/4651612872/campaignBudgets/13022493317", "campaign_budget.status": "ENABLED", "campaign_budget.total_amount_micros": 0, "campaign_budget.type": "STANDARD", "segments.date": "2024-01-01", "segments.budget_campaign_association_status.campaign": "customers/4651612872/campaigns/20643300404", "segments.budget_campaign_association_status.status": "ENABLED", "metrics.all_conversions": 164.291889, "metrics.all_conversions_from_interactions_rate": 4.440321324324325, "metrics.all_conversions_value": 1240.049854, "metrics.average_cost": 8890000.0, "metrics.average_cpc": 8890000.0, "metrics.average_cpe": 0.0, "metrics.average_cpm": 1279883268.4824903, "metrics.average_cpv": 0.0, "metrics.clicks": 37, "metrics.conversions": 12.795535, "metrics.conversions_from_interactions_rate": 0.34582527027027027, "metrics.conversions_value": 1079.5535, "metrics.cost_micros": 328930000, "metrics.cost_per_all_conversions": 2002107.3590553214, "metrics.cost_per_conversion": 25706623.443255793, "metrics.cross_device_conversions": 2.0, "metrics.ctr": 0.14396887159533073, "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "metrics.impressions": 257, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interaction_rate": 0.14396887159533073, "metrics.interactions": 37, "metrics.value_per_all_conversions": 7.547845858659524, "metrics.value_per_conversion": 84.36954765861685, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0}, "emitted_at": 1704408105190} -{"stream": "campaign_budget", "data": {"customer.id": 4651612872, "campaign.id": 20637264648, "campaign_budget.aligned_bidding_strategy_id": 0, "campaign_budget.amount_micros": 250000000, "campaign_budget.delivery_method": "STANDARD", "campaign_budget.explicitly_shared": false, "campaign_budget.has_recommended_budget": false, "campaign_budget.id": 13027970877, "campaign_budget.name": "mm_search_nonbrand", "campaign_budget.period": "DAILY", "campaign_budget.recommended_budget_amount_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_clicks": 0, "campaign_budget.recommended_budget_estimated_change_weekly_cost_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_interactions": 0, "campaign_budget.recommended_budget_estimated_change_weekly_views": 0, "campaign_budget.reference_count": 1, "campaign_budget.resource_name": "customers/4651612872/campaignBudgets/13027970877", "campaign_budget.status": "ENABLED", "campaign_budget.total_amount_micros": 0, "campaign_budget.type": "STANDARD", "segments.date": "2024-01-02", "segments.budget_campaign_association_status.campaign": "customers/4651612872/campaigns/20637264648", "segments.budget_campaign_association_status.status": "ENABLED", "metrics.all_conversions": 11.0, "metrics.all_conversions_from_interactions_rate": 3.6666666666666665, "metrics.all_conversions_value": 11.0, "metrics.average_cost": 3703333.3333333335, "metrics.average_cpc": 3703333.3333333335, "metrics.average_cpe": 0.0, "metrics.average_cpm": 57564766.83937824, "metrics.average_cpv": 0.0, "metrics.clicks": 3, "metrics.conversions": 0.0, "metrics.conversions_from_interactions_rate": 0.0, "metrics.conversions_value": 0.0, "metrics.cost_micros": 11110000, "metrics.cost_per_all_conversions": 1010000.0, "metrics.cost_per_conversion": 0.0, "metrics.cross_device_conversions": 0.0, "metrics.ctr": 0.015544041450777202, "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "metrics.impressions": 193, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interaction_rate": 0.015544041450777202, "metrics.interactions": 3, "metrics.value_per_all_conversions": 1.0, "metrics.value_per_conversion": 0.0, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0}, "emitted_at": 1704408105192} +{"stream": "ad_group_ad_legacy", "data": {"ad_group_ad.ad.legacy_responsive_display_ad.accent_color": "", "ad_group.id": 155311392438, "customer.currency_code": "USD", "customer.descriptive_name": "Airbyte", "customer.time_zone": "America/Los_Angeles", "metrics.active_view_cpm": 0.0, "metrics.active_view_ctr": 0.0, "metrics.active_view_impressions": 0, "metrics.active_view_measurability": 0.0, "metrics.active_view_measurable_cost_micros": 0, "metrics.active_view_measurable_impressions": 0, "metrics.active_view_viewability": 0.0, "ad_group_ad.ad_group": "customers/4651612872/adGroups/155311392438", "ad_group.name": "Airbyte", "ad_group.status": "ENABLED", "segments.ad_network_type": "SEARCH", "ad_group_ad.ad_strength": "EXCELLENT", "ad_group_ad.ad.type": "RESPONSIVE_SEARCH_AD", "metrics.all_conversions_from_interactions_rate": 4.833920699999999, "metrics.all_conversions_value": 800.783622, "metrics.all_conversions": 145.017621, "ad_group_ad.ad.legacy_responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.added_by_google_ads": false, "metrics.average_cost": 5602666.666666667, "metrics.average_cpc": 5602666.666666667, "metrics.average_cpe": 0.0, "metrics.average_cpm": 1031165644.1717792, "metrics.average_cpv": 0.0, "metrics.average_page_views": 0.0, "metrics.average_time_on_site": 0.0, "ad_group.base_ad_group": "customers/4651612872/adGroups/155311392438", "campaign.base_campaign": "customers/4651612872/campaigns/20643300404", "metrics.bounce_rate": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.business_name": "", "ad_group_ad.ad.legacy_responsive_display_ad.call_to_action_text": "", "campaign.id": 20643300404, "campaign.name": "mm_search_brand", "campaign.status": "PAUSED", "metrics.clicks": 30, "ad_group_ad.policy_summary.approval_status": "APPROVED", "metrics.conversions_from_interactions_rate": 0.22079663333333333, "metrics.conversions_value": 662.3899, "metrics.conversions": 6.623899, "metrics.cost_micros": 168080000, "metrics.cost_per_all_conversions": 1159031.5634815167, "metrics.cost_per_conversion": 25374783.039415307, "metrics.cost_per_current_model_attributed_conversion": 25374783.039415307, "ad_group_ad.ad.final_mobile_urls": [], "ad_group_ad.ad.final_urls": ["https://airbyte.com/"], "ad_group_ad.ad.tracking_url_template": "", "ad_group_ad.ad.url_custom_parameters": [], "metrics.cross_device_conversions": 0.0, "metrics.ctr": 0.18404907975460122, "metrics.current_model_attributed_conversions_value": 662.3899, "metrics.current_model_attributed_conversions": 6.623899, "segments.date": "2023-12-31", "segments.day_of_week": "SUNDAY", "ad_group_ad.ad.expanded_text_ad.description": "", "ad_group_ad.ad.text_ad.description1": "", "ad_group_ad.ad.text_ad.description2": "", "ad_group_ad.ad.device_preference": "UNSPECIFIED", "ad_group_ad.ad.display_url": "", "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "ad_group_ad.ad.legacy_responsive_display_ad.logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.marketing_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_marketing_image": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description": "", "ad_group_ad.ad.expanded_text_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.headline_part3": "", "customer.id": 4651612872, "ad_group_ad.ad.legacy_responsive_display_ad.format_setting": "UNSPECIFIED", "metrics.gmail_forwards": 0, "metrics.gmail_saves": 0, "metrics.gmail_secondary_clicks": 0, "ad_group_ad.ad.text_ad.headline": "", "ad_group_ad.ad.expanded_text_ad.headline_part1": "", "ad_group_ad.ad.expanded_text_ad.headline_part2": "", "ad_group_ad.ad.id": 676665180945, "ad_group_ad.ad.image_ad.image_url": "", "ad_group_ad.ad.image_ad.pixel_height": 0, "ad_group_ad.ad.image_ad.pixel_width": 0, "ad_group_ad.ad.image_ad.mime_type": "UNSPECIFIED", "ad_group_ad.ad.image_ad.name": "", "metrics.impressions": 163, "metrics.interaction_rate": 0.18404907975460122, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interactions": 30, "ad_group_ad.ad.legacy_responsive_display_ad.long_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.main_color": "", "segments.month": "2023-12-01", "ad_group_ad.ad.responsive_display_ad.accent_color": "", "ad_group_ad.ad.responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.responsive_display_ad.business_name": "", "ad_group_ad.ad.responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.responsive_display_ad.descriptions": [], "ad_group_ad.ad.responsive_display_ad.price_prefix": "", "ad_group_ad.ad.responsive_display_ad.promo_text": "", "ad_group_ad.ad.responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.responsive_display_ad.headlines": [], "ad_group_ad.ad.responsive_display_ad.logo_images": [], "ad_group_ad.ad.responsive_display_ad.square_logo_images": [], "ad_group_ad.ad.responsive_display_ad.long_headline": "", "ad_group_ad.ad.responsive_display_ad.main_color": "", "ad_group_ad.ad.responsive_display_ad.marketing_images": [], "ad_group_ad.ad.responsive_display_ad.square_marketing_images": [], "ad_group_ad.ad.responsive_display_ad.youtube_videos": [], "ad_group_ad.ad.expanded_text_ad.path1": "", "ad_group_ad.ad.expanded_text_ad.path2": "", "metrics.percent_new_visitors": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.price_prefix": "", "ad_group_ad.ad.legacy_responsive_display_ad.promo_text": "", "segments.quarter": "2023-10-01", "ad_group_ad.ad.responsive_search_ad.descriptions": ["text: \"The most comprehensive catalog of connectors, trusted by 40,000K engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"A high-performing and scalable data integration platform with advanced features.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte for free! Connect Any Data, Any User, & Any Application Effortlessly.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build custom connectors in 10 min with our no-code connector builder.\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.headlines": ["text: \"Airbyte\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"The only ETL tool you need\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build ELT Pipelines In Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No code, ELT Tool\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Replicate Data in Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open Source Integration\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte Cloud Free\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Browse Our Catalog\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Trusted by over 40K Engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"14 Day Free Trial\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"300+ off-the-shelf connectors\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No-Code Connector Builder\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Get started in minutes\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Extract, Load & Transform\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Streamlined Data Pipeline\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.path1": "", "ad_group_ad.ad.responsive_search_ad.path2": "", "ad_group_ad.ad.legacy_responsive_display_ad.short_headline": "", "ad_group_ad.status": "ENABLED", "ad_group_ad.ad.system_managed_resource_source": "UNSPECIFIED", "metrics.top_impression_percentage": 0.8834355828220859, "ad_group_ad.ad.app_ad.descriptions": [], "ad_group_ad.ad.app_ad.headlines": [], "ad_group_ad.ad.app_ad.html5_media_bundles": [], "ad_group_ad.ad.app_ad.images": [], "ad_group_ad.ad.app_ad.mandatory_ad_text": "", "ad_group_ad.ad.app_ad.youtube_videos": [], "metrics.value_per_all_conversions": 5.52197461576066, "metrics.value_per_conversion": 100.0, "metrics.value_per_current_model_attributed_conversion": 100.0, "metrics.video_quartile_p100_rate": 0.0, "metrics.video_quartile_p25_rate": 0.0, "metrics.video_quartile_p50_rate": 0.0, "metrics.video_quartile_p75_rate": 0.0, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0, "segments.week": "2023-12-25", "segments.year": 2023}, "emitted_at": 1704716796168} +{"stream": "ad_group_ad_legacy", "data": {"ad_group_ad.ad.legacy_responsive_display_ad.accent_color": "", "ad_group.id": 155311392438, "customer.currency_code": "USD", "customer.descriptive_name": "Airbyte", "customer.time_zone": "America/Los_Angeles", "metrics.active_view_cpm": 0.0, "metrics.active_view_ctr": 0.0, "metrics.active_view_impressions": 0, "metrics.active_view_measurability": 0.0, "metrics.active_view_measurable_cost_micros": 0, "metrics.active_view_measurable_impressions": 0, "metrics.active_view_viewability": 0.0, "ad_group_ad.ad_group": "customers/4651612872/adGroups/155311392438", "ad_group.name": "Airbyte", "ad_group.status": "ENABLED", "segments.ad_network_type": "SEARCH", "ad_group_ad.ad_strength": "EXCELLENT", "ad_group_ad.ad.type": "RESPONSIVE_SEARCH_AD", "metrics.all_conversions_from_interactions_rate": 4.832213216216216, "metrics.all_conversions_value": 1254.549854, "metrics.all_conversions": 178.791889, "ad_group_ad.ad.legacy_responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.added_by_google_ads": false, "metrics.average_cost": 8890000.0, "metrics.average_cpc": 8890000.0, "metrics.average_cpe": 0.0, "metrics.average_cpm": 1279883268.4824903, "metrics.average_cpv": 0.0, "metrics.average_page_views": 0.0, "metrics.average_time_on_site": 0.0, "ad_group.base_ad_group": "customers/4651612872/adGroups/155311392438", "campaign.base_campaign": "customers/4651612872/campaigns/20643300404", "metrics.bounce_rate": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.business_name": "", "ad_group_ad.ad.legacy_responsive_display_ad.call_to_action_text": "", "campaign.id": 20643300404, "campaign.name": "mm_search_brand", "campaign.status": "PAUSED", "metrics.clicks": 37, "ad_group_ad.policy_summary.approval_status": "APPROVED", "metrics.conversions_from_interactions_rate": 0.34582527027027027, "metrics.conversions_value": 1079.5535, "metrics.conversions": 12.795535, "metrics.cost_micros": 328930000, "metrics.cost_per_all_conversions": 1839736.7008074957, "metrics.cost_per_conversion": 25706623.443255793, "metrics.cost_per_current_model_attributed_conversion": 25706623.443255793, "ad_group_ad.ad.final_mobile_urls": [], "ad_group_ad.ad.final_urls": ["https://airbyte.com/"], "ad_group_ad.ad.tracking_url_template": "", "ad_group_ad.ad.url_custom_parameters": [], "metrics.cross_device_conversions": 2.0, "metrics.ctr": 0.14396887159533073, "metrics.current_model_attributed_conversions_value": 1079.5535, "metrics.current_model_attributed_conversions": 12.795535, "segments.date": "2024-01-01", "segments.day_of_week": "MONDAY", "ad_group_ad.ad.expanded_text_ad.description": "", "ad_group_ad.ad.text_ad.description1": "", "ad_group_ad.ad.text_ad.description2": "", "ad_group_ad.ad.device_preference": "UNSPECIFIED", "ad_group_ad.ad.display_url": "", "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "ad_group_ad.ad.legacy_responsive_display_ad.logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.marketing_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_marketing_image": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description": "", "ad_group_ad.ad.expanded_text_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.headline_part3": "", "customer.id": 4651612872, "ad_group_ad.ad.legacy_responsive_display_ad.format_setting": "UNSPECIFIED", "metrics.gmail_forwards": 0, "metrics.gmail_saves": 0, "metrics.gmail_secondary_clicks": 0, "ad_group_ad.ad.text_ad.headline": "", "ad_group_ad.ad.expanded_text_ad.headline_part1": "", "ad_group_ad.ad.expanded_text_ad.headline_part2": "", "ad_group_ad.ad.id": 676665180945, "ad_group_ad.ad.image_ad.image_url": "", "ad_group_ad.ad.image_ad.pixel_height": 0, "ad_group_ad.ad.image_ad.pixel_width": 0, "ad_group_ad.ad.image_ad.mime_type": "UNSPECIFIED", "ad_group_ad.ad.image_ad.name": "", "metrics.impressions": 257, "metrics.interaction_rate": 0.14396887159533073, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interactions": 37, "ad_group_ad.ad.legacy_responsive_display_ad.long_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.main_color": "", "segments.month": "2024-01-01", "ad_group_ad.ad.responsive_display_ad.accent_color": "", "ad_group_ad.ad.responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.responsive_display_ad.business_name": "", "ad_group_ad.ad.responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.responsive_display_ad.descriptions": [], "ad_group_ad.ad.responsive_display_ad.price_prefix": "", "ad_group_ad.ad.responsive_display_ad.promo_text": "", "ad_group_ad.ad.responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.responsive_display_ad.headlines": [], "ad_group_ad.ad.responsive_display_ad.logo_images": [], "ad_group_ad.ad.responsive_display_ad.square_logo_images": [], "ad_group_ad.ad.responsive_display_ad.long_headline": "", "ad_group_ad.ad.responsive_display_ad.main_color": "", "ad_group_ad.ad.responsive_display_ad.marketing_images": [], "ad_group_ad.ad.responsive_display_ad.square_marketing_images": [], "ad_group_ad.ad.responsive_display_ad.youtube_videos": [], "ad_group_ad.ad.expanded_text_ad.path1": "", "ad_group_ad.ad.expanded_text_ad.path2": "", "metrics.percent_new_visitors": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.price_prefix": "", "ad_group_ad.ad.legacy_responsive_display_ad.promo_text": "", "segments.quarter": "2024-01-01", "ad_group_ad.ad.responsive_search_ad.descriptions": ["text: \"The most comprehensive catalog of connectors, trusted by 40,000K engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"A high-performing and scalable data integration platform with advanced features.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte for free! Connect Any Data, Any User, & Any Application Effortlessly.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build custom connectors in 10 min with our no-code connector builder.\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.headlines": ["text: \"Airbyte\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"The only ETL tool you need\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build ELT Pipelines In Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No code, ELT Tool\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Replicate Data in Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open Source Integration\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte Cloud Free\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Browse Our Catalog\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Trusted by over 40K Engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"14 Day Free Trial\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"300+ off-the-shelf connectors\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No-Code Connector Builder\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Get started in minutes\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Extract, Load & Transform\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Streamlined Data Pipeline\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.path1": "", "ad_group_ad.ad.responsive_search_ad.path2": "", "ad_group_ad.ad.legacy_responsive_display_ad.short_headline": "", "ad_group_ad.status": "ENABLED", "ad_group_ad.ad.system_managed_resource_source": "UNSPECIFIED", "metrics.top_impression_percentage": 0.9688715953307393, "ad_group_ad.ad.app_ad.descriptions": [], "ad_group_ad.ad.app_ad.headlines": [], "ad_group_ad.ad.app_ad.html5_media_bundles": [], "ad_group_ad.ad.app_ad.images": [], "ad_group_ad.ad.app_ad.mandatory_ad_text": "", "ad_group_ad.ad.app_ad.youtube_videos": [], "metrics.value_per_all_conversions": 7.016816372469783, "metrics.value_per_conversion": 84.36954765861685, "metrics.value_per_current_model_attributed_conversion": 84.36954765861685, "metrics.video_quartile_p100_rate": 0.0, "metrics.video_quartile_p25_rate": 0.0, "metrics.video_quartile_p50_rate": 0.0, "metrics.video_quartile_p75_rate": 0.0, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0, "segments.week": "2024-01-01", "segments.year": 2024}, "emitted_at": 1704716796174} +{"stream": "campaign_budget", "data": {"customer.id": 4651612872, "campaign.id": 20643300404, "campaign_budget.aligned_bidding_strategy_id": 0, "campaign_budget.amount_micros": 330000000, "campaign_budget.delivery_method": "STANDARD", "campaign_budget.explicitly_shared": false, "campaign_budget.has_recommended_budget": false, "campaign_budget.id": 13022493317, "campaign_budget.name": "mm_search_brand", "campaign_budget.period": "DAILY", "campaign_budget.recommended_budget_amount_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_clicks": 0, "campaign_budget.recommended_budget_estimated_change_weekly_cost_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_interactions": 0, "campaign_budget.recommended_budget_estimated_change_weekly_views": 0, "campaign_budget.reference_count": 1, "campaign_budget.resource_name": "customers/4651612872/campaignBudgets/13022493317", "campaign_budget.status": "ENABLED", "campaign_budget.total_amount_micros": 0, "campaign_budget.type": "STANDARD", "segments.date": "2023-12-31", "segments.budget_campaign_association_status.campaign": "customers/4651612872/campaigns/20643300404", "segments.budget_campaign_association_status.status": "ENABLED", "metrics.all_conversions": 145.017621, "metrics.all_conversions_from_interactions_rate": 4.833920699999999, "metrics.all_conversions_value": 800.783622, "metrics.average_cost": 5602666.666666667, "metrics.average_cpc": 5602666.666666667, "metrics.average_cpe": 0.0, "metrics.average_cpm": 1031165644.1717792, "metrics.average_cpv": 0.0, "metrics.clicks": 30, "metrics.conversions": 6.623899, "metrics.conversions_from_interactions_rate": 0.22079663333333333, "metrics.conversions_value": 662.3899, "metrics.cost_micros": 168080000, "metrics.cost_per_all_conversions": 1159031.5634815167, "metrics.cost_per_conversion": 25374783.039415307, "metrics.cross_device_conversions": 0.0, "metrics.ctr": 0.18404907975460122, "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "metrics.impressions": 163, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interaction_rate": 0.18404907975460122, "metrics.interactions": 30, "metrics.value_per_all_conversions": 5.52197461576066, "metrics.value_per_conversion": 100.0, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0}, "emitted_at": 1704717423823} +{"stream": "campaign_budget", "data": {"customer.id": 4651612872, "campaign.id": 20643300404, "campaign_budget.aligned_bidding_strategy_id": 0, "campaign_budget.amount_micros": 330000000, "campaign_budget.delivery_method": "STANDARD", "campaign_budget.explicitly_shared": false, "campaign_budget.has_recommended_budget": false, "campaign_budget.id": 13022493317, "campaign_budget.name": "mm_search_brand", "campaign_budget.period": "DAILY", "campaign_budget.recommended_budget_amount_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_clicks": 0, "campaign_budget.recommended_budget_estimated_change_weekly_cost_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_interactions": 0, "campaign_budget.recommended_budget_estimated_change_weekly_views": 0, "campaign_budget.reference_count": 1, "campaign_budget.resource_name": "customers/4651612872/campaignBudgets/13022493317", "campaign_budget.status": "ENABLED", "campaign_budget.total_amount_micros": 0, "campaign_budget.type": "STANDARD", "segments.date": "2024-01-01", "segments.budget_campaign_association_status.campaign": "customers/4651612872/campaigns/20643300404", "segments.budget_campaign_association_status.status": "ENABLED", "metrics.all_conversions": 178.791889, "metrics.all_conversions_from_interactions_rate": 4.832213216216216, "metrics.all_conversions_value": 1254.549854, "metrics.average_cost": 8890000.0, "metrics.average_cpc": 8890000.0, "metrics.average_cpe": 0.0, "metrics.average_cpm": 1279883268.4824903, "metrics.average_cpv": 0.0, "metrics.clicks": 37, "metrics.conversions": 12.795535, "metrics.conversions_from_interactions_rate": 0.34582527027027027, "metrics.conversions_value": 1079.5535, "metrics.cost_micros": 328930000, "metrics.cost_per_all_conversions": 1839736.7008074957, "metrics.cost_per_conversion": 25706623.443255793, "metrics.cross_device_conversions": 2.0, "metrics.ctr": 0.14396887159533073, "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "metrics.impressions": 257, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interaction_rate": 0.14396887159533073, "metrics.interactions": 37, "metrics.value_per_all_conversions": 7.016816372469783, "metrics.value_per_conversion": 84.36954765861685, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0}, "emitted_at": 1704717423824} {"stream": "ad_group_custom", "data": {"ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "segments.date": "2024-01-03"}, "emitted_at": 1704408105935} {"stream": "ad_group_custom", "data": {"ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n", "targeting_dimension: TOPIC\nbid_only: true\n"], "segments.date": "2024-01-03"}, "emitted_at": 1704408105942} {"stream": "ad_group_custom", "data": {"ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AUDIENCE\nbid_only: true\n", "targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "segments.date": "2024-01-02"}, "emitted_at": 1704408105943} @@ -19,9 +19,9 @@ {"stream": "ad_group_ad", "data": {"ad_group.id": 155311392438, "ad_group_ad.ad.added_by_google_ads": false, "ad_group_ad.ad.app_ad.descriptions": [], "ad_group_ad.ad.app_ad.headlines": [], "ad_group_ad.ad.app_ad.html5_media_bundles": [], "ad_group_ad.ad.app_ad.images": [], "ad_group_ad.ad.app_ad.mandatory_ad_text": "", "ad_group_ad.ad.app_ad.youtube_videos": [], "ad_group_ad.ad.app_engagement_ad.descriptions": [], "ad_group_ad.ad.app_engagement_ad.headlines": [], "ad_group_ad.ad.app_engagement_ad.images": [], "ad_group_ad.ad.app_engagement_ad.videos": [], "ad_group_ad.ad.call_ad.business_name": "", "ad_group_ad.ad.call_ad.call_tracked": false, "ad_group_ad.ad.call_ad.conversion_action": "", "ad_group_ad.ad.call_ad.conversion_reporting_state": "UNSPECIFIED", "ad_group_ad.ad.call_ad.country_code": "", "ad_group_ad.ad.call_ad.description1": "", "ad_group_ad.ad.call_ad.description2": "", "ad_group_ad.ad.call_ad.disable_call_conversion": false, "ad_group_ad.ad.call_ad.headline1": "", "ad_group_ad.ad.call_ad.headline2": "", "ad_group_ad.ad.call_ad.path1": "", "ad_group_ad.ad.call_ad.path2": "", "ad_group_ad.ad.call_ad.phone_number": "", "ad_group_ad.ad.call_ad.phone_number_verification_url": "", "ad_group_ad.ad.device_preference": "UNSPECIFIED", "ad_group_ad.ad.display_upload_ad.display_upload_product_type": "UNSPECIFIED", "ad_group_ad.ad.display_upload_ad.media_bundle": "", "ad_group_ad.ad.display_url": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.description": "", "ad_group_ad.ad.expanded_text_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.headline_part1": "", "ad_group_ad.ad.expanded_text_ad.headline_part2": "", "ad_group_ad.ad.expanded_text_ad.headline_part3": "", "ad_group_ad.ad.expanded_text_ad.path1": "", "ad_group_ad.ad.expanded_text_ad.path2": "", "ad_group_ad.ad.final_app_urls": [], "ad_group_ad.ad.final_mobile_urls": [], "ad_group_ad.ad.final_url_suffix": "", "ad_group_ad.ad.final_urls": ["https://airbyte.com/"], "ad_group_ad.ad.hotel_ad": "", "ad_group_ad.ad.id": 676665180945, "ad_group_ad.ad.image_ad.image_url": "", "ad_group_ad.ad.image_ad.mime_type": "UNSPECIFIED", "ad_group_ad.ad.image_ad.name": "", "ad_group_ad.ad.image_ad.pixel_height": 0, "ad_group_ad.ad.image_ad.pixel_width": 0, "ad_group_ad.ad.image_ad.preview_image_url": "", "ad_group_ad.ad.image_ad.preview_pixel_height": 0, "ad_group_ad.ad.image_ad.preview_pixel_width": 0, "ad_group_ad.ad.legacy_app_install_ad": "", "ad_group_ad.ad.legacy_responsive_display_ad.accent_color": "", "ad_group_ad.ad.legacy_responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.legacy_responsive_display_ad.business_name": "", "ad_group_ad.ad.legacy_responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.legacy_responsive_display_ad.description": "", "ad_group_ad.ad.legacy_responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.legacy_responsive_display_ad.logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.long_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.main_color": "", "ad_group_ad.ad.legacy_responsive_display_ad.marketing_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.price_prefix": "", "ad_group_ad.ad.legacy_responsive_display_ad.promo_text": "", "ad_group_ad.ad.legacy_responsive_display_ad.short_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_marketing_image": "", "ad_group_ad.ad.local_ad.call_to_actions": [], "ad_group_ad.ad.local_ad.descriptions": [], "ad_group_ad.ad.local_ad.headlines": [], "ad_group_ad.ad.local_ad.logo_images": [], "ad_group_ad.ad.local_ad.marketing_images": [], "ad_group_ad.ad.local_ad.path1": "", "ad_group_ad.ad.local_ad.path2": "", "ad_group_ad.ad.local_ad.videos": [], "ad_group_ad.ad.name": "", "ad_group_ad.ad.resource_name": "customers/4651612872/ads/676665180945", "ad_group_ad.ad.responsive_display_ad.accent_color": "", "ad_group_ad.ad.responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.responsive_display_ad.business_name": "", "ad_group_ad.ad.responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.responsive_display_ad.control_spec.enable_asset_enhancements": false, "ad_group_ad.ad.responsive_display_ad.control_spec.enable_autogen_video": false, "ad_group_ad.ad.responsive_display_ad.descriptions": [], "ad_group_ad.ad.responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.responsive_display_ad.headlines": [], "ad_group_ad.ad.responsive_display_ad.logo_images": [], "ad_group_ad.ad.responsive_display_ad.long_headline": "", "ad_group_ad.ad.responsive_display_ad.main_color": "", "ad_group_ad.ad.responsive_display_ad.marketing_images": [], "ad_group_ad.ad.responsive_display_ad.price_prefix": "", "ad_group_ad.ad.responsive_display_ad.promo_text": "", "ad_group_ad.ad.responsive_display_ad.square_logo_images": [], "ad_group_ad.ad.responsive_display_ad.square_marketing_images": [], "ad_group_ad.ad.responsive_display_ad.youtube_videos": [], "ad_group_ad.ad.responsive_search_ad.descriptions": ["text: \"The most comprehensive catalog of connectors, trusted by 40,000K engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"A high-performing and scalable data integration platform with advanced features.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte for free! Connect Any Data, Any User, & Any Application Effortlessly.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build custom connectors in 10 min with our no-code connector builder.\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.headlines": ["text: \"Airbyte\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"The only ETL tool you need\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build ELT Pipelines In Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No code, ELT Tool\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Replicate Data in Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open Source Integration\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte Cloud Free\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Browse Our Catalog\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Trusted by over 40K Engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"14 Day Free Trial\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"300+ off-the-shelf connectors\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No-Code Connector Builder\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Get started in minutes\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Extract, Load & Transform\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Streamlined Data Pipeline\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.path1": "", "ad_group_ad.ad.responsive_search_ad.path2": "", "ad_group_ad.ad.shopping_comparison_listing_ad.headline": "", "ad_group_ad.ad.shopping_product_ad": "", "ad_group_ad.ad.shopping_smart_ad": "", "ad_group_ad.ad.smart_campaign_ad.descriptions": [], "ad_group_ad.ad.smart_campaign_ad.headlines": [], "ad_group_ad.ad.system_managed_resource_source": "UNSPECIFIED", "ad_group_ad.ad.text_ad.description1": "", "ad_group_ad.ad.text_ad.description2": "", "ad_group_ad.ad.text_ad.headline": "", "ad_group_ad.ad.tracking_url_template": "", "ad_group_ad.ad.type": "RESPONSIVE_SEARCH_AD", "ad_group_ad.ad.url_collections": [], "ad_group_ad.ad.url_custom_parameters": [], "ad_group_ad.ad.video_ad.in_feed.description1": "", "ad_group_ad.ad.video_ad.in_feed.description2": "", "ad_group_ad.ad.video_ad.in_feed.headline": "", "ad_group_ad.ad.video_ad.in_stream.action_button_label": "", "ad_group_ad.ad.video_ad.in_stream.action_headline": "", "ad_group_ad.ad.video_ad.out_stream.description": "", "ad_group_ad.ad.video_ad.out_stream.headline": "", "ad_group_ad.ad.video_responsive_ad.call_to_actions": [], "ad_group_ad.ad.video_responsive_ad.companion_banners": [], "ad_group_ad.ad.video_responsive_ad.descriptions": [], "ad_group_ad.ad.video_responsive_ad.headlines": [], "ad_group_ad.ad.video_responsive_ad.long_headlines": [], "ad_group_ad.ad.video_responsive_ad.videos": [], "ad_group_ad.ad_group": "customers/4651612872/adGroups/155311392438", "ad_group_ad.ad_strength": "EXCELLENT", "ad_group_ad.labels": [], "ad_group_ad.policy_summary.approval_status": "APPROVED", "ad_group_ad.policy_summary.policy_topic_entries": [], "ad_group_ad.policy_summary.review_status": "REVIEWED", "ad_group_ad.resource_name": "customers/4651612872/adGroupAds/155311392438~676665180945", "ad_group_ad.status": "ENABLED", "segments.date": "2023-12-31"}, "emitted_at": 1704408116313} {"stream": "ad_group_ad", "data": {"ad_group.id": 155311392438, "ad_group_ad.ad.added_by_google_ads": false, "ad_group_ad.ad.app_ad.descriptions": [], "ad_group_ad.ad.app_ad.headlines": [], "ad_group_ad.ad.app_ad.html5_media_bundles": [], "ad_group_ad.ad.app_ad.images": [], "ad_group_ad.ad.app_ad.mandatory_ad_text": "", "ad_group_ad.ad.app_ad.youtube_videos": [], "ad_group_ad.ad.app_engagement_ad.descriptions": [], "ad_group_ad.ad.app_engagement_ad.headlines": [], "ad_group_ad.ad.app_engagement_ad.images": [], "ad_group_ad.ad.app_engagement_ad.videos": [], "ad_group_ad.ad.call_ad.business_name": "", "ad_group_ad.ad.call_ad.call_tracked": false, "ad_group_ad.ad.call_ad.conversion_action": "", "ad_group_ad.ad.call_ad.conversion_reporting_state": "UNSPECIFIED", "ad_group_ad.ad.call_ad.country_code": "", "ad_group_ad.ad.call_ad.description1": "", "ad_group_ad.ad.call_ad.description2": "", "ad_group_ad.ad.call_ad.disable_call_conversion": false, "ad_group_ad.ad.call_ad.headline1": "", "ad_group_ad.ad.call_ad.headline2": "", "ad_group_ad.ad.call_ad.path1": "", "ad_group_ad.ad.call_ad.path2": "", "ad_group_ad.ad.call_ad.phone_number": "", "ad_group_ad.ad.call_ad.phone_number_verification_url": "", "ad_group_ad.ad.device_preference": "UNSPECIFIED", "ad_group_ad.ad.display_upload_ad.display_upload_product_type": "UNSPECIFIED", "ad_group_ad.ad.display_upload_ad.media_bundle": "", "ad_group_ad.ad.display_url": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.description": "", "ad_group_ad.ad.expanded_text_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.headline_part1": "", "ad_group_ad.ad.expanded_text_ad.headline_part2": "", "ad_group_ad.ad.expanded_text_ad.headline_part3": "", "ad_group_ad.ad.expanded_text_ad.path1": "", "ad_group_ad.ad.expanded_text_ad.path2": "", "ad_group_ad.ad.final_app_urls": [], "ad_group_ad.ad.final_mobile_urls": [], "ad_group_ad.ad.final_url_suffix": "", "ad_group_ad.ad.final_urls": ["https://airbyte.com/"], "ad_group_ad.ad.hotel_ad": "", "ad_group_ad.ad.id": 676665180945, "ad_group_ad.ad.image_ad.image_url": "", "ad_group_ad.ad.image_ad.mime_type": "UNSPECIFIED", "ad_group_ad.ad.image_ad.name": "", "ad_group_ad.ad.image_ad.pixel_height": 0, "ad_group_ad.ad.image_ad.pixel_width": 0, "ad_group_ad.ad.image_ad.preview_image_url": "", "ad_group_ad.ad.image_ad.preview_pixel_height": 0, "ad_group_ad.ad.image_ad.preview_pixel_width": 0, "ad_group_ad.ad.legacy_app_install_ad": "", "ad_group_ad.ad.legacy_responsive_display_ad.accent_color": "", "ad_group_ad.ad.legacy_responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.legacy_responsive_display_ad.business_name": "", "ad_group_ad.ad.legacy_responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.legacy_responsive_display_ad.description": "", "ad_group_ad.ad.legacy_responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.legacy_responsive_display_ad.logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.long_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.main_color": "", "ad_group_ad.ad.legacy_responsive_display_ad.marketing_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.price_prefix": "", "ad_group_ad.ad.legacy_responsive_display_ad.promo_text": "", "ad_group_ad.ad.legacy_responsive_display_ad.short_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_marketing_image": "", "ad_group_ad.ad.local_ad.call_to_actions": [], "ad_group_ad.ad.local_ad.descriptions": [], "ad_group_ad.ad.local_ad.headlines": [], "ad_group_ad.ad.local_ad.logo_images": [], "ad_group_ad.ad.local_ad.marketing_images": [], "ad_group_ad.ad.local_ad.path1": "", "ad_group_ad.ad.local_ad.path2": "", "ad_group_ad.ad.local_ad.videos": [], "ad_group_ad.ad.name": "", "ad_group_ad.ad.resource_name": "customers/4651612872/ads/676665180945", "ad_group_ad.ad.responsive_display_ad.accent_color": "", "ad_group_ad.ad.responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.responsive_display_ad.business_name": "", "ad_group_ad.ad.responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.responsive_display_ad.control_spec.enable_asset_enhancements": false, "ad_group_ad.ad.responsive_display_ad.control_spec.enable_autogen_video": false, "ad_group_ad.ad.responsive_display_ad.descriptions": [], "ad_group_ad.ad.responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.responsive_display_ad.headlines": [], "ad_group_ad.ad.responsive_display_ad.logo_images": [], "ad_group_ad.ad.responsive_display_ad.long_headline": "", "ad_group_ad.ad.responsive_display_ad.main_color": "", "ad_group_ad.ad.responsive_display_ad.marketing_images": [], "ad_group_ad.ad.responsive_display_ad.price_prefix": "", "ad_group_ad.ad.responsive_display_ad.promo_text": "", "ad_group_ad.ad.responsive_display_ad.square_logo_images": [], "ad_group_ad.ad.responsive_display_ad.square_marketing_images": [], "ad_group_ad.ad.responsive_display_ad.youtube_videos": [], "ad_group_ad.ad.responsive_search_ad.descriptions": ["text: \"The most comprehensive catalog of connectors, trusted by 40,000K engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"A high-performing and scalable data integration platform with advanced features.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte for free! Connect Any Data, Any User, & Any Application Effortlessly.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build custom connectors in 10 min with our no-code connector builder.\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.headlines": ["text: \"Airbyte\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"The only ETL tool you need\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build ELT Pipelines In Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No code, ELT Tool\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Replicate Data in Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open Source Integration\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte Cloud Free\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Browse Our Catalog\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Trusted by over 40K Engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"14 Day Free Trial\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"300+ off-the-shelf connectors\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No-Code Connector Builder\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Get started in minutes\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Extract, Load & Transform\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Streamlined Data Pipeline\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.path1": "", "ad_group_ad.ad.responsive_search_ad.path2": "", "ad_group_ad.ad.shopping_comparison_listing_ad.headline": "", "ad_group_ad.ad.shopping_product_ad": "", "ad_group_ad.ad.shopping_smart_ad": "", "ad_group_ad.ad.smart_campaign_ad.descriptions": [], "ad_group_ad.ad.smart_campaign_ad.headlines": [], "ad_group_ad.ad.system_managed_resource_source": "UNSPECIFIED", "ad_group_ad.ad.text_ad.description1": "", "ad_group_ad.ad.text_ad.description2": "", "ad_group_ad.ad.text_ad.headline": "", "ad_group_ad.ad.tracking_url_template": "", "ad_group_ad.ad.type": "RESPONSIVE_SEARCH_AD", "ad_group_ad.ad.url_collections": [], "ad_group_ad.ad.url_custom_parameters": [], "ad_group_ad.ad.video_ad.in_feed.description1": "", "ad_group_ad.ad.video_ad.in_feed.description2": "", "ad_group_ad.ad.video_ad.in_feed.headline": "", "ad_group_ad.ad.video_ad.in_stream.action_button_label": "", "ad_group_ad.ad.video_ad.in_stream.action_headline": "", "ad_group_ad.ad.video_ad.out_stream.description": "", "ad_group_ad.ad.video_ad.out_stream.headline": "", "ad_group_ad.ad.video_responsive_ad.call_to_actions": [], "ad_group_ad.ad.video_responsive_ad.companion_banners": [], "ad_group_ad.ad.video_responsive_ad.descriptions": [], "ad_group_ad.ad.video_responsive_ad.headlines": [], "ad_group_ad.ad.video_responsive_ad.long_headlines": [], "ad_group_ad.ad.video_responsive_ad.videos": [], "ad_group_ad.ad_group": "customers/4651612872/adGroups/155311392438", "ad_group_ad.ad_strength": "EXCELLENT", "ad_group_ad.labels": [], "ad_group_ad.policy_summary.approval_status": "APPROVED", "ad_group_ad.policy_summary.policy_topic_entries": [], "ad_group_ad.policy_summary.review_status": "REVIEWED", "ad_group_ad.resource_name": "customers/4651612872/adGroupAds/155311392438~676665180945", "ad_group_ad.status": "ENABLED", "segments.date": "2024-01-01"}, "emitted_at": 1704408116319} {"stream": "ad_group_ad", "data": {"ad_group.id": 155311392438, "ad_group_ad.ad.added_by_google_ads": false, "ad_group_ad.ad.app_ad.descriptions": [], "ad_group_ad.ad.app_ad.headlines": [], "ad_group_ad.ad.app_ad.html5_media_bundles": [], "ad_group_ad.ad.app_ad.images": [], "ad_group_ad.ad.app_ad.mandatory_ad_text": "", "ad_group_ad.ad.app_ad.youtube_videos": [], "ad_group_ad.ad.app_engagement_ad.descriptions": [], "ad_group_ad.ad.app_engagement_ad.headlines": [], "ad_group_ad.ad.app_engagement_ad.images": [], "ad_group_ad.ad.app_engagement_ad.videos": [], "ad_group_ad.ad.call_ad.business_name": "", "ad_group_ad.ad.call_ad.call_tracked": false, "ad_group_ad.ad.call_ad.conversion_action": "", "ad_group_ad.ad.call_ad.conversion_reporting_state": "UNSPECIFIED", "ad_group_ad.ad.call_ad.country_code": "", "ad_group_ad.ad.call_ad.description1": "", "ad_group_ad.ad.call_ad.description2": "", "ad_group_ad.ad.call_ad.disable_call_conversion": false, "ad_group_ad.ad.call_ad.headline1": "", "ad_group_ad.ad.call_ad.headline2": "", "ad_group_ad.ad.call_ad.path1": "", "ad_group_ad.ad.call_ad.path2": "", "ad_group_ad.ad.call_ad.phone_number": "", "ad_group_ad.ad.call_ad.phone_number_verification_url": "", "ad_group_ad.ad.device_preference": "UNSPECIFIED", "ad_group_ad.ad.display_upload_ad.display_upload_product_type": "UNSPECIFIED", "ad_group_ad.ad.display_upload_ad.media_bundle": "", "ad_group_ad.ad.display_url": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.description": "", "ad_group_ad.ad.expanded_text_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.headline_part1": "", "ad_group_ad.ad.expanded_text_ad.headline_part2": "", "ad_group_ad.ad.expanded_text_ad.headline_part3": "", "ad_group_ad.ad.expanded_text_ad.path1": "", "ad_group_ad.ad.expanded_text_ad.path2": "", "ad_group_ad.ad.final_app_urls": [], "ad_group_ad.ad.final_mobile_urls": [], "ad_group_ad.ad.final_url_suffix": "", "ad_group_ad.ad.final_urls": ["https://airbyte.com/"], "ad_group_ad.ad.hotel_ad": "", "ad_group_ad.ad.id": 676665180945, "ad_group_ad.ad.image_ad.image_url": "", "ad_group_ad.ad.image_ad.mime_type": "UNSPECIFIED", "ad_group_ad.ad.image_ad.name": "", "ad_group_ad.ad.image_ad.pixel_height": 0, "ad_group_ad.ad.image_ad.pixel_width": 0, "ad_group_ad.ad.image_ad.preview_image_url": "", "ad_group_ad.ad.image_ad.preview_pixel_height": 0, "ad_group_ad.ad.image_ad.preview_pixel_width": 0, "ad_group_ad.ad.legacy_app_install_ad": "", "ad_group_ad.ad.legacy_responsive_display_ad.accent_color": "", "ad_group_ad.ad.legacy_responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.legacy_responsive_display_ad.business_name": "", "ad_group_ad.ad.legacy_responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.legacy_responsive_display_ad.description": "", "ad_group_ad.ad.legacy_responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.legacy_responsive_display_ad.logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.long_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.main_color": "", "ad_group_ad.ad.legacy_responsive_display_ad.marketing_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.price_prefix": "", "ad_group_ad.ad.legacy_responsive_display_ad.promo_text": "", "ad_group_ad.ad.legacy_responsive_display_ad.short_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_marketing_image": "", "ad_group_ad.ad.local_ad.call_to_actions": [], "ad_group_ad.ad.local_ad.descriptions": [], "ad_group_ad.ad.local_ad.headlines": [], "ad_group_ad.ad.local_ad.logo_images": [], "ad_group_ad.ad.local_ad.marketing_images": [], "ad_group_ad.ad.local_ad.path1": "", "ad_group_ad.ad.local_ad.path2": "", "ad_group_ad.ad.local_ad.videos": [], "ad_group_ad.ad.name": "", "ad_group_ad.ad.resource_name": "customers/4651612872/ads/676665180945", "ad_group_ad.ad.responsive_display_ad.accent_color": "", "ad_group_ad.ad.responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.responsive_display_ad.business_name": "", "ad_group_ad.ad.responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.responsive_display_ad.control_spec.enable_asset_enhancements": false, "ad_group_ad.ad.responsive_display_ad.control_spec.enable_autogen_video": false, "ad_group_ad.ad.responsive_display_ad.descriptions": [], "ad_group_ad.ad.responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.responsive_display_ad.headlines": [], "ad_group_ad.ad.responsive_display_ad.logo_images": [], "ad_group_ad.ad.responsive_display_ad.long_headline": "", "ad_group_ad.ad.responsive_display_ad.main_color": "", "ad_group_ad.ad.responsive_display_ad.marketing_images": [], "ad_group_ad.ad.responsive_display_ad.price_prefix": "", "ad_group_ad.ad.responsive_display_ad.promo_text": "", "ad_group_ad.ad.responsive_display_ad.square_logo_images": [], "ad_group_ad.ad.responsive_display_ad.square_marketing_images": [], "ad_group_ad.ad.responsive_display_ad.youtube_videos": [], "ad_group_ad.ad.responsive_search_ad.descriptions": ["text: \"The most comprehensive catalog of connectors, trusted by 40,000K engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"A high-performing and scalable data integration platform with advanced features.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte for free! Connect Any Data, Any User, & Any Application Effortlessly.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build custom connectors in 10 min with our no-code connector builder.\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.headlines": ["text: \"Airbyte\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"The only ETL tool you need\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build ELT Pipelines In Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No code, ELT Tool\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Replicate Data in Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open Source Integration\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte Cloud Free\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Browse Our Catalog\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Trusted by over 40K Engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"14 Day Free Trial\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"300+ off-the-shelf connectors\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No-Code Connector Builder\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Get started in minutes\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Extract, Load & Transform\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Streamlined Data Pipeline\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.path1": "", "ad_group_ad.ad.responsive_search_ad.path2": "", "ad_group_ad.ad.shopping_comparison_listing_ad.headline": "", "ad_group_ad.ad.shopping_product_ad": "", "ad_group_ad.ad.shopping_smart_ad": "", "ad_group_ad.ad.smart_campaign_ad.descriptions": [], "ad_group_ad.ad.smart_campaign_ad.headlines": [], "ad_group_ad.ad.system_managed_resource_source": "UNSPECIFIED", "ad_group_ad.ad.text_ad.description1": "", "ad_group_ad.ad.text_ad.description2": "", "ad_group_ad.ad.text_ad.headline": "", "ad_group_ad.ad.tracking_url_template": "", "ad_group_ad.ad.type": "RESPONSIVE_SEARCH_AD", "ad_group_ad.ad.url_collections": [], "ad_group_ad.ad.url_custom_parameters": [], "ad_group_ad.ad.video_ad.in_feed.description1": "", "ad_group_ad.ad.video_ad.in_feed.description2": "", "ad_group_ad.ad.video_ad.in_feed.headline": "", "ad_group_ad.ad.video_ad.in_stream.action_button_label": "", "ad_group_ad.ad.video_ad.in_stream.action_headline": "", "ad_group_ad.ad.video_ad.out_stream.description": "", "ad_group_ad.ad.video_ad.out_stream.headline": "", "ad_group_ad.ad.video_responsive_ad.call_to_actions": [], "ad_group_ad.ad.video_responsive_ad.companion_banners": [], "ad_group_ad.ad.video_responsive_ad.descriptions": [], "ad_group_ad.ad.video_responsive_ad.headlines": [], "ad_group_ad.ad.video_responsive_ad.long_headlines": [], "ad_group_ad.ad.video_responsive_ad.videos": [], "ad_group_ad.ad_group": "customers/4651612872/adGroups/155311392438", "ad_group_ad.ad_strength": "EXCELLENT", "ad_group_ad.labels": [], "ad_group_ad.policy_summary.approval_status": "APPROVED", "ad_group_ad.policy_summary.policy_topic_entries": [], "ad_group_ad.policy_summary.review_status": "REVIEWED", "ad_group_ad.resource_name": "customers/4651612872/adGroupAds/155311392438~676665180945", "ad_group_ad.status": "ENABLED", "segments.date": "2024-01-02"}, "emitted_at": 1704408116320} -{"stream": "ad_group", "data": {"campaign.id": 20643300404, "ad_group.ad_rotation_mode": "UNSPECIFIED", "ad_group.base_ad_group": "customers/4651612872/adGroups/155311392438", "ad_group.campaign": "customers/4651612872/campaigns/20643300404", "ad_group.cpc_bid_micros": 10000, "ad_group.cpm_bid_micros": 10000, "ad_group.cpv_bid_micros": 0, "ad_group.display_custom_bid_dimension": "UNSPECIFIED", "ad_group.effective_target_cpa_micros": 0, "ad_group.effective_target_cpa_source": "UNSPECIFIED", "ad_group.effective_target_roas": 0.0, "ad_group.effective_target_roas_source": "UNSPECIFIED", "ad_group.excluded_parent_asset_field_types": [], "ad_group.optimized_targeting_enabled": false, "ad_group.final_url_suffix": "", "ad_group.id": 155311392438, "ad_group.labels": [], "ad_group.name": "Airbyte", "ad_group.percent_cpc_bid_micros": 0, "ad_group.resource_name": "customers/4651612872/adGroups/155311392438", "ad_group.status": "ENABLED", "ad_group.target_cpa_micros": 0, "ad_group.target_cpm_micros": 10000, "ad_group.target_roas": 0.0, "ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AUDIENCE\nbid_only: true\n", "targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "ad_group.tracking_url_template": "", "ad_group.type": "SEARCH_STANDARD", "ad_group.url_custom_parameters": ["key: \"adgroup\"\nvalue: \"Airbyte\"\n", "key: \"campaign\"\nvalue: \"mm_search_brand\"\n"], "segments.date": "2023-12-31"}, "emitted_at": 1704408117013} -{"stream": "ad_group", "data": {"campaign.id": 20643300404, "ad_group.ad_rotation_mode": "UNSPECIFIED", "ad_group.base_ad_group": "customers/4651612872/adGroups/155311392438", "ad_group.campaign": "customers/4651612872/campaigns/20643300404", "ad_group.cpc_bid_micros": 10000, "ad_group.cpm_bid_micros": 10000, "ad_group.cpv_bid_micros": 0, "ad_group.display_custom_bid_dimension": "UNSPECIFIED", "ad_group.effective_target_cpa_micros": 0, "ad_group.effective_target_cpa_source": "UNSPECIFIED", "ad_group.effective_target_roas": 0.0, "ad_group.effective_target_roas_source": "UNSPECIFIED", "ad_group.excluded_parent_asset_field_types": [], "ad_group.optimized_targeting_enabled": false, "ad_group.final_url_suffix": "", "ad_group.id": 155311392438, "ad_group.labels": [], "ad_group.name": "Airbyte", "ad_group.percent_cpc_bid_micros": 0, "ad_group.resource_name": "customers/4651612872/adGroups/155311392438", "ad_group.status": "ENABLED", "ad_group.target_cpa_micros": 0, "ad_group.target_cpm_micros": 10000, "ad_group.target_roas": 0.0, "ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AUDIENCE\nbid_only: true\n", "targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "ad_group.tracking_url_template": "", "ad_group.type": "SEARCH_STANDARD", "ad_group.url_custom_parameters": ["key: \"adgroup\"\nvalue: \"Airbyte\"\n", "key: \"campaign\"\nvalue: \"mm_search_brand\"\n"], "segments.date": "2024-01-01"}, "emitted_at": 1704408117014} -{"stream": "ad_group", "data": {"campaign.id": 20655886237, "ad_group.ad_rotation_mode": "UNSPECIFIED", "ad_group.base_ad_group": "customers/4651612872/adGroups/152406853857", "ad_group.campaign": "customers/4651612872/campaigns/20655886237", "ad_group.cpc_bid_micros": 10000, "ad_group.cpm_bid_micros": 10000, "ad_group.cpv_bid_micros": 0, "ad_group.display_custom_bid_dimension": "UNSPECIFIED", "ad_group.effective_target_cpa_micros": 0, "ad_group.effective_target_cpa_source": "UNSPECIFIED", "ad_group.effective_target_roas": 0.0, "ad_group.effective_target_roas_source": "UNSPECIFIED", "ad_group.excluded_parent_asset_field_types": [], "ad_group.optimized_targeting_enabled": false, "ad_group.final_url_suffix": "", "ad_group.id": 152406853857, "ad_group.labels": [], "ad_group.name": "Fivetran", "ad_group.percent_cpc_bid_micros": 0, "ad_group.resource_name": "customers/4651612872/adGroups/152406853857", "ad_group.status": "ENABLED", "ad_group.target_cpa_micros": 0, "ad_group.target_cpm_micros": 10000, "ad_group.target_roas": 0.0, "ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AUDIENCE\nbid_only: true\n", "targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "ad_group.tracking_url_template": "", "ad_group.type": "SEARCH_STANDARD", "ad_group.url_custom_parameters": ["key: \"adgroup\"\nvalue: \"Fivetran\"\n", "key: \"campaign\"\nvalue: \"mm_search_competitors\"\n"], "segments.date": "2024-01-02"}, "emitted_at": 1704408117015} +{"stream": "ad_group", "data": {"campaign.id": 20643300404, "ad_group.ad_rotation_mode": "UNSPECIFIED", "ad_group.base_ad_group": "customers/4651612872/adGroups/155311392438", "ad_group.campaign": "customers/4651612872/campaigns/20643300404", "metrics.cost_micros": 168080000, "ad_group.cpc_bid_micros": 10000, "ad_group.cpm_bid_micros": 10000, "ad_group.cpv_bid_micros": 0, "ad_group.display_custom_bid_dimension": "UNSPECIFIED", "ad_group.effective_target_cpa_micros": 0, "ad_group.effective_target_cpa_source": "UNSPECIFIED", "ad_group.effective_target_roas": 0.0, "ad_group.effective_target_roas_source": "UNSPECIFIED", "ad_group.excluded_parent_asset_field_types": [], "ad_group.optimized_targeting_enabled": false, "ad_group.final_url_suffix": "", "ad_group.id": 155311392438, "ad_group.labels": [], "ad_group.name": "Airbyte", "ad_group.percent_cpc_bid_micros": 0, "ad_group.resource_name": "customers/4651612872/adGroups/155311392438", "ad_group.status": "ENABLED", "ad_group.target_cpa_micros": 0, "ad_group.target_cpm_micros": 10000, "ad_group.target_roas": 0.0, "ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AUDIENCE\nbid_only: true\n", "targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "ad_group.tracking_url_template": "", "ad_group.type": "SEARCH_STANDARD", "ad_group.url_custom_parameters": ["key: \"adgroup\"\nvalue: \"Airbyte\"\n", "key: \"campaign\"\nvalue: \"mm_search_brand\"\n"], "segments.date": "2023-12-31"}, "emitted_at": 1704717743436} +{"stream": "ad_group", "data": {"campaign.id": 20643300404, "ad_group.ad_rotation_mode": "UNSPECIFIED", "ad_group.base_ad_group": "customers/4651612872/adGroups/155311392438", "ad_group.campaign": "customers/4651612872/campaigns/20643300404", "metrics.cost_micros": 328930000, "ad_group.cpc_bid_micros": 10000, "ad_group.cpm_bid_micros": 10000, "ad_group.cpv_bid_micros": 0, "ad_group.display_custom_bid_dimension": "UNSPECIFIED", "ad_group.effective_target_cpa_micros": 0, "ad_group.effective_target_cpa_source": "UNSPECIFIED", "ad_group.effective_target_roas": 0.0, "ad_group.effective_target_roas_source": "UNSPECIFIED", "ad_group.excluded_parent_asset_field_types": [], "ad_group.optimized_targeting_enabled": false, "ad_group.final_url_suffix": "", "ad_group.id": 155311392438, "ad_group.labels": [], "ad_group.name": "Airbyte", "ad_group.percent_cpc_bid_micros": 0, "ad_group.resource_name": "customers/4651612872/adGroups/155311392438", "ad_group.status": "ENABLED", "ad_group.target_cpa_micros": 0, "ad_group.target_cpm_micros": 10000, "ad_group.target_roas": 0.0, "ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AUDIENCE\nbid_only: true\n", "targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "ad_group.tracking_url_template": "", "ad_group.type": "SEARCH_STANDARD", "ad_group.url_custom_parameters": ["key: \"adgroup\"\nvalue: \"Airbyte\"\n", "key: \"campaign\"\nvalue: \"mm_search_brand\"\n"], "segments.date": "2024-01-01"}, "emitted_at": 1704717743438} +{"stream": "ad_group", "data": {"campaign.id": 20655886237, "ad_group.ad_rotation_mode": "UNSPECIFIED", "ad_group.base_ad_group": "customers/4651612872/adGroups/153930342465", "ad_group.campaign": "customers/4651612872/campaigns/20655886237", "metrics.cost_micros": 27110000, "ad_group.cpc_bid_micros": 10000, "ad_group.cpm_bid_micros": 10000, "ad_group.cpv_bid_micros": 0, "ad_group.display_custom_bid_dimension": "UNSPECIFIED", "ad_group.effective_target_cpa_micros": 0, "ad_group.effective_target_cpa_source": "UNSPECIFIED", "ad_group.effective_target_roas": 0.0, "ad_group.effective_target_roas_source": "UNSPECIFIED", "ad_group.excluded_parent_asset_field_types": [], "ad_group.optimized_targeting_enabled": false, "ad_group.final_url_suffix": "", "ad_group.id": 153930342465, "ad_group.labels": [], "ad_group.name": "Airflow", "ad_group.percent_cpc_bid_micros": 0, "ad_group.resource_name": "customers/4651612872/adGroups/153930342465", "ad_group.status": "ENABLED", "ad_group.target_cpa_micros": 0, "ad_group.target_cpm_micros": 10000, "ad_group.target_roas": 0.0, "ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AUDIENCE\nbid_only: true\n", "targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "ad_group.tracking_url_template": "", "ad_group.type": "SEARCH_STANDARD", "ad_group.url_custom_parameters": ["key: \"adgroup\"\nvalue: \"Airflow\"\n", "key: \"campaign\"\nvalue: \"mm_search_competitors\"\n"], "segments.date": "2024-01-02"}, "emitted_at": 1704717743440} {"stream": "customer", "data": {"customer.auto_tagging_enabled": true, "customer.call_reporting_setting.call_conversion_action": "customers/4651612872/conversionActions/179", "customer.call_reporting_setting.call_conversion_reporting_enabled": true, "customer.call_reporting_setting.call_reporting_enabled": true, "customer.conversion_tracking_setting.conversion_tracking_id": 657981234, "customer.conversion_tracking_setting.cross_account_conversion_tracking_id": 0, "customer.currency_code": "USD", "customer.descriptive_name": "Airbyte", "customer.final_url_suffix": "", "customer.has_partners_badge": false, "customer.id": 4651612872, "customer.manager": false, "customer.optimization_score": 0.7609283000000001, "customer.optimization_score_weight": 3182.4700060000005, "customer.pay_per_conversion_eligibility_failure_reasons": [], "customer.remarketing_setting.google_global_site_tag": "\n\n\n", "customer.resource_name": "customers/4651612872", "customer.test_account": false, "customer.time_zone": "America/Los_Angeles", "customer.tracking_url_template": "{lpurl}?utm_term={keyword}&utm_campaign={_utmcampaign}&utm_source=adwords&utm_medium=ppc&hsa_acc=4651612872&hsa_cam={campaignid}&hsa_grp={adgroupid}&hsa_ad={creative}&hsa_src={network}&hsa_tgt={targetid}&hsa_kw={keyword}&hsa_mt={matchtype}&hsa_net=adwords&hsa_ver=3", "segments.date": "2023-12-31"}, "emitted_at": 1704408117407} {"stream": "customer", "data": {"customer.auto_tagging_enabled": true, "customer.call_reporting_setting.call_conversion_action": "customers/4651612872/conversionActions/179", "customer.call_reporting_setting.call_conversion_reporting_enabled": true, "customer.call_reporting_setting.call_reporting_enabled": true, "customer.conversion_tracking_setting.conversion_tracking_id": 657981234, "customer.conversion_tracking_setting.cross_account_conversion_tracking_id": 0, "customer.currency_code": "USD", "customer.descriptive_name": "Airbyte", "customer.final_url_suffix": "", "customer.has_partners_badge": false, "customer.id": 4651612872, "customer.manager": false, "customer.optimization_score": 0.7609283000000001, "customer.optimization_score_weight": 3182.4700060000005, "customer.pay_per_conversion_eligibility_failure_reasons": [], "customer.remarketing_setting.google_global_site_tag": "\n\n\n", "customer.resource_name": "customers/4651612872", "customer.test_account": false, "customer.time_zone": "America/Los_Angeles", "customer.tracking_url_template": "{lpurl}?utm_term={keyword}&utm_campaign={_utmcampaign}&utm_source=adwords&utm_medium=ppc&hsa_acc=4651612872&hsa_cam={campaignid}&hsa_grp={adgroupid}&hsa_ad={creative}&hsa_src={network}&hsa_tgt={targetid}&hsa_kw={keyword}&hsa_mt={matchtype}&hsa_net=adwords&hsa_ver=3", "segments.date": "2024-01-01"}, "emitted_at": 1704408117408} {"stream": "customer", "data": {"customer.auto_tagging_enabled": true, "customer.call_reporting_setting.call_conversion_action": "customers/4651612872/conversionActions/179", "customer.call_reporting_setting.call_conversion_reporting_enabled": true, "customer.call_reporting_setting.call_reporting_enabled": true, "customer.conversion_tracking_setting.conversion_tracking_id": 657981234, "customer.conversion_tracking_setting.cross_account_conversion_tracking_id": 0, "customer.currency_code": "USD", "customer.descriptive_name": "Airbyte", "customer.final_url_suffix": "", "customer.has_partners_badge": false, "customer.id": 4651612872, "customer.manager": false, "customer.optimization_score": 0.7609283000000001, "customer.optimization_score_weight": 3182.4700060000005, "customer.pay_per_conversion_eligibility_failure_reasons": [], "customer.remarketing_setting.google_global_site_tag": "\n\n\n", "customer.resource_name": "customers/4651612872", "customer.test_account": false, "customer.time_zone": "America/Los_Angeles", "customer.tracking_url_template": "{lpurl}?utm_term={keyword}&utm_campaign={_utmcampaign}&utm_source=adwords&utm_medium=ppc&hsa_acc=4651612872&hsa_cam={campaignid}&hsa_grp={adgroupid}&hsa_ad={creative}&hsa_src={network}&hsa_tgt={targetid}&hsa_kw={keyword}&hsa_mt={matchtype}&hsa_net=adwords&hsa_ver=3", "segments.date": "2024-01-02"}, "emitted_at": 1704408117408} diff --git a/airbyte-integrations/connectors/source-google-ads/metadata.yaml b/airbyte-integrations/connectors/source-google-ads/metadata.yaml index 0471931bc5ec4..af49b32503d0c 100644 --- a/airbyte-integrations/connectors/source-google-ads/metadata.yaml +++ b/airbyte-integrations/connectors/source-google-ads/metadata.yaml @@ -11,7 +11,7 @@ data: connectorSubtype: api connectorType: source definitionId: 253487c0-2246-43ba-a21f-5116b20a2c50 - dockerImageTag: 3.0.1 + dockerImageTag: 3.0.2 dockerRepository: airbyte/source-google-ads documentationUrl: https://docs.airbyte.com/integrations/sources/google-ads githubIssueLabel: source-google-ads diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/schemas/ad_group.json b/airbyte-integrations/connectors/source-google-ads/source_google_ads/schemas/ad_group.json index 87f32300d8096..96dbdc94edea5 100644 --- a/airbyte-integrations/connectors/source-google-ads/source_google_ads/schemas/ad_group.json +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/schemas/ad_group.json @@ -14,6 +14,9 @@ "ad_group.campaign": { "type": ["null", "string"] }, + "metrics.cost_micros": { + "type": ["null", "integer"] + }, "ad_group.cpc_bid_micros": { "type": ["null", "integer"] }, diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py b/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py index c2c6a3bfc94de..695ad285c8a64 100644 --- a/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py @@ -16,7 +16,7 @@ from google.ads.googleads.errors import GoogleAdsException from google.ads.googleads.v15.services.services.google_ads_service.pagers import SearchPager from google.ads.googleads.v15.services.types.google_ads_service import SearchGoogleAdsResponse -from google.api_core.exceptions import InternalServerError, ServerError, ServiceUnavailable, TooManyRequests +from google.api_core.exceptions import InternalServerError, ServerError, ServiceUnavailable, TooManyRequests, Unauthenticated from .google_ads import GoogleAds, logger from .models import CustomerModel @@ -65,7 +65,7 @@ def read_records(self, sync_mode, stream_slice: Optional[Mapping[str, Any]] = No customer_id = stream_slice["customer_id"] try: yield from self.request_records_job(customer_id, self.get_query(stream_slice), stream_slice) - except GoogleAdsException as exception: + except (GoogleAdsException, Unauthenticated) as exception: traced_exception(exception, customer_id, self.CATCH_CUSTOMER_NOT_ENABLED_ERROR) except TimeoutError as exception: # Prevent sync failure diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/utils.py b/airbyte-integrations/connectors/source-google-ads/source_google_ads/utils.py index 0f438026d4341..3085343c92789 100644 --- a/airbyte-integrations/connectors/source-google-ads/source_google_ads/utils.py +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/utils.py @@ -19,6 +19,7 @@ from google.ads.googleads.v15.errors.types.authorization_error import AuthorizationErrorEnum from google.ads.googleads.v15.errors.types.quota_error import QuotaErrorEnum from google.ads.googleads.v15.errors.types.request_error import RequestErrorEnum +from google.api_core.exceptions import Unauthenticated from source_google_ads.google_ads import logger @@ -53,12 +54,19 @@ def is_error_type(error_value, target_enum_value): return int(error_value) == int(target_enum_value) -def traced_exception(ga_exception: GoogleAdsException, customer_id: str, catch_disabled_customer_error: bool): +def traced_exception(ga_exception: Union[GoogleAdsException, Unauthenticated], customer_id: str, catch_disabled_customer_error: bool): """Add user-friendly message for GoogleAdsException""" messages = [] raise_exception = AirbyteTracedException failure_type = FailureType.config_error + if isinstance(ga_exception, Unauthenticated): + message = ( + f"Authentication failed for the customer '{customer_id}'. " + f"Please try to Re-authenticate your credentials on set up Google Ads page." + ) + raise raise_exception.from_exception(failure_type=failure_type, exc=ga_exception, message=message) from ga_exception + for error in ga_exception.failure.errors: # Get error codes authorization_error = error.error_code.authorization_error diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_streams.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_streams.py index 9f8c65fcb8469..67015ec041dfe 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_streams.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_streams.py @@ -11,10 +11,10 @@ from google.ads.googleads.errors import GoogleAdsException from google.ads.googleads.v15.errors.types.errors import ErrorCode, GoogleAdsError, GoogleAdsFailure from google.ads.googleads.v15.errors.types.request_error import RequestErrorEnum -from google.api_core.exceptions import DataLoss, InternalServerError, ResourceExhausted, TooManyRequests +from google.api_core.exceptions import DataLoss, InternalServerError, ResourceExhausted, TooManyRequests, Unauthenticated from grpc import RpcError from source_google_ads.google_ads import GoogleAds -from source_google_ads.streams import ClickView, Customer +from source_google_ads.streams import ClickView, Customer, CustomerLabel # EXPIRED_PAGE_TOKEN exception will be raised when page token has expired. exception = GoogleAdsException( @@ -261,3 +261,21 @@ def test_parse_response(mocker, customers, config): ] assert output == expected_output + + +def test_read_records_unauthenticated(mocker, customers, config): + credentials = config["credentials"] + api = GoogleAds(credentials=credentials) + + mocker.patch.object(api, "parse_single_result", side_effect=Unauthenticated(message="Unauthenticated")) + + stream_config = dict( + api=api, + customers=customers, + ) + stream = CustomerLabel(**stream_config) + with pytest.raises(AirbyteTracedException) as exc_info: + list(stream.read_records(SyncMode.full_refresh, {"customer_id": "customer_id"})) + + assert exc_info.value.message == ("Authentication failed for the customer 'customer_id'. " + "Please try to Re-authenticate your credentials on set up Google Ads page.") diff --git a/docs/integrations/sources/google-ads.md b/docs/integrations/sources/google-ads.md index 5faef1d85790e..7019dd857ca16 100644 --- a/docs/integrations/sources/google-ads.md +++ b/docs/integrations/sources/google-ads.md @@ -278,6 +278,7 @@ Due to a limitation in the Google Ads API which does not allow getting performan | Version | Date | Pull Request | Subject | |:---------|:-----------|:---------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------| +| `3.0.2` | 2024-01-08 | [33494](https://github.com/airbytehq/airbyte/pull/33494) | Add handling for 401 error while parsing response. Add `metrics.cost_micros` field to Ad Group stream. | | `3.0.1` | 2023-12-26 | [33769](https://github.com/airbytehq/airbyte/pull/33769) | Run a read function in a separate thread to enforce a time limit for its execution | | `3.0.0` | 2023-12-07 | [33120](https://github.com/airbytehq/airbyte/pull/33120) | Upgrade API version to v15 | | `2.0.4` | 2023-11-10 | [32414](https://github.com/airbytehq/airbyte/pull/32414) | Add backoff strategy for read_records method | From 3cb1f1e97e42e585573f67808de5ca667ea4619f Mon Sep 17 00:00:00 2001 From: Augustin Date: Tue, 9 Jan 2024 17:51:14 +0100 Subject: [PATCH 025/574] airbyte-ci: fix nightly build binary (#34022) --- .github/workflows/connectors_nightly_build.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/connectors_nightly_build.yml b/.github/workflows/connectors_nightly_build.yml index ff419f64d46d0..d89ad8ad4a6f4 100644 --- a/.github/workflows/connectors_nightly_build.yml +++ b/.github/workflows/connectors_nightly_build.yml @@ -13,10 +13,6 @@ on: test-connectors-options: default: --concurrency=5 --support-level=certified required: true - airbyte-ci-binary-url: - description: "URL to airbyte-ci binary" - required: false - default: https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci run-name: "Test connectors: ${{ inputs.test-connectors-options || 'nightly build for Certified connectors' }} - on ${{ inputs.runs-on || 'ci-runner-connector-nightly-xlarge-dagger-0-9-5' }}" @@ -50,4 +46,3 @@ jobs: s3_build_cache_access_key_id: ${{ secrets.SELF_RUNNER_AWS_ACCESS_KEY_ID }} s3_build_cache_secret_key: ${{ secrets.SELF_RUNNER_AWS_SECRET_ACCESS_KEY }} subcommand: "connectors ${{ inputs.test-connectors-options || '--concurrency=8 --support-level=certified' }} test" - airbyte_ci_binary_url: ${{ github.event.inputs.airbyte-ci-binary-url }} From c09d5d335197a984792f17736f510b955ca0d581 Mon Sep 17 00:00:00 2001 From: Anatolii Yatsuk <35109939+tolik0@users.noreply.github.com> Date: Tue, 9 Jan 2024 19:27:50 +0200 Subject: [PATCH 026/574] =?UTF-8?q?=F0=9F=90=9B=20Source=20Google=20Ads:?= =?UTF-8?q?=20Fix=20custom=20queries=20(#33603)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../connectors/source-google-ads/main.py | 2 + .../source-google-ads/metadata.yaml | 2 +- .../connectors/source-google-ads/setup.py | 2 + .../source_google_ads/config_migrations.py | 131 ++++++++++++ .../source_google_ads/custom_query_stream.py | 5 +- .../source_google_ads/source.py | 76 ++++--- .../source_google_ads/spec.json | 2 +- .../source-google-ads/unit_tests/conftest.py | 2 +- .../unit_tests/test_config_migrations.py | 79 +++++++ .../unit_tests/test_custom_query.py | 6 +- .../unit_tests/test_errors.py | 10 +- .../custom_query/test_config.json | 18 ++ .../custom_query/test_new_config.json | 12 ++ .../unit_tests/test_source.py | 3 +- .../unit_tests/test_utils.py | 8 +- docs/integrations/sources/google-ads.md | 195 +++++++++--------- 16 files changed, 411 insertions(+), 142 deletions(-) create mode 100644 airbyte-integrations/connectors/source-google-ads/source_google_ads/config_migrations.py create mode 100644 airbyte-integrations/connectors/source-google-ads/unit_tests/test_config_migrations.py create mode 100644 airbyte-integrations/connectors/source-google-ads/unit_tests/test_migrations/custom_query/test_config.json create mode 100644 airbyte-integrations/connectors/source-google-ads/unit_tests/test_migrations/custom_query/test_new_config.json diff --git a/airbyte-integrations/connectors/source-google-ads/main.py b/airbyte-integrations/connectors/source-google-ads/main.py index 74d3215025261..d18603af20f37 100644 --- a/airbyte-integrations/connectors/source-google-ads/main.py +++ b/airbyte-integrations/connectors/source-google-ads/main.py @@ -7,7 +7,9 @@ from airbyte_cdk.entrypoint import launch from source_google_ads import SourceGoogleAds +from source_google_ads.config_migrations import MigrateCustomQuery if __name__ == "__main__": source = SourceGoogleAds() + MigrateCustomQuery.migrate(sys.argv[1:], source) launch(source, sys.argv[1:]) diff --git a/airbyte-integrations/connectors/source-google-ads/metadata.yaml b/airbyte-integrations/connectors/source-google-ads/metadata.yaml index af49b32503d0c..f77549067f968 100644 --- a/airbyte-integrations/connectors/source-google-ads/metadata.yaml +++ b/airbyte-integrations/connectors/source-google-ads/metadata.yaml @@ -11,7 +11,7 @@ data: connectorSubtype: api connectorType: source definitionId: 253487c0-2246-43ba-a21f-5116b20a2c50 - dockerImageTag: 3.0.2 + dockerImageTag: 3.1.0 dockerRepository: airbyte/source-google-ads documentationUrl: https://docs.airbyte.com/integrations/sources/google-ads githubIssueLabel: source-google-ads diff --git a/airbyte-integrations/connectors/source-google-ads/setup.py b/airbyte-integrations/connectors/source-google-ads/setup.py index 32874402f9fbb..d0694f67fa7ba 100644 --- a/airbyte-integrations/connectors/source-google-ads/setup.py +++ b/airbyte-integrations/connectors/source-google-ads/setup.py @@ -7,6 +7,8 @@ # pin protobuf==3.20.0 as other versions may cause problems on different architectures # (see https://github.com/airbytehq/airbyte/issues/13580) +# pendulum <3.0.0 is required to align with the CDK version, and should be updated once the next issue is resolved: +# https://github.com/airbytehq/airbyte/issues/33573 MAIN_REQUIREMENTS = ["airbyte-cdk>=0.51.3", "google-ads==22.1.0", "protobuf", "pendulum<3.0.0"] TEST_REQUIREMENTS = ["pytest~=6.1", "pytest-mock", "freezegun", "requests-mock"] diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/config_migrations.py b/airbyte-integrations/connectors/source-google-ads/source_google_ads/config_migrations.py new file mode 100644 index 0000000000000..be206ee13e623 --- /dev/null +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/config_migrations.py @@ -0,0 +1,131 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import logging +from typing import Any, List, Mapping + +from airbyte_cdk.config_observation import create_connector_config_control_message +from airbyte_cdk.entrypoint import AirbyteEntrypoint +from airbyte_cdk.models import FailureType +from airbyte_cdk.sources import Source +from airbyte_cdk.sources.message import InMemoryMessageRepository, MessageRepository +from airbyte_cdk.utils import AirbyteTracedException + +from .utils import GAQL + +logger = logging.getLogger("airbyte_logger") + +FULL_REFRESH_CUSTOM_TABLE = [ + "asset", + "asset_group_listing_group_filter", + "custom_audience", + "geo_target_constant", + "change_event", + "change_status", +] + + +class MigrateCustomQuery: + """ + This class stands for migrating the config at runtime. + This migration is backwards compatible with the previous version, as new property will be created. + When falling back to the previous source version connector will use old property `custom_queries`. + + Add `segments.date` for all queries where it was previously added by IncrementalCustomQuery class. + """ + + message_repository: MessageRepository = InMemoryMessageRepository() + + @classmethod + def should_migrate(cls, config: Mapping[str, Any]) -> bool: + """ + Determines if a configuration requires migration. + + Args: + - config (Mapping[str, Any]): The configuration data to check. + + Returns: + - True: If the configuration requires migration. + - False: Otherwise. + """ + return "custom_queries_array" not in config + + @classmethod + def update_custom_queries(cls, config: Mapping[str, Any], source: Source = None) -> Mapping[str, Any]: + """ + Update custom queries with segments.date field. + + Args: + - config (Mapping[str, Any]): The configuration from which the key should be removed. + - source (Source, optional): The data source. Defaults to None. + + Returns: + - Mapping[str, Any]: The configuration after removing the key. + """ + custom_queries = [] + for query in config.get("custom_queries", []): + new_query = query.copy() + try: + query_object = GAQL.parse(query["query"]) + except ValueError: + message = f"The custom GAQL query {query['table_name']} failed. Validate your GAQL query with the Google Ads query validator. https://developers.google.com/google-ads/api/fields/v13/query_validator" + raise AirbyteTracedException(message=message, failure_type=FailureType.config_error) + + if query_object.resource_name not in FULL_REFRESH_CUSTOM_TABLE and "segments.date" not in query_object.fields: + query_object = query_object.append_field("segments.date") + + new_query["query"] = str(query_object) + custom_queries.append(new_query) + + config["custom_queries_array"] = custom_queries + return config + + @classmethod + def modify_and_save(cls, config_path: str, source: Source, config: Mapping[str, Any]) -> Mapping[str, Any]: + """ + Modifies the configuration and then saves it back to the source. + + Args: + - config_path (str): The path where the configuration is stored. + - source (Source): The data source. + - config (Mapping[str, Any]): The current configuration. + + Returns: + - Mapping[str, Any]: The updated configuration. + """ + migrated_config = cls.update_custom_queries(config, source) + source.write_config(migrated_config, config_path) + return migrated_config + + @classmethod + def emit_control_message(cls, migrated_config: Mapping[str, Any]) -> None: + """ + Emits the control messages related to configuration migration. + + Args: + - migrated_config (Mapping[str, Any]): The migrated configuration. + """ + cls.message_repository.emit_message(create_connector_config_control_message(migrated_config)) + for message in cls.message_repository._message_queue: + print(message.json(exclude_unset=True)) + + @classmethod + def migrate(cls, args: List[str], source: Source) -> None: + """ + Orchestrates the configuration migration process. + + It first checks if the `--config` argument is provided, and if so, + determines whether migration is needed, and then performs the migration + if required. + + Args: + - args (List[str]): List of command-line arguments. + - source (Source): The data source. + """ + config_path = AirbyteEntrypoint(source).extract_config(args) + if config_path: + config = source.read_config(config_path) + if cls.should_migrate(config): + cls.emit_control_message(cls.modify_and_save(config_path, source, config)) diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/custom_query_stream.py b/airbyte-integrations/connectors/source-google-ads/source_google_ads/custom_query_stream.py index a5ab6cf6d0ba6..4a3ac096cfdd6 100644 --- a/airbyte-integrations/connectors/source-google-ads/source_google_ads/custom_query_stream.py +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/custom_query_stream.py @@ -9,6 +9,8 @@ from .streams import GoogleAdsStream, IncrementalGoogleAdsStream from .utils import GAQL +DATE_TYPES = ("segments.date", "segments.month", "segments.quarter", "segments.week") + class CustomQueryMixin: def __init__(self, config, **kwargs): @@ -67,7 +69,8 @@ def get_json_schema(self) -> Dict[str, Any]: google_data_type = node.data_type.name field_value = {"type": [google_datatype_mapping.get(google_data_type, "string"), "null"]} - if google_data_type == "DATE": + # Google Ads doesn't differentiate between DATE and DATETIME, so we need to manually check for fields with known type + if google_data_type == "DATE" and field in DATE_TYPES: field_value["format"] = "date" if google_data_type == "ENUM": diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/source.py b/airbyte-integrations/connectors/source-google-ads/source_google_ads/source.py index e34ef6fb5555a..1e23f20fe5129 100644 --- a/airbyte-integrations/connectors/source-google-ads/source_google_ads/source.py +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/source.py @@ -47,15 +47,6 @@ ) from .utils import GAQL -FULL_REFRESH_CUSTOM_TABLE = [ - "asset", - "asset_group_listing_group_filter", - "custom_audience", - "geo_target_constant", - "change_event", - "change_status", -] - class SourceGoogleAds(AbstractSource): # Skip exceptions on missing streams @@ -65,7 +56,7 @@ class SourceGoogleAds(AbstractSource): def _validate_and_transform(config: Mapping[str, Any]): if config.get("end_date") == "": config.pop("end_date") - for query in config.get("custom_queries", []): + for query in config.get("custom_queries_array", []): try: query["query"] = GAQL.parse(query["query"]) except ValueError: @@ -121,6 +112,37 @@ def is_metrics_in_custom_query(query: GAQL) -> bool: return True return False + @staticmethod + def is_custom_query_incremental(query: GAQL) -> bool: + time_segment_in_select, time_segment_in_where = ["segments.date" in clause for clause in [query.fields, query.where]] + return time_segment_in_select and not time_segment_in_where + + def create_custom_query_stream( + self, + google_api: GoogleAds, + single_query_config: Mapping[str, Any], + customers: List[CustomerModel], + non_manager_accounts: List[CustomerModel], + incremental_config: Mapping[str, Any], + non_manager_incremental_config: Mapping[str, Any], + ): + query = single_query_config["query"] + is_incremental = self.is_custom_query_incremental(query) + is_non_manager = self.is_metrics_in_custom_query(query) + + if is_non_manager: + # Skip query with metrics if there are no non-manager accounts + if not non_manager_accounts: + return + + customers = non_manager_accounts + incremental_config = non_manager_incremental_config + + if is_incremental: + return IncrementalCustomQuery(config=single_query_config, **incremental_config) + else: + return CustomQuery(config=single_query_config, api=google_api, customers=customers) + def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> Tuple[bool, any]: config = self._validate_and_transform(config) @@ -131,18 +153,20 @@ def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> customers = CustomerModel.from_accounts(accounts) # Check custom query request validity by sending metric request with non-existent time window for customer in customers: - for query in config.get("custom_queries", []): + for query in config.get("custom_queries_array", []): query = query["query"] if customer.is_manager_account and self.is_metrics_in_custom_query(query): logger.warning( f"Metrics are not available for manager account {customer.id}. " - f"Please remove metrics fields in your custom query: {query}." + f'Skipping the custom query: "{query}" for manager account.' ) - if query.resource_name not in FULL_REFRESH_CUSTOM_TABLE: - if IncrementalCustomQuery.cursor_field in query.fields: - message = f"Custom query should not contain {IncrementalCustomQuery.cursor_field}" - raise AirbyteTracedException(message=message, internal_message=message, failure_type=FailureType.config_error) + continue + + # Add segments.date to where clause of incremental custom queries if they are not present. + # The same will be done during read, but with start and end date from config + if self.is_custom_query_incremental(query): query = IncrementalCustomQuery.insert_segments_date_expr(query, "1980-01-01", "1980-01-01") + query = query.set_limit(1) response = google_api.send_request(str(query), customer_id=customer.id) # iterate over the response otherwise exceptions will not be raised! @@ -194,17 +218,11 @@ def streams(self, config: Mapping[str, Any]) -> List[Stream]: KeywordView(**non_manager_incremental_config), ] ) - for single_query_config in config.get("custom_queries", []): - query = single_query_config["query"] - if self.is_metrics_in_custom_query(query): - if non_manager_accounts: - if query.resource_name in FULL_REFRESH_CUSTOM_TABLE: - streams.append(CustomQuery(config=single_query_config, api=google_api, customers=non_manager_accounts)) - else: - streams.append(IncrementalCustomQuery(config=single_query_config, **non_manager_incremental_config)) - continue - if query.resource_name in FULL_REFRESH_CUSTOM_TABLE: - streams.append(CustomQuery(config=single_query_config, api=google_api, customers=customers)) - else: - streams.append(IncrementalCustomQuery(config=single_query_config, **incremental_config)) + + for single_query_config in config.get("custom_queries_array", []): + query_stream = self.create_custom_query_stream( + google_api, single_query_config, customers, non_manager_accounts, incremental_config, non_manager_incremental_config + ) + if query_stream: + streams.append(query_stream) return streams diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/spec.json b/airbyte-integrations/connectors/source-google-ads/source_google_ads/spec.json index 8a15796874fcf..b875b6d419d90 100644 --- a/airbyte-integrations/connectors/source-google-ads/source_google_ads/spec.json +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/spec.json @@ -84,7 +84,7 @@ "order": 6, "format": "date" }, - "custom_queries": { + "custom_queries_array": { "type": "array", "title": "Custom GAQL Queries", "description": "", diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/conftest.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/conftest.py index cc6867bd105a2..845780e9f3838 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/conftest.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/conftest.py @@ -19,7 +19,7 @@ def test_config(): "customer_id": "123", "start_date": "2021-01-01", "conversion_window_days": 14, - "custom_queries": [ + "custom_queries_array": [ { "query": "SELECT campaign.accessible_bidding_strategy, segments.ad_destination_type, campaign.start_date, campaign.end_date FROM campaign", "primary_key": None, diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_config_migrations.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_config_migrations.py new file mode 100644 index 0000000000000..4ca91bc77892c --- /dev/null +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_config_migrations.py @@ -0,0 +1,79 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import json +from typing import Any, Mapping + +from airbyte_cdk.models import OrchestratorType, Type +from airbyte_cdk.sources import Source +from source_google_ads.config_migrations import MigrateCustomQuery +from source_google_ads.source import SourceGoogleAds + +# BASE ARGS +CMD = "check" +TEST_CONFIG_PATH = "unit_tests/test_migrations/custom_query/test_config.json" +NEW_TEST_CONFIG_PATH = "unit_tests/test_migrations/custom_query/test_new_config.json" +SOURCE_INPUT_ARGS = [CMD, "--config", TEST_CONFIG_PATH] +SOURCE: Source = SourceGoogleAds() + + +# HELPERS +def load_config(config_path: str = TEST_CONFIG_PATH) -> Mapping[str, Any]: + with open(config_path, "r") as config: + return json.load(config) + + +def revert_migration(config_path: str = TEST_CONFIG_PATH) -> None: + with open(config_path, "r") as test_config: + config = json.load(test_config) + config.pop("custom_queries_array") + with open(config_path, "w") as updated_config: + config = json.dumps(config) + updated_config.write(config) + + +def test_migrate_config(): + migration_instance = MigrateCustomQuery() + original_config = load_config() + original_config_queries = original_config["custom_queries"].copy() + # migrate the test_config + migration_instance.migrate(SOURCE_INPUT_ARGS, SOURCE) + # load the updated config + test_migrated_config = load_config() + # check migrated property + assert "custom_queries_array" in test_migrated_config + assert "segments.date" in test_migrated_config["custom_queries_array"][0]["query"] + # check the old property is in place + assert "custom_queries" in test_migrated_config + assert test_migrated_config["custom_queries"] == original_config_queries + assert "segments.date" not in test_migrated_config["custom_queries"][0]["query"] + # check the migration should be skipped, once already done + assert not migration_instance.should_migrate(test_migrated_config) + # load the old custom reports VS migrated + new_config_queries = test_migrated_config["custom_queries_array"].copy() + new_config_queries[0]["query"] = new_config_queries[0]["query"].replace(", segments.date", "") + print(f"{original_config=} \n {test_migrated_config=}") + assert original_config["custom_queries"] == new_config_queries + # test CONTROL MESSAGE was emitted + control_msg = migration_instance.message_repository._message_queue[0] + assert control_msg.type == Type.CONTROL + assert control_msg.control.type == OrchestratorType.CONNECTOR_CONFIG + # revert the test_config to the starting point + revert_migration() + + +def test_config_is_reverted(): + # check the test_config state, it has to be the same as before tests + test_config = load_config() + # check the config no longer has the migarted property + assert "custom_queries_array" not in test_config + # check the old property is still there + assert "custom_queries" in test_config + + +def test_should_not_migrate_new_config(): + new_config = load_config(NEW_TEST_CONFIG_PATH) + migration_instance = MigrateCustomQuery() + assert not migration_instance.should_migrate(new_config) diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_custom_query.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_custom_query.py index 0201db4101133..862324d3c2375 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_custom_query.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_custom_query.py @@ -36,9 +36,10 @@ def test_get_json_schema(): "d": Obj(data_type=Obj(name="MESSAGE"), is_repeated=True), "e": Obj(data_type=Obj(name="STRING"), is_repeated=False), "f": Obj(data_type=Obj(name="DATE"), is_repeated=False), + "segments.month": Obj(data_type=Obj(name="DATE"), is_repeated=False), } ) - instance = CustomQueryMixin(config={"query": Obj(fields=["a", "b", "c", "d", "e", "f"])}) + instance = CustomQueryMixin(config={"query": Obj(fields=["a", "b", "c", "d", "e", "f", "segments.month"])}) instance.cursor_field = None instance.google_ads_client = Obj(get_fields_metadata=query_object) schema = instance.get_json_schema() @@ -53,6 +54,7 @@ def test_get_json_schema(): "c": {"type": ["string", "null"]}, "d": {"type": ["null", "array"], "items": {"type": ["string", "null"]}}, "e": {"type": ["string", "null"]}, - "f": {"type": ["string", "null"], "format": "date"}, + "f": {"type": ["string", "null"]}, + "segments.month": {"type": ["string", "null"], "format": "date"}, }, } diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_errors.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_errors.py index 85526e90ed17e..ef599d97cbf8f 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_errors.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_errors.py @@ -99,9 +99,9 @@ def test_read_record_error_handling(mocker, config, customers, cls, raise_expect True, None, ( - "Metrics are not available for manager account 8765. Please remove metrics " - "fields in your custom query: SELECT campaign.accessible_bidding_strategy, " - "metrics.clicks FROM campaigns." + "Metrics are not available for manager account 8765. " + 'Skipping the custom query: "SELECT campaign.accessible_bidding_strategy, ' + 'metrics.clicks FROM campaigns" for manager account.' ), ), ( @@ -123,13 +123,13 @@ def test_read_record_error_handling(mocker, config, customers, cls, raise_expect "table_name": "unhappytable", }, False, - "Custom query should not contain segments.date", + None, None, ), ], ) def test_check_custom_queries(mocker, config, custom_query, is_manager_account, error_message, warning): - config["custom_queries"] = [custom_query] + config["custom_queries_array"] = [custom_query] mocker.patch( "source_google_ads.source.SourceGoogleAds.get_account_info", Mock(return_value=[[{"customer.manager": is_manager_account, "customer.time_zone": "Europe/Berlin", "customer.id": "8765"}]]), diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_migrations/custom_query/test_config.json b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_migrations/custom_query/test_config.json new file mode 100644 index 0000000000000..2ce005d03ec09 --- /dev/null +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_migrations/custom_query/test_config.json @@ -0,0 +1,18 @@ +{ + "credentials": { + "developer_token": "developer_token", + "client_id": "client_id", + "client_secret": "client_secret", + "refresh_token": "refresh_token" + }, + "customer_id": "1234567890", + "start_date": "2023-09-04", + "conversion_window_days": 14, + "custom_queries": [ + { + "query": "SELECT campaign.name, metrics.clicks FROM campaign", + "primary_key": null, + "table_name": "test_query" + } + ] +} diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_migrations/custom_query/test_new_config.json b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_migrations/custom_query/test_new_config.json new file mode 100644 index 0000000000000..7d8097055f09c --- /dev/null +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_migrations/custom_query/test_new_config.json @@ -0,0 +1,12 @@ +{ + "credentials": { + "developer_token": "developer_token", + "client_id": "client_id", + "client_secret": "client_secret", + "refresh_token": "refresh_token" + }, + "customer_id": "1234567890", + "start_date": "2023-09-04", + "conversion_window_days": 14, + "custom_queries_array": [] +} diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_source.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_source.py index bd57bd5520dbb..f39fa1e3f95df 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_source.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_source.py @@ -117,6 +117,7 @@ def test_streams_count(config, mock_account_info): source = SourceGoogleAds() streams = source.streams(config) expected_streams_number = 30 + print(f"{config=} \n{streams=}") assert len(streams) == expected_streams_number @@ -391,7 +392,7 @@ def test_check_connection_should_pass_when_config_valid(mocker): "customer_id": "fake_customer_id", "start_date": "2022-01-01", "conversion_window_days": 14, - "custom_queries": [ + "custom_queries_array": [ { "query": "SELECT campaign.accessible_bidding_strategy, segments.ad_destination_type, campaign.start_date, campaign.end_date FROM campaign", "primary_key": None, diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_utils.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_utils.py index fd43f8111020b..7c42cd50360e0 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_utils.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_utils.py @@ -84,10 +84,10 @@ def test_parse_GAQL_ok(): @pytest.mark.parametrize( "config", [ - {"custom_queries": [{"query": "SELECT field1, field2 FROM x_Table2", "table_name": "test_table"}]}, - {"custom_queries": [{"query": "SELECT field1, field2 FROM x_Table WHERE ", "table_name": "test_table"}]}, - {"custom_queries": [{"query": "SELECT field1, , field2 FROM table", "table_name": "test_table"}]}, - {"custom_queries": [{"query": "SELECT fie ld1, field2 FROM table", "table_name": "test_table"}]}, + {"custom_queries_array": [{"query": "SELECT field1, field2 FROM x_Table2", "table_name": "test_table"}]}, + {"custom_queries_array": [{"query": "SELECT field1, field2 FROM x_Table WHERE ", "table_name": "test_table"}]}, + {"custom_queries_array": [{"query": "SELECT field1, , field2 FROM table", "table_name": "test_table"}]}, + {"custom_queries_array": [{"query": "SELECT fie ld1, field2 FROM table", "table_name": "test_table"}]}, ], ) def test_parse_GAQL_fail(config): diff --git a/docs/integrations/sources/google-ads.md b/docs/integrations/sources/google-ads.md index 7019dd857ca16..a56f046a29fd5 100644 --- a/docs/integrations/sources/google-ads.md +++ b/docs/integrations/sources/google-ads.md @@ -230,7 +230,7 @@ SELECT FROM ad_group ``` -Note the segments.date is automatically added to the output, and does not need to be specified in the custom query. All custom reports will by synced by day. +Note that `segments.date` is automatically added to the `WHERE` clause if it is included in the `SELECT` clause. Custom reports including `segments.date` in the `SELECT` clause will be synced by day. Each custom query in the input configuration must work for all the customer account IDs. Otherwise, the customer ID will be skipped for every query that fails the validation test. For example, if your query contains metrics fields in the select clause, it will not be executed against manager accounts. @@ -276,99 +276,100 @@ Due to a limitation in the Google Ads API which does not allow getting performan ## Changelog -| Version | Date | Pull Request | Subject | -|:---------|:-----------|:---------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------| -| `3.0.2` | 2024-01-08 | [33494](https://github.com/airbytehq/airbyte/pull/33494) | Add handling for 401 error while parsing response. Add `metrics.cost_micros` field to Ad Group stream. | -| `3.0.1` | 2023-12-26 | [33769](https://github.com/airbytehq/airbyte/pull/33769) | Run a read function in a separate thread to enforce a time limit for its execution | -| `3.0.0` | 2023-12-07 | [33120](https://github.com/airbytehq/airbyte/pull/33120) | Upgrade API version to v15 | -| `2.0.4` | 2023-11-10 | [32414](https://github.com/airbytehq/airbyte/pull/32414) | Add backoff strategy for read_records method | -| `2.0.3` | 2023-11-02 | [32102](https://github.com/airbytehq/airbyte/pull/32102) | Fix incremental events streams | -| `2.0.2` | 2023-10-31 | [32001](https://github.com/airbytehq/airbyte/pull/32001) | Added handling (retry) for `InternalServerError` while reading the streams | -| `2.0.1` | 2023-10-27 | [31908](https://github.com/airbytehq/airbyte/pull/31908) | Base image migration: remove Dockerfile and use the python-connector-base image | -| `2.0.0` | 2023-10-04 | [31048](https://github.com/airbytehq/airbyte/pull/31048) | Fix schem default streams, change names of streams. | -| `1.0.0` | 2023-09-28 | [30705](https://github.com/airbytehq/airbyte/pull/30705) | Fix schemas for custom queries | -| `0.11.1` | 2023-09-26 | [30758](https://github.com/airbytehq/airbyte/pull/30758) | Exception should not be raises if a stream is not found | -| `0.11.0` | 2023-09-23 | [30704](https://github.com/airbytehq/airbyte/pull/30704) | Update error handling | -| `0.10.0` | 2023-09-19 | [30091](https://github.com/airbytehq/airbyte/pull/30091) | Fix schemas for correct primary and foreign keys | -| `0.9.0` | 2023-09-14 | [28970](https://github.com/airbytehq/airbyte/pull/28970) | Add incremental deletes for Campaign and Ad Group Criterion streams | -| `0.8.1` | 2023-09-13 | [30376](https://github.com/airbytehq/airbyte/pull/30376) | Revert pagination changes from 0.8.0 | -| `0.8.0` | 2023-09-01 | [30071](https://github.com/airbytehq/airbyte/pull/30071) | Delete start_date from required parameters and fix pagination | -| `0.7.4` | 2023-07-28 | [28832](https://github.com/airbytehq/airbyte/pull/28832) | Update field descriptions | -| `0.7.3` | 2023-07-24 | [28510](https://github.com/airbytehq/airbyte/pull/28510) | Set dates with client's timezone | -| `0.7.2` | 2023-07-20 | [28535](https://github.com/airbytehq/airbyte/pull/28535) | UI improvement: Make the query field in custom reports a multi-line string field | -| `0.7.1` | 2023-07-17 | [28365](https://github.com/airbytehq/airbyte/pull/28365) | 0.3.1 and 0.3.2 follow up: make today the end date, not yesterday | -| `0.7.0` | 2023-07-12 | [28246](https://github.com/airbytehq/airbyte/pull/28246) | Add new streams: labels, criterions, biddig strategies | -| `0.6.1` | 2023-07-12 | [28230](https://github.com/airbytehq/airbyte/pull/28230) | Reduce amount of logs produced by the connector while working with big amount of data | -| `0.6.0` | 2023-07-10 | [28078](https://github.com/airbytehq/airbyte/pull/28078) | Add new stream `Campaign Budget` | -| `0.5.0` | 2023-07-07 | [28042](https://github.com/airbytehq/airbyte/pull/28042) | Add metrics & segment to `Campaigns` stream | -| `0.4.3` | 2023-07-05 | [27959](https://github.com/airbytehq/airbyte/pull/27959) | Add `audience` and `user_interest` streams | -| `0.3.3` | 2023-07-03 | [27913](https://github.com/airbytehq/airbyte/pull/27913) | Improve Google Ads exception handling (wrong customer ID) | -| `0.3.2` | 2023-06-29 | [27835](https://github.com/airbytehq/airbyte/pull/27835) | Fix bug introduced in 0.3.1: update query template | -| `0.3.1` | 2023-06-26 | [27711](https://github.com/airbytehq/airbyte/pull/27711) | Refactor date slicing; make start date inclusive | -| `0.3.0` | 2023-06-26 | [27738](https://github.com/airbytehq/airbyte/pull/27738) | License Update: Elv2 | -| `0.2.24` | 2023-06-06 | [27608](https://github.com/airbytehq/airbyte/pull/27608) | Improve Google Ads exception handling | -| `0.2.23` | 2023-06-06 | [26905](https://github.com/airbytehq/airbyte/pull/26905) | Replace deprecated `authSpecification` in the connector specification with `advancedAuth` | -| `0.2.22` | 2023-06-02 | [26948](https://github.com/airbytehq/airbyte/pull/26948) | Refactor error messages; add `pattern_descriptor` for fields in spec | -| `0.2.21` | 2023-05-30 | [25314](https://github.com/airbytehq/airbyte/pull/25314) | Add full refresh custom table `asset_group_listing_group_filter` | -| `0.2.20` | 2023-05-30 | [25624](https://github.com/airbytehq/airbyte/pull/25624) | Add `asset` Resource to full refresh custom tables (GAQL Queries) | -| `0.2.19` | 2023-05-15 | [26209](https://github.com/airbytehq/airbyte/pull/26209) | Handle Token Refresh errors as `config_error` | -| `0.2.18` | 2023-05-15 | [25947](https://github.com/airbytehq/airbyte/pull/25947) | Improve GAQL parser error message if multiple resources provided | -| `0.2.17` | 2023-05-11 | [25987](https://github.com/airbytehq/airbyte/pull/25987) | Categorized Config Errors Accurately | -| `0.2.16` | 2023-05-10 | [25965](https://github.com/airbytehq/airbyte/pull/25965) | Fix Airbyte date-time data-types | -| `0.2.14` | 2023-03-21 | [24945](https://github.com/airbytehq/airbyte/pull/24945) | For custom google query fixed schema type for "data_type: ENUM" and "is_repeated: true" to array of strings | -| `0.2.13` | 2023-03-21 | [24338](https://github.com/airbytehq/airbyte/pull/24338) | Migrate to v13 | -| `0.2.12` | 2023-03-17 | [22985](https://github.com/airbytehq/airbyte/pull/22985) | Specified date formatting in specification | -| `0.2.11` | 2023-03-13 | [23999](https://github.com/airbytehq/airbyte/pull/23999) | Fix incremental sync for Campaigns stream | -| `0.2.10` | 2023-02-11 | [22703](https://github.com/airbytehq/airbyte/pull/22703) | Add support for custom full_refresh streams | -| `0.2.9` | 2023-01-23 | [21705](https://github.com/airbytehq/airbyte/pull/21705) | Fix multibyte issue; Bump google-ads package to 19.0.0 | -| `0.2.8` | 2023-01-18 | [21517](https://github.com/airbytehq/airbyte/pull/21517) | Write fewer logs | -| `0.2.7` | 2023-01-10 | [20755](https://github.com/airbytehq/airbyte/pull/20755) | Add more logs to debug stuck syncs | -| `0.2.6` | 2022-12-22 | [20855](https://github.com/airbytehq/airbyte/pull/20855) | Retry 429 and 5xx errors | -| `0.2.5` | 2022-11-22 | [19700](https://github.com/airbytehq/airbyte/pull/19700) | Fix schema for `campaigns` stream | -| `0.2.4` | 2022-11-09 | [19208](https://github.com/airbytehq/airbyte/pull/19208) | Add TypeTransofrmer to Campaings stream to force proper type casting | -| `0.2.3` | 2022-10-17 | [18069](https://github.com/airbytehq/airbyte/pull/18069) | Add `segments.hour`, `metrics.ctr`, `metrics.conversions` and `metrics.conversions_values` fields to `campaigns` report stream | -| `0.2.2` | 2022-10-21 | [17412](https://github.com/airbytehq/airbyte/pull/17412) | Release with CDK >= 0.2.2 | -| `0.2.1` | 2022-09-29 | [17412](https://github.com/airbytehq/airbyte/pull/17412) | Always use latest CDK version | -| `0.2.0` | 2022-08-23 | [15858](https://github.com/airbytehq/airbyte/pull/15858) | Mark the `query` and `table_name` fields in `custom_queries` as required | -| `0.1.44` | 2022-07-27 | [15084](https://github.com/airbytehq/airbyte/pull/15084) | Fix data type `ad_group_criterion.topic.path` in `display_topics_performance_report` and shifted `campaigns` to non-managers streams | -| `0.1.43` | 2022-07-12 | [14614](https://github.com/airbytehq/airbyte/pull/14614) | Update API version to `v11`, update `google-ads` to 17.0.0 | -| `0.1.42` | 2022-06-08 | [13624](https://github.com/airbytehq/airbyte/pull/13624) | Update `google-ads` to 15.1.1, pin `protobuf==3.20.0` to work on MacOS M1 machines (AMD) | -| `0.1.41` | 2022-06-08 | [13618](https://github.com/airbytehq/airbyte/pull/13618) | Add missing dependency | -| `0.1.40` | 2022-06-02 | [13423](https://github.com/airbytehq/airbyte/pull/13423) | Fix the missing data [issue](https://github.com/airbytehq/airbyte/issues/12999) | -| `0.1.39` | 2022-05-18 | [12914](https://github.com/airbytehq/airbyte/pull/12914) | Fix GAQL query validation and log auth errors instead of failing the sync | -| `0.1.38` | 2022-05-12 | [12807](https://github.com/airbytehq/airbyte/pull/12807) | Documentation updates | -| `0.1.37` | 2022-05-06 | [12651](https://github.com/airbytehq/airbyte/pull/12651) | Improve integration and unit tests | -| `0.1.36` | 2022-04-19 | [12158](https://github.com/airbytehq/airbyte/pull/12158) | Fix `*_labels` streams data type | -| `0.1.35` | 2022-04-18 | [9310](https://github.com/airbytehq/airbyte/pull/9310) | Add new fields to reports | -| `0.1.34` | 2022-03-29 | [11602](https://github.com/airbytehq/airbyte/pull/11602) | Add budget amount to campaigns stream. | -| `0.1.33` | 2022-03-29 | [11513](https://github.com/airbytehq/airbyte/pull/11513) | When `end_date` is configured in the future, use today's date instead. | -| `0.1.32` | 2022-03-24 | [11371](https://github.com/airbytehq/airbyte/pull/11371) | Improve how connection check returns error messages | -| `0.1.31` | 2022-03-23 | [11301](https://github.com/airbytehq/airbyte/pull/11301) | Update docs and spec to clarify usage | -| `0.1.30` | 2022-03-23 | [11221](https://github.com/airbytehq/airbyte/pull/11221) | Add `*_labels` streams to fetch the label text rather than their IDs | -| `0.1.29` | 2022-03-22 | [10919](https://github.com/airbytehq/airbyte/pull/10919) | Fix user location report schema and add to acceptance tests | -| `0.1.28` | 2022-02-25 | [10372](https://github.com/airbytehq/airbyte/pull/10372) | Add network fields to click view stream | -| `0.1.27` | 2022-02-16 | [10315](https://github.com/airbytehq/airbyte/pull/10315) | Make `ad_group_ads` and other streams support incremental sync. | -| `0.1.26` | 2022-02-11 | [10150](https://github.com/airbytehq/airbyte/pull/10150) | Add support for multiple customer IDs. | -| `0.1.25` | 2022-02-04 | [9812](https://github.com/airbytehq/airbyte/pull/9812) | Handle `EXPIRED_PAGE_TOKEN` exception and retry with updated state. | -| `0.1.24` | 2022-02-04 | [9996](https://github.com/airbytehq/airbyte/pull/9996) | Use Google Ads API version V9. | -| `0.1.23` | 2022-01-25 | [8669](https://github.com/airbytehq/airbyte/pull/8669) | Add end date parameter in spec. | -| `0.1.22` | 2022-01-24 | [9608](https://github.com/airbytehq/airbyte/pull/9608) | Reduce stream slice date range. | -| `0.1.21` | 2021-12-28 | [9149](https://github.com/airbytehq/airbyte/pull/9149) | Update title and description | -| `0.1.20` | 2021-12-22 | [9071](https://github.com/airbytehq/airbyte/pull/9071) | Fix: Keyword schema enum | -| `0.1.19` | 2021-12-14 | [8431](https://github.com/airbytehq/airbyte/pull/8431) | Add new streams: Geographic and Keyword | -| `0.1.18` | 2021-12-09 | [8225](https://github.com/airbytehq/airbyte/pull/8225) | Include time_zone to sync. Remove streams for manager account. | -| `0.1.16` | 2021-11-22 | [8178](https://github.com/airbytehq/airbyte/pull/8178) | Clarify setup fields | -| `0.1.15` | 2021-10-07 | [6684](https://github.com/airbytehq/airbyte/pull/6684) | Add new stream `click_view` | -| `0.1.14` | 2021-10-01 | [6565](https://github.com/airbytehq/airbyte/pull/6565) | Fix OAuth Spec File | -| `0.1.13` | 2021-09-27 | [6458](https://github.com/airbytehq/airbyte/pull/6458) | Update OAuth Spec File | -| `0.1.11` | 2021-09-22 | [6373](https://github.com/airbytehq/airbyte/pull/6373) | Fix inconsistent segments.date field type across all streams | -| `0.1.10` | 2021-09-13 | [6022](https://github.com/airbytehq/airbyte/pull/6022) | Annotate Oauth2 flow initialization parameters in connector spec | -| `0.1.9` | 2021-09-07 | [5302](https://github.com/airbytehq/airbyte/pull/5302) | Add custom query stream support | -| `0.1.8` | 2021-08-03 | [5509](https://github.com/airbytehq/airbyte/pull/5509) | Allow additionalProperties in spec.json | -| `0.1.7` | 2021-08-03 | [5422](https://github.com/airbytehq/airbyte/pull/5422) | Correct query to not skip dates | -| `0.1.6` | 2021-08-03 | [5423](https://github.com/airbytehq/airbyte/pull/5423) | Added new stream UserLocationReport | -| `0.1.5` | 2021-08-03 | [5159](https://github.com/airbytehq/airbyte/pull/5159) | Add field `login_customer_id` to spec | -| `0.1.4` | 2021-07-28 | [4962](https://github.com/airbytehq/airbyte/pull/4962) | Support new Report streams | -| `0.1.3` | 2021-07-23 | [4788](https://github.com/airbytehq/airbyte/pull/4788) | Support main streams, fix bug with exception `DATE_RANGE_TOO_NARROW` for incremental streams | -| `0.1.2` | 2021-07-06 | [4539](https://github.com/airbytehq/airbyte/pull/4539) | Add `AIRBYTE_ENTRYPOINT` for Kubernetes support | -| `0.1.1` | 2021-06-23 | [4288](https://github.com/airbytehq/airbyte/pull/4288) | Fix `Bugfix: Correctly declare required parameters` | +| Version | Date | Pull Request | Subject | +|:---------|:-----------|:---------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------| +| `3.1.0` | 2024-01-09 | [33603](https://github.com/airbytehq/airbyte/pull/33603) | Fix two issues in the custom queries: automatic addition of `segments.date` in the query; incorrect field type for `DATE` fields. | +| `3.0.2` | 2024-01-08 | [33494](https://github.com/airbytehq/airbyte/pull/33494) | Add handling for 401 error while parsing response. Add `metrics.cost_micros` field to Ad Group stream. | +| `3.0.1` | 2023-12-26 | [33769](https://github.com/airbytehq/airbyte/pull/33769) | Run a read function in a separate thread to enforce a time limit for its execution | +| `3.0.0` | 2023-12-07 | [33120](https://github.com/airbytehq/airbyte/pull/33120) | Upgrade API version to v15 | +| `2.0.4` | 2023-11-10 | [32414](https://github.com/airbytehq/airbyte/pull/32414) | Add backoff strategy for read_records method | +| `2.0.3` | 2023-11-02 | [32102](https://github.com/airbytehq/airbyte/pull/32102) | Fix incremental events streams | +| `2.0.2` | 2023-10-31 | [32001](https://github.com/airbytehq/airbyte/pull/32001) | Added handling (retry) for `InternalServerError` while reading the streams | +| `2.0.1` | 2023-10-27 | [31908](https://github.com/airbytehq/airbyte/pull/31908) | Base image migration: remove Dockerfile and use the python-connector-base image | +| `2.0.0` | 2023-10-04 | [31048](https://github.com/airbytehq/airbyte/pull/31048) | Fix schem default streams, change names of streams. | +| `1.0.0` | 2023-09-28 | [30705](https://github.com/airbytehq/airbyte/pull/30705) | Fix schemas for custom queries | +| `0.11.1` | 2023-09-26 | [30758](https://github.com/airbytehq/airbyte/pull/30758) | Exception should not be raises if a stream is not found | +| `0.11.0` | 2023-09-23 | [30704](https://github.com/airbytehq/airbyte/pull/30704) | Update error handling | +| `0.10.0` | 2023-09-19 | [30091](https://github.com/airbytehq/airbyte/pull/30091) | Fix schemas for correct primary and foreign keys | +| `0.9.0` | 2023-09-14 | [28970](https://github.com/airbytehq/airbyte/pull/28970) | Add incremental deletes for Campaign and Ad Group Criterion streams | +| `0.8.1` | 2023-09-13 | [30376](https://github.com/airbytehq/airbyte/pull/30376) | Revert pagination changes from 0.8.0 | +| `0.8.0` | 2023-09-01 | [30071](https://github.com/airbytehq/airbyte/pull/30071) | Delete start_date from required parameters and fix pagination | +| `0.7.4` | 2023-07-28 | [28832](https://github.com/airbytehq/airbyte/pull/28832) | Update field descriptions | +| `0.7.3` | 2023-07-24 | [28510](https://github.com/airbytehq/airbyte/pull/28510) | Set dates with client's timezone | +| `0.7.2` | 2023-07-20 | [28535](https://github.com/airbytehq/airbyte/pull/28535) | UI improvement: Make the query field in custom reports a multi-line string field | +| `0.7.1` | 2023-07-17 | [28365](https://github.com/airbytehq/airbyte/pull/28365) | 0.3.1 and 0.3.2 follow up: make today the end date, not yesterday | +| `0.7.0` | 2023-07-12 | [28246](https://github.com/airbytehq/airbyte/pull/28246) | Add new streams: labels, criterions, biddig strategies | +| `0.6.1` | 2023-07-12 | [28230](https://github.com/airbytehq/airbyte/pull/28230) | Reduce amount of logs produced by the connector while working with big amount of data | +| `0.6.0` | 2023-07-10 | [28078](https://github.com/airbytehq/airbyte/pull/28078) | Add new stream `Campaign Budget` | +| `0.5.0` | 2023-07-07 | [28042](https://github.com/airbytehq/airbyte/pull/28042) | Add metrics & segment to `Campaigns` stream | +| `0.4.3` | 2023-07-05 | [27959](https://github.com/airbytehq/airbyte/pull/27959) | Add `audience` and `user_interest` streams | +| `0.3.3` | 2023-07-03 | [27913](https://github.com/airbytehq/airbyte/pull/27913) | Improve Google Ads exception handling (wrong customer ID) | +| `0.3.2` | 2023-06-29 | [27835](https://github.com/airbytehq/airbyte/pull/27835) | Fix bug introduced in 0.3.1: update query template | +| `0.3.1` | 2023-06-26 | [27711](https://github.com/airbytehq/airbyte/pull/27711) | Refactor date slicing; make start date inclusive | +| `0.3.0` | 2023-06-26 | [27738](https://github.com/airbytehq/airbyte/pull/27738) | License Update: Elv2 | +| `0.2.24` | 2023-06-06 | [27608](https://github.com/airbytehq/airbyte/pull/27608) | Improve Google Ads exception handling | +| `0.2.23` | 2023-06-06 | [26905](https://github.com/airbytehq/airbyte/pull/26905) | Replace deprecated `authSpecification` in the connector specification with `advancedAuth` | +| `0.2.22` | 2023-06-02 | [26948](https://github.com/airbytehq/airbyte/pull/26948) | Refactor error messages; add `pattern_descriptor` for fields in spec | +| `0.2.21` | 2023-05-30 | [25314](https://github.com/airbytehq/airbyte/pull/25314) | Add full refresh custom table `asset_group_listing_group_filter` | +| `0.2.20` | 2023-05-30 | [25624](https://github.com/airbytehq/airbyte/pull/25624) | Add `asset` Resource to full refresh custom tables (GAQL Queries) | +| `0.2.19` | 2023-05-15 | [26209](https://github.com/airbytehq/airbyte/pull/26209) | Handle Token Refresh errors as `config_error` | +| `0.2.18` | 2023-05-15 | [25947](https://github.com/airbytehq/airbyte/pull/25947) | Improve GAQL parser error message if multiple resources provided | +| `0.2.17` | 2023-05-11 | [25987](https://github.com/airbytehq/airbyte/pull/25987) | Categorized Config Errors Accurately | +| `0.2.16` | 2023-05-10 | [25965](https://github.com/airbytehq/airbyte/pull/25965) | Fix Airbyte date-time data-types | +| `0.2.14` | 2023-03-21 | [24945](https://github.com/airbytehq/airbyte/pull/24945) | For custom google query fixed schema type for "data_type: ENUM" and "is_repeated: true" to array of strings | +| `0.2.13` | 2023-03-21 | [24338](https://github.com/airbytehq/airbyte/pull/24338) | Migrate to v13 | +| `0.2.12` | 2023-03-17 | [22985](https://github.com/airbytehq/airbyte/pull/22985) | Specified date formatting in specification | +| `0.2.11` | 2023-03-13 | [23999](https://github.com/airbytehq/airbyte/pull/23999) | Fix incremental sync for Campaigns stream | +| `0.2.10` | 2023-02-11 | [22703](https://github.com/airbytehq/airbyte/pull/22703) | Add support for custom full_refresh streams | +| `0.2.9` | 2023-01-23 | [21705](https://github.com/airbytehq/airbyte/pull/21705) | Fix multibyte issue; Bump google-ads package to 19.0.0 | +| `0.2.8` | 2023-01-18 | [21517](https://github.com/airbytehq/airbyte/pull/21517) | Write fewer logs | +| `0.2.7` | 2023-01-10 | [20755](https://github.com/airbytehq/airbyte/pull/20755) | Add more logs to debug stuck syncs | +| `0.2.6` | 2022-12-22 | [20855](https://github.com/airbytehq/airbyte/pull/20855) | Retry 429 and 5xx errors | +| `0.2.5` | 2022-11-22 | [19700](https://github.com/airbytehq/airbyte/pull/19700) | Fix schema for `campaigns` stream | +| `0.2.4` | 2022-11-09 | [19208](https://github.com/airbytehq/airbyte/pull/19208) | Add TypeTransofrmer to Campaings stream to force proper type casting | +| `0.2.3` | 2022-10-17 | [18069](https://github.com/airbytehq/airbyte/pull/18069) | Add `segments.hour`, `metrics.ctr`, `metrics.conversions` and `metrics.conversions_values` fields to `campaigns` report stream | +| `0.2.2` | 2022-10-21 | [17412](https://github.com/airbytehq/airbyte/pull/17412) | Release with CDK >= 0.2.2 | +| `0.2.1` | 2022-09-29 | [17412](https://github.com/airbytehq/airbyte/pull/17412) | Always use latest CDK version | +| `0.2.0` | 2022-08-23 | [15858](https://github.com/airbytehq/airbyte/pull/15858) | Mark the `query` and `table_name` fields in `custom_queries` as required | +| `0.1.44` | 2022-07-27 | [15084](https://github.com/airbytehq/airbyte/pull/15084) | Fix data type `ad_group_criterion.topic.path` in `display_topics_performance_report` and shifted `campaigns` to non-managers streams | +| `0.1.43` | 2022-07-12 | [14614](https://github.com/airbytehq/airbyte/pull/14614) | Update API version to `v11`, update `google-ads` to 17.0.0 | +| `0.1.42` | 2022-06-08 | [13624](https://github.com/airbytehq/airbyte/pull/13624) | Update `google-ads` to 15.1.1, pin `protobuf==3.20.0` to work on MacOS M1 machines (AMD) | +| `0.1.41` | 2022-06-08 | [13618](https://github.com/airbytehq/airbyte/pull/13618) | Add missing dependency | +| `0.1.40` | 2022-06-02 | [13423](https://github.com/airbytehq/airbyte/pull/13423) | Fix the missing data [issue](https://github.com/airbytehq/airbyte/issues/12999) | +| `0.1.39` | 2022-05-18 | [12914](https://github.com/airbytehq/airbyte/pull/12914) | Fix GAQL query validation and log auth errors instead of failing the sync | +| `0.1.38` | 2022-05-12 | [12807](https://github.com/airbytehq/airbyte/pull/12807) | Documentation updates | +| `0.1.37` | 2022-05-06 | [12651](https://github.com/airbytehq/airbyte/pull/12651) | Improve integration and unit tests | +| `0.1.36` | 2022-04-19 | [12158](https://github.com/airbytehq/airbyte/pull/12158) | Fix `*_labels` streams data type | +| `0.1.35` | 2022-04-18 | [9310](https://github.com/airbytehq/airbyte/pull/9310) | Add new fields to reports | +| `0.1.34` | 2022-03-29 | [11602](https://github.com/airbytehq/airbyte/pull/11602) | Add budget amount to campaigns stream. | +| `0.1.33` | 2022-03-29 | [11513](https://github.com/airbytehq/airbyte/pull/11513) | When `end_date` is configured in the future, use today's date instead. | +| `0.1.32` | 2022-03-24 | [11371](https://github.com/airbytehq/airbyte/pull/11371) | Improve how connection check returns error messages | +| `0.1.31` | 2022-03-23 | [11301](https://github.com/airbytehq/airbyte/pull/11301) | Update docs and spec to clarify usage | +| `0.1.30` | 2022-03-23 | [11221](https://github.com/airbytehq/airbyte/pull/11221) | Add `*_labels` streams to fetch the label text rather than their IDs | +| `0.1.29` | 2022-03-22 | [10919](https://github.com/airbytehq/airbyte/pull/10919) | Fix user location report schema and add to acceptance tests | +| `0.1.28` | 2022-02-25 | [10372](https://github.com/airbytehq/airbyte/pull/10372) | Add network fields to click view stream | +| `0.1.27` | 2022-02-16 | [10315](https://github.com/airbytehq/airbyte/pull/10315) | Make `ad_group_ads` and other streams support incremental sync. | +| `0.1.26` | 2022-02-11 | [10150](https://github.com/airbytehq/airbyte/pull/10150) | Add support for multiple customer IDs. | +| `0.1.25` | 2022-02-04 | [9812](https://github.com/airbytehq/airbyte/pull/9812) | Handle `EXPIRED_PAGE_TOKEN` exception and retry with updated state. | +| `0.1.24` | 2022-02-04 | [9996](https://github.com/airbytehq/airbyte/pull/9996) | Use Google Ads API version V9. | +| `0.1.23` | 2022-01-25 | [8669](https://github.com/airbytehq/airbyte/pull/8669) | Add end date parameter in spec. | +| `0.1.22` | 2022-01-24 | [9608](https://github.com/airbytehq/airbyte/pull/9608) | Reduce stream slice date range. | +| `0.1.21` | 2021-12-28 | [9149](https://github.com/airbytehq/airbyte/pull/9149) | Update title and description | +| `0.1.20` | 2021-12-22 | [9071](https://github.com/airbytehq/airbyte/pull/9071) | Fix: Keyword schema enum | +| `0.1.19` | 2021-12-14 | [8431](https://github.com/airbytehq/airbyte/pull/8431) | Add new streams: Geographic and Keyword | +| `0.1.18` | 2021-12-09 | [8225](https://github.com/airbytehq/airbyte/pull/8225) | Include time_zone to sync. Remove streams for manager account. | +| `0.1.16` | 2021-11-22 | [8178](https://github.com/airbytehq/airbyte/pull/8178) | Clarify setup fields | +| `0.1.15` | 2021-10-07 | [6684](https://github.com/airbytehq/airbyte/pull/6684) | Add new stream `click_view` | +| `0.1.14` | 2021-10-01 | [6565](https://github.com/airbytehq/airbyte/pull/6565) | Fix OAuth Spec File | +| `0.1.13` | 2021-09-27 | [6458](https://github.com/airbytehq/airbyte/pull/6458) | Update OAuth Spec File | +| `0.1.11` | 2021-09-22 | [6373](https://github.com/airbytehq/airbyte/pull/6373) | Fix inconsistent segments.date field type across all streams | +| `0.1.10` | 2021-09-13 | [6022](https://github.com/airbytehq/airbyte/pull/6022) | Annotate Oauth2 flow initialization parameters in connector spec | +| `0.1.9` | 2021-09-07 | [5302](https://github.com/airbytehq/airbyte/pull/5302) | Add custom query stream support | +| `0.1.8` | 2021-08-03 | [5509](https://github.com/airbytehq/airbyte/pull/5509) | Allow additionalProperties in spec.json | +| `0.1.7` | 2021-08-03 | [5422](https://github.com/airbytehq/airbyte/pull/5422) | Correct query to not skip dates | +| `0.1.6` | 2021-08-03 | [5423](https://github.com/airbytehq/airbyte/pull/5423) | Added new stream UserLocationReport | +| `0.1.5` | 2021-08-03 | [5159](https://github.com/airbytehq/airbyte/pull/5159) | Add field `login_customer_id` to spec | +| `0.1.4` | 2021-07-28 | [4962](https://github.com/airbytehq/airbyte/pull/4962) | Support new Report streams | +| `0.1.3` | 2021-07-23 | [4788](https://github.com/airbytehq/airbyte/pull/4788) | Support main streams, fix bug with exception `DATE_RANGE_TOO_NARROW` for incremental streams | +| `0.1.2` | 2021-07-06 | [4539](https://github.com/airbytehq/airbyte/pull/4539) | Add `AIRBYTE_ENTRYPOINT` for Kubernetes support | +| `0.1.1` | 2021-06-23 | [4288](https://github.com/airbytehq/airbyte/pull/4288) | Fix `Bugfix: Correctly declare required parameters` | From 796c4845a897668b59a7227a7b1269c6e233486e Mon Sep 17 00:00:00 2001 From: Subodh Kant Chaturvedi Date: Wed, 10 Jan 2024 00:16:35 +0530 Subject: [PATCH 027/574] java-cdk-destination: implement logic to update destination stats in state message from async framework (#33969) --- airbyte-cdk/java/airbyte-cdk/README.md | 1 + .../AsyncStreamConsumer.java | 2 +- .../destination_async/FlushWorkers.java | 13 +- .../buffers/BufferEnqueue.java | 4 +- .../buffers/MemoryAwareMessageBatch.java | 4 +- .../state/GlobalAsyncStateManager.java | 69 +++++++++-- .../PartialStateWithDestinationStats.java | 10 ++ .../src/main/resources/version.properties | 2 +- .../AsyncStreamConsumerTest.java | 19 ++- .../buffers/BufferDequeueTest.java | 33 ++--- .../buffers/BufferEnqueueTest.java | 7 +- .../state/GlobalAsyncStateManagerTest.java | 117 ++++++++++++------ 12 files changed, 203 insertions(+), 78 deletions(-) create mode 100644 airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/state/PartialStateWithDestinationStats.java diff --git a/airbyte-cdk/java/airbyte-cdk/README.md b/airbyte-cdk/java/airbyte-cdk/README.md index b817e3ac12cb7..611e0bb074b67 100644 --- a/airbyte-cdk/java/airbyte-cdk/README.md +++ b/airbyte-cdk/java/airbyte-cdk/README.md @@ -166,6 +166,7 @@ MavenLocal debugging steps: | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0.11.2 | 2024-01-09 | [\#33969](https://github.com/airbytehq/airbyte/pull/33969) | Destination state stats implementation | | 0.11.1 | 2024-01-04 | [\#33727](https://github.com/airbytehq/airbyte/pull/33727) | SSH bastion heartbeats for Destinations | | 0.11.0 | 2024-01-04 | [\#33730](https://github.com/airbytehq/airbyte/pull/33730) | DV2 T+D uses Sql struct to represent transactions; other T+D-related changes | | 0.10.4 | 2023-12-20 | [\#33071](https://github.com/airbytehq/airbyte/pull/33071) | Add the ability to parse JDBC parameters with another delimiter than '&' | diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/AsyncStreamConsumer.java b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/AsyncStreamConsumer.java index 1a96732601fe9..711326fd919b0 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/AsyncStreamConsumer.java +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/AsyncStreamConsumer.java @@ -157,7 +157,7 @@ public void accept(final String messageString, final Integer sizeInBytes) throws getRecordCounter(message.getRecord().getStreamDescriptor()).incrementAndGet(); } - bufferEnqueue.addRecord(message, sizeInBytes + PARTIAL_DESERIALIZE_REF_BYTES); + bufferEnqueue.addRecord(message, sizeInBytes + PARTIAL_DESERIALIZE_REF_BYTES, defaultNamespace); } /** diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/FlushWorkers.java b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/FlushWorkers.java index 78ad409196cdd..603fa5054da2f 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/FlushWorkers.java +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/FlushWorkers.java @@ -6,9 +6,9 @@ import io.airbyte.cdk.integrations.destination_async.buffers.BufferDequeue; import io.airbyte.cdk.integrations.destination_async.buffers.StreamAwareQueue.MessageWithMeta; -import io.airbyte.cdk.integrations.destination_async.partial_messages.PartialAirbyteMessage; import io.airbyte.cdk.integrations.destination_async.state.FlushFailure; import io.airbyte.cdk.integrations.destination_async.state.GlobalAsyncStateManager; +import io.airbyte.cdk.integrations.destination_async.state.PartialStateWithDestinationStats; import io.airbyte.commons.json.Jsons; import io.airbyte.protocol.models.v0.AirbyteMessage; import io.airbyte.protocol.models.v0.StreamDescriptor; @@ -237,11 +237,12 @@ public void close() throws Exception { debugLoop.shutdownNow(); } - private void emitStateMessages(final List partials) { - partials - .stream() - .map(partial -> Jsons.deserialize(partial.getSerialized(), AirbyteMessage.class)) - .forEach(outputRecordCollector); + private void emitStateMessages(final List partials) { + for (final PartialStateWithDestinationStats partial : partials) { + final AirbyteMessage message = Jsons.deserialize(partial.stateMessage().getSerialized(), AirbyteMessage.class); + message.getState().setDestinationStats(partial.stats()); + outputRecordCollector.accept(message); + } } private static String humanReadableFlushWorkerId(final UUID flushWorkerId) { diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/buffers/BufferEnqueue.java b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/buffers/BufferEnqueue.java index 0434678e12a2a..09f67f62c786c 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/buffers/BufferEnqueue.java +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/buffers/BufferEnqueue.java @@ -38,11 +38,11 @@ public BufferEnqueue(final GlobalMemoryManager memoryManager, * @param message to buffer * @param sizeInBytes */ - public void addRecord(final PartialAirbyteMessage message, final Integer sizeInBytes) { + public void addRecord(final PartialAirbyteMessage message, final Integer sizeInBytes, final String defaultNamespace) { if (message.getType() == Type.RECORD) { handleRecord(message, sizeInBytes); } else if (message.getType() == Type.STATE) { - stateManager.trackState(message, sizeInBytes); + stateManager.trackState(message, sizeInBytes, defaultNamespace); } } diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/buffers/MemoryAwareMessageBatch.java b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/buffers/MemoryAwareMessageBatch.java index 2a0f541c6284c..591837196c1ae 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/buffers/MemoryAwareMessageBatch.java +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/buffers/MemoryAwareMessageBatch.java @@ -6,8 +6,8 @@ import io.airbyte.cdk.integrations.destination_async.GlobalMemoryManager; import io.airbyte.cdk.integrations.destination_async.buffers.StreamAwareQueue.MessageWithMeta; -import io.airbyte.cdk.integrations.destination_async.partial_messages.PartialAirbyteMessage; import io.airbyte.cdk.integrations.destination_async.state.GlobalAsyncStateManager; +import io.airbyte.cdk.integrations.destination_async.state.PartialStateWithDestinationStats; import java.util.List; import java.util.Map; import org.slf4j.Logger; @@ -64,7 +64,7 @@ public void close() throws Exception { * * @return list of states that can be flushed */ - public List flushStates(final Map stateIdToCount) { + public List flushStates(final Map stateIdToCount) { stateIdToCount.forEach(stateManager::decrement); return stateManager.flushStates(); } diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/state/GlobalAsyncStateManager.java b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/state/GlobalAsyncStateManager.java index c704a36d753dd..e0283bdfe7678 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/state/GlobalAsyncStateManager.java +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/state/GlobalAsyncStateManager.java @@ -7,11 +7,12 @@ import static java.lang.Thread.sleep; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import io.airbyte.cdk.integrations.destination_async.GlobalMemoryManager; import io.airbyte.cdk.integrations.destination_async.partial_messages.PartialAirbyteMessage; -import io.airbyte.cdk.integrations.destination_async.partial_messages.PartialAirbyteStreamState; import io.airbyte.protocol.models.v0.AirbyteMessage; import io.airbyte.protocol.models.v0.AirbyteStateMessage; +import io.airbyte.protocol.models.v0.AirbyteStateStats; import io.airbyte.protocol.models.v0.StreamDescriptor; import java.util.ArrayList; import java.util.Collection; @@ -70,7 +71,22 @@ public class GlobalAsyncStateManager { boolean preState = true; private final ConcurrentMap> descToStateIdQ = new ConcurrentHashMap<>(); + /** + * Both {@link stateIdToCounter} and {@link stateIdToCounterForPopulatingDestinationStats} are used + * to maintain a counter for the number of records associated with a give state i.e. before a state + * was received, how many records were seen until that point. As records are received the value for + * both are incremented. The difference is the purpose of the two attributes. + * {@link stateIdToCounter} is used to determine whether a state is safe to emit or not. This is + * done by decrementing the value as records are committed to the destination. If the value hits 0, + * it means all the records associated with a given state have been committed to the destination, it + * is safe to emit the state back to platform. But because of this we can't use it to determine the + * actual number of records that are associated with a state to update the value of + * {@link AirbyteStateMessage#destinationStats} at the time of emitting the state message. That's + * where we need {@link stateIdToCounterForPopulatingDestinationStats}, which is only reset when a + * state message has been emitted. + */ private final ConcurrentMap stateIdToCounter = new ConcurrentHashMap<>(); + private final ConcurrentMap stateIdToCounterForPopulatingDestinationStats = new ConcurrentHashMap<>(); private final ConcurrentMap> stateIdToState = new ConcurrentHashMap<>(); // Alias-ing only exists in the non-STREAM case where we have to convert existing state ids to one @@ -97,7 +113,7 @@ public GlobalAsyncStateManager(final GlobalMemoryManager memoryManager) { * Because state messages are a watermark, all preceding records need to be flushed before the state * message can be processed. */ - public void trackState(final PartialAirbyteMessage message, final long sizeInBytes) { + public void trackState(final PartialAirbyteMessage message, final long sizeInBytes, final String defaultNamespace) { if (preState) { convertToGlobalIfNeeded(message); preState = false; @@ -105,7 +121,7 @@ public void trackState(final PartialAirbyteMessage message, final long sizeInByt // stateType should not change after a conversion. Preconditions.checkArgument(stateType == extractStateType(message)); - closeState(message, sizeInBytes); + closeState(message, sizeInBytes, defaultNamespace); } /** @@ -140,8 +156,8 @@ public void decrement(final long stateId, final long count) { * * @return list of state messages with no more inflight records. */ - public List flushStates() { - final List output = new ArrayList<>(); + public List flushStates() { + final List output = new ArrayList<>(); Long bytesFlushed = 0L; synchronized (this) { for (final Map.Entry> entry : descToStateIdQ.entrySet()) { @@ -169,13 +185,17 @@ public List flushStates() { final var allRecordsCommitted = oldestStateCounter.get() == 0; if (allRecordsCommitted) { - output.add(oldestState.getLeft()); + final PartialAirbyteMessage stateMessage = oldestState.getLeft(); + final double flushedRecordsAssociatedWithState = stateIdToCounterForPopulatingDestinationStats.get(oldestStateId).doubleValue(); + output.add(new PartialStateWithDestinationStats(stateMessage, + new AirbyteStateStats().withRecordCount(flushedRecordsAssociatedWithState))); bytesFlushed += oldestState.getRight(); // cleanup entry.getValue().poll(); stateIdToState.remove(oldestStateId); stateIdToCounter.remove(oldestStateId); + stateIdToCounterForPopulatingDestinationStats.remove(oldestStateId); } else { break; } @@ -196,6 +216,9 @@ private Long getStateIdAndIncrement(final StreamDescriptor streamDescriptor, fin } final Long stateId = descToStateIdQ.get(resolvedDescriptor).peekLast(); final var update = stateIdToCounter.get(stateId).addAndGet(increment); + if (increment >= 0) { + stateIdToCounterForPopulatingDestinationStats.get(stateId).addAndGet(increment); + } log.trace("State id: {}, count: {}", stateId, update); return stateId; } @@ -252,6 +275,10 @@ private void convertToGlobalIfNeeded(final PartialAirbyteMessage message) { .sum(); stateIdToCounter.clear(); stateIdToCounter.put(retroactiveGlobalStateId, new AtomicLong(combinedCounter)); + + stateIdToCounterForPopulatingDestinationStats.clear(); + stateIdToCounterForPopulatingDestinationStats.put(retroactiveGlobalStateId, new AtomicLong(combinedCounter)); + } } @@ -269,8 +296,8 @@ private AirbyteStateMessage.AirbyteStateType extractStateType(final PartialAirby * to the newly arrived state message. We also increment the state id in preparation for the next * state message. */ - private void closeState(final PartialAirbyteMessage message, final long sizeInBytes) { - final StreamDescriptor resolvedDescriptor = extractStream(message).orElse(SENTINEL_GLOBAL_DESC); + private void closeState(final PartialAirbyteMessage message, final long sizeInBytes, final String defaultNamespace) { + final StreamDescriptor resolvedDescriptor = extractStream(message, defaultNamespace).orElse(SENTINEL_GLOBAL_DESC); stateIdToState.put(getStateId(resolvedDescriptor), ImmutablePair.of(message, sizeInBytes)); registerNewStateId(resolvedDescriptor); allocateMemoryToState(sizeInBytes); @@ -309,8 +336,29 @@ public String getMemoryUsageMessage() { (double) memoryUsed.get() / memoryAllocated.get()); } - private static Optional extractStream(final PartialAirbyteMessage message) { - return Optional.ofNullable(message.getState().getStream()).map(PartialAirbyteStreamState::getStreamDescriptor); + /** + * If the user has selected the Destination Namespace as the Destination default while setting up + * the connector, the platform sets the namespace as null in the StreamDescriptor in the + * AirbyteMessages (both record and state messages). The destination checks that if the namespace is + * empty or null, if yes then re-populates it with the defaultNamespace. See + * {@link io.airbyte.cdk.integrations.destination_async.AsyncStreamConsumer#accept(String,Integer)} + * But destination only does this for the record messages. So when state messages arrive without a + * namespace and since the destination doesn't repopulate it with the default namespace, there is a + * mismatch between the StreamDescriptor from record messages and state messages. That breaks the + * logic of the state management class as {@link descToStateIdQ} needs to have consistent + * StreamDescriptor. This is why while trying to extract the StreamDescriptor from state messages, + * we check if the namespace is null, if yes then replace it with defaultNamespace to keep it + * consistent with the record messages. + */ + private static Optional extractStream(final PartialAirbyteMessage message, final String defaultNamespace) { + if (message.getState().getType() != null && message.getState().getType() == AirbyteStateMessage.AirbyteStateType.STREAM) { + final StreamDescriptor streamDescriptor = message.getState().getStream().getStreamDescriptor(); + if (Strings.isNullOrEmpty(streamDescriptor.getNamespace())) { + return Optional.of(new StreamDescriptor().withName(streamDescriptor.getName()).withNamespace(defaultNamespace)); + } + return Optional.of(streamDescriptor); + } + return Optional.empty(); } private long getStateAfterAlias(final long stateId) { @@ -329,6 +377,7 @@ private void registerNewStreamDescriptor(final StreamDescriptor resolvedDescript private void registerNewStateId(final StreamDescriptor resolvedDescriptor) { final long stateId = StateIdProvider.getNextId(); stateIdToCounter.put(stateId, new AtomicLong(0)); + stateIdToCounterForPopulatingDestinationStats.put(stateId, new AtomicLong(0)); descToStateIdQ.get(resolvedDescriptor).add(stateId); } diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/state/PartialStateWithDestinationStats.java b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/state/PartialStateWithDestinationStats.java new file mode 100644 index 0000000000000..4270fdc00415f --- /dev/null +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/integrations/destination_async/state/PartialStateWithDestinationStats.java @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2023 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.cdk.integrations.destination_async.state; + +import io.airbyte.cdk.integrations.destination_async.partial_messages.PartialAirbyteMessage; +import io.airbyte.protocol.models.v0.AirbyteStateStats; + +public record PartialStateWithDestinationStats(PartialAirbyteMessage stateMessage, AirbyteStateStats stats) {} diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties b/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties index 94b3b6e26a45f..490fce0e6149f 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties @@ -1 +1 @@ -version=0.11.1 +version=0.11.2 diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/AsyncStreamConsumerTest.java b/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/AsyncStreamConsumerTest.java index 7a34150ea76bd..f06b67121d5b4 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/AsyncStreamConsumerTest.java +++ b/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/AsyncStreamConsumerTest.java @@ -31,6 +31,7 @@ import io.airbyte.protocol.models.v0.AirbyteRecordMessage; import io.airbyte.protocol.models.v0.AirbyteStateMessage; import io.airbyte.protocol.models.v0.AirbyteStateMessage.AirbyteStateType; +import io.airbyte.protocol.models.v0.AirbyteStateStats; import io.airbyte.protocol.models.v0.AirbyteStreamState; import io.airbyte.protocol.models.v0.CatalogHelpers; import io.airbyte.protocol.models.v0.ConfiguredAirbyteCatalog; @@ -137,7 +138,14 @@ void test1StreamWith1State() throws Exception { verifyRecords(STREAM_NAME, SCHEMA_NAME, expectedRecords); - verify(outputRecordCollector).accept(STATE_MESSAGE1); + final AirbyteMessage stateMessageWithDestinationStatsUpdated = new AirbyteMessage() + .withType(Type.STATE) + .withState(new AirbyteStateMessage() + .withType(AirbyteStateType.STREAM) + .withStream(new AirbyteStreamState().withStreamDescriptor(STREAM1_DESC).withStreamState(Jsons.jsonNode(1))) + .withDestinationStats(new AirbyteStateStats().withRecordCount((double) expectedRecords.size()))); + + verify(outputRecordCollector).accept(stateMessageWithDestinationStatsUpdated); } @Test @@ -154,7 +162,14 @@ void test1StreamWith2State() throws Exception { verifyRecords(STREAM_NAME, SCHEMA_NAME, expectedRecords); - verify(outputRecordCollector, times(1)).accept(STATE_MESSAGE2); + final AirbyteMessage stateMessageWithDestinationStatsUpdated = new AirbyteMessage() + .withType(Type.STATE) + .withState(new AirbyteStateMessage() + .withType(AirbyteStateType.STREAM) + .withStream(new AirbyteStreamState().withStreamDescriptor(STREAM1_DESC).withStreamState(Jsons.jsonNode(2))) + .withDestinationStats(new AirbyteStateStats().withRecordCount(0.0))); + + verify(outputRecordCollector, times(1)).accept(stateMessageWithDestinationStatsUpdated); } @Test diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/buffers/BufferDequeueTest.java b/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/buffers/BufferDequeueTest.java index eb345f9b0c696..eb565b90ec6d3 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/buffers/BufferDequeueTest.java +++ b/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/buffers/BufferDequeueTest.java @@ -21,6 +21,7 @@ public class BufferDequeueTest { private static final int RECORD_SIZE_20_BYTES = 20; + private static final String DEFAULT_NAMESPACE = "foo_namespace"; public static final String RECORD_20_BYTES = "abc"; private static final String STREAM_NAME = "stream1"; private static final StreamDescriptor STREAM_DESC = new StreamDescriptor().withName(STREAM_NAME); @@ -38,10 +39,10 @@ void testTakeShouldBestEffortRead() { final BufferEnqueue enqueue = bufferManager.getBufferEnqueue(); final BufferDequeue dequeue = bufferManager.getBufferDequeue(); - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); // total size of records is 80, so we expect 50 to get us 2 records (prefer to under-pull records // than over-pull). @@ -60,9 +61,9 @@ void testTakeShouldReturnAllIfPossible() { final BufferEnqueue enqueue = bufferManager.getBufferEnqueue(); final BufferDequeue dequeue = bufferManager.getBufferDequeue(); - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); try (final MemoryAwareMessageBatch take = dequeue.take(STREAM_DESC, 60)) { assertEquals(3, take.getData().size()); @@ -77,8 +78,8 @@ void testTakeFewerRecordsThanSizeLimitShouldNotError() { final BufferEnqueue enqueue = bufferManager.getBufferEnqueue(); final BufferDequeue dequeue = bufferManager.getBufferDequeue(); - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); try (final MemoryAwareMessageBatch take = dequeue.take(STREAM_DESC, Long.MAX_VALUE)) { assertEquals(2, take.getData().size()); @@ -95,13 +96,13 @@ void testMetadataOperationsCorrect() { final BufferEnqueue enqueue = bufferManager.getBufferEnqueue(); final BufferDequeue dequeue = bufferManager.getBufferDequeue(); - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); final var secondStream = new StreamDescriptor().withName("stream_2"); final PartialAirbyteMessage recordFromSecondStream = Jsons.clone(RECORD_MSG_20_BYTES); recordFromSecondStream.getRecord().withStream(secondStream.getName()); - enqueue.addRecord(recordFromSecondStream, RECORD_SIZE_20_BYTES); + enqueue.addRecord(recordFromSecondStream, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); assertEquals(60, dequeue.getTotalGlobalQueueSizeBytes()); @@ -144,12 +145,12 @@ void cleansUpMemoryForEmptyQueues() throws Exception { assertEquals(BLOCK_SIZE_BYTES, memoryManager.getCurrentMemoryBytes()); // allocate a block for new stream - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); assertEquals(2 * BLOCK_SIZE_BYTES, memoryManager.getCurrentMemoryBytes()); - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); - enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); + enqueue.addRecord(RECORD_MSG_20_BYTES, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); // no re-allocates as we haven't breached block size assertEquals(2 * BLOCK_SIZE_BYTES, memoryManager.getCurrentMemoryBytes()); diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/buffers/BufferEnqueueTest.java b/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/buffers/BufferEnqueueTest.java index 11e61c6e4eb97..a555c403e5c07 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/buffers/BufferEnqueueTest.java +++ b/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/buffers/BufferEnqueueTest.java @@ -19,6 +19,7 @@ public class BufferEnqueueTest { private static final int RECORD_SIZE_20_BYTES = 20; + private static final String DEFAULT_NAMESPACE = "foo_namespace"; @Test void testAddRecordShouldAdd() { @@ -33,7 +34,7 @@ final var record = new PartialAirbyteMessage() .withRecord(new PartialAirbyteRecordMessage() .withStream(streamName)); - enqueue.addRecord(record, RECORD_SIZE_20_BYTES); + enqueue.addRecord(record, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); assertEquals(1, streamToBuffer.get(stream).size()); assertEquals(20L, streamToBuffer.get(stream).getCurrentMemoryUsage()); @@ -53,8 +54,8 @@ final var record = new PartialAirbyteMessage() .withRecord(new PartialAirbyteRecordMessage() .withStream(streamName)); - enqueue.addRecord(record, RECORD_SIZE_20_BYTES); - enqueue.addRecord(record, RECORD_SIZE_20_BYTES); + enqueue.addRecord(record, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); + enqueue.addRecord(record, RECORD_SIZE_20_BYTES, DEFAULT_NAMESPACE); assertEquals(2, streamToBuffer.get(stream).size()); assertEquals(40, streamToBuffer.get(stream).getCurrentMemoryUsage()); diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/state/GlobalAsyncStateManagerTest.java b/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/state/GlobalAsyncStateManagerTest.java index 8c6f3ecf2e3b0..338b131d3cbeb 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/state/GlobalAsyncStateManagerTest.java +++ b/airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/destination_async/state/GlobalAsyncStateManagerTest.java @@ -14,27 +14,31 @@ import io.airbyte.cdk.integrations.destination_async.partial_messages.PartialAirbyteStreamState; import io.airbyte.protocol.models.v0.AirbyteMessage.Type; import io.airbyte.protocol.models.v0.AirbyteStateMessage.AirbyteStateType; +import io.airbyte.protocol.models.v0.AirbyteStateStats; import io.airbyte.protocol.models.v0.StreamDescriptor; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; class GlobalAsyncStateManagerTest { private static final long TOTAL_QUEUES_MAX_SIZE_LIMIT_BYTES = 100 * 1024 * 1024; // 10MB - + private static final String DEFAULT_NAMESPACE = "foo_namespace"; private static final long STATE_MSG_SIZE = 1000; + private static final String NAMESPACE = "namespace"; private static final String STREAM_NAME = "id_and_name"; private static final String STREAM_NAME2 = STREAM_NAME + 2; private static final String STREAM_NAME3 = STREAM_NAME + 3; private static final StreamDescriptor STREAM1_DESC = new StreamDescriptor() - .withName(STREAM_NAME); + .withName(STREAM_NAME).withNamespace(NAMESPACE); private static final StreamDescriptor STREAM2_DESC = new StreamDescriptor() - .withName(STREAM_NAME2); + .withName(STREAM_NAME2).withNamespace(NAMESPACE); private static final StreamDescriptor STREAM3_DESC = new StreamDescriptor() - .withName(STREAM_NAME3); + .withName(STREAM_NAME3).withNamespace(NAMESPACE); private static final PartialAirbyteMessage GLOBAL_STATE_MESSAGE1 = new PartialAirbyteMessage() .withType(Type.STATE) @@ -54,6 +58,11 @@ class GlobalAsyncStateManagerTest { .withState(new PartialAirbyteStateMessage() .withType(AirbyteStateType.STREAM) .withStream(new PartialAirbyteStreamState().withStreamDescriptor(STREAM1_DESC))); + private static final PartialAirbyteMessage STREAM2_STATE_MESSAGE = new PartialAirbyteMessage() + .withType(Type.STATE) + .withState(new PartialAirbyteStateMessage() + .withType(AirbyteStateType.STREAM) + .withStream(new PartialAirbyteStreamState().withStreamDescriptor(STREAM2_DESC))); @Test void testBasic() { @@ -65,12 +74,17 @@ void testBasic() { stateManager.decrement(firstStateId, 2); // because no state message has been tracked, there is nothing to flush yet. - var flushed = stateManager.flushStates(); - assertEquals(0, flushed.size()); - - stateManager.trackState(STREAM1_STATE_MESSAGE1, STATE_MSG_SIZE); - flushed = stateManager.flushStates(); - assertEquals(List.of(STREAM1_STATE_MESSAGE1), flushed); + final Map stateWithStats = + stateManager.flushStates().stream() + .collect(Collectors.toMap(PartialStateWithDestinationStats::stateMessage, PartialStateWithDestinationStats::stats)); + assertEquals(0, stateWithStats.size()); + + stateManager.trackState(STREAM1_STATE_MESSAGE1, STATE_MSG_SIZE, DEFAULT_NAMESPACE); + final Map stateWithStats2 = + stateManager.flushStates().stream() + .collect(Collectors.toMap(PartialStateWithDestinationStats::stateMessage, PartialStateWithDestinationStats::stats)); + assertEquals(List.of(STREAM1_STATE_MESSAGE1), stateWithStats2.keySet().stream().toList()); + assertEquals(List.of(new AirbyteStateStats().withRecordCount(2.0)), stateWithStats2.values().stream().toList()); } @Nested @@ -81,10 +95,13 @@ void testEmptyQueuesGlobalState() { final GlobalAsyncStateManager stateManager = new GlobalAsyncStateManager(new GlobalMemoryManager(TOTAL_QUEUES_MAX_SIZE_LIMIT_BYTES)); // GLOBAL - stateManager.trackState(GLOBAL_STATE_MESSAGE1, STATE_MSG_SIZE); - assertEquals(List.of(GLOBAL_STATE_MESSAGE1), stateManager.flushStates()); + stateManager.trackState(GLOBAL_STATE_MESSAGE1, STATE_MSG_SIZE, DEFAULT_NAMESPACE); + final Map stateWithStats = stateManager.flushStates().stream() + .collect(Collectors.toMap(PartialStateWithDestinationStats::stateMessage, PartialStateWithDestinationStats::stats)); + assertEquals(List.of(GLOBAL_STATE_MESSAGE1), stateWithStats.keySet().stream().toList()); + assertEquals(List.of(new AirbyteStateStats().withRecordCount(0.0)), stateWithStats.values().stream().toList()); - assertThrows(IllegalArgumentException.class, () -> stateManager.trackState(STREAM1_STATE_MESSAGE1, STATE_MSG_SIZE)); + assertThrows(IllegalArgumentException.class, () -> stateManager.trackState(STREAM1_STATE_MESSAGE1, STATE_MSG_SIZE, DEFAULT_NAMESPACE)); } @Test @@ -96,7 +113,7 @@ void testConversion() { final var preConvertId2 = simulateIncomingRecords(STREAM3_DESC, 10, stateManager); assertEquals(3, Set.of(preConvertId0, preConvertId1, preConvertId2).size()); - stateManager.trackState(GLOBAL_STATE_MESSAGE1, STATE_MSG_SIZE); + stateManager.trackState(GLOBAL_STATE_MESSAGE1, STATE_MSG_SIZE, DEFAULT_NAMESPACE); // Since this is actually a global state, we can only flush after all streams are done. stateManager.decrement(preConvertId0, 10); @@ -104,7 +121,11 @@ void testConversion() { stateManager.decrement(preConvertId1, 10); assertEquals(List.of(), stateManager.flushStates()); stateManager.decrement(preConvertId2, 10); - assertEquals(List.of(GLOBAL_STATE_MESSAGE1), stateManager.flushStates()); + final Map stateWithStats = stateManager.flushStates().stream() + .collect(Collectors.toMap(PartialStateWithDestinationStats::stateMessage, PartialStateWithDestinationStats::stats)); + assertEquals(List.of(GLOBAL_STATE_MESSAGE1), stateWithStats.keySet().stream().toList()); + assertEquals(List.of(new AirbyteStateStats().withRecordCount(30.0)), stateWithStats.values().stream().toList()); + } @Test @@ -112,14 +133,20 @@ void testCorrectFlushingOneStream() { final GlobalAsyncStateManager stateManager = new GlobalAsyncStateManager(new GlobalMemoryManager(TOTAL_QUEUES_MAX_SIZE_LIMIT_BYTES)); final var preConvertId0 = simulateIncomingRecords(STREAM1_DESC, 10, stateManager); - stateManager.trackState(GLOBAL_STATE_MESSAGE1, STATE_MSG_SIZE); + stateManager.trackState(GLOBAL_STATE_MESSAGE1, STATE_MSG_SIZE, DEFAULT_NAMESPACE); stateManager.decrement(preConvertId0, 10); - assertEquals(List.of(GLOBAL_STATE_MESSAGE1), stateManager.flushStates()); + final Map stateWithStats = stateManager.flushStates().stream() + .collect(Collectors.toMap(PartialStateWithDestinationStats::stateMessage, PartialStateWithDestinationStats::stats)); + assertEquals(List.of(GLOBAL_STATE_MESSAGE1), stateWithStats.keySet().stream().toList()); + assertEquals(List.of(new AirbyteStateStats().withRecordCount(10.0)), stateWithStats.values().stream().toList()); final var afterConvertId1 = simulateIncomingRecords(STREAM1_DESC, 10, stateManager); - stateManager.trackState(GLOBAL_STATE_MESSAGE2, STATE_MSG_SIZE); + stateManager.trackState(GLOBAL_STATE_MESSAGE2, STATE_MSG_SIZE, DEFAULT_NAMESPACE); stateManager.decrement(afterConvertId1, 10); - assertEquals(List.of(GLOBAL_STATE_MESSAGE2), stateManager.flushStates()); + final Map stateWithStats2 = stateManager.flushStates().stream() + .collect(Collectors.toMap(PartialStateWithDestinationStats::stateMessage, PartialStateWithDestinationStats::stats)); + assertEquals(List.of(GLOBAL_STATE_MESSAGE2), stateWithStats2.keySet().stream().toList()); + assertEquals(List.of(new AirbyteStateStats().withRecordCount(10.0)), stateWithStats2.values().stream().toList()); } @Test @@ -129,17 +156,23 @@ void testCorrectFlushingManyStreams() { final var preConvertId0 = simulateIncomingRecords(STREAM1_DESC, 10, stateManager); final var preConvertId1 = simulateIncomingRecords(STREAM2_DESC, 10, stateManager); assertNotEquals(preConvertId0, preConvertId1); - stateManager.trackState(GLOBAL_STATE_MESSAGE1, STATE_MSG_SIZE); + stateManager.trackState(GLOBAL_STATE_MESSAGE1, STATE_MSG_SIZE, DEFAULT_NAMESPACE); stateManager.decrement(preConvertId0, 10); stateManager.decrement(preConvertId1, 10); - assertEquals(List.of(GLOBAL_STATE_MESSAGE1), stateManager.flushStates()); + final Map stateWithStats = stateManager.flushStates().stream() + .collect(Collectors.toMap(PartialStateWithDestinationStats::stateMessage, PartialStateWithDestinationStats::stats)); + assertEquals(List.of(GLOBAL_STATE_MESSAGE1), stateWithStats.keySet().stream().toList()); + assertEquals(List.of(new AirbyteStateStats().withRecordCount(20.0)), stateWithStats.values().stream().toList()); final var afterConvertId0 = simulateIncomingRecords(STREAM1_DESC, 10, stateManager); final var afterConvertId1 = simulateIncomingRecords(STREAM2_DESC, 10, stateManager); assertEquals(afterConvertId0, afterConvertId1); - stateManager.trackState(GLOBAL_STATE_MESSAGE2, STATE_MSG_SIZE); + stateManager.trackState(GLOBAL_STATE_MESSAGE2, STATE_MSG_SIZE, DEFAULT_NAMESPACE); stateManager.decrement(afterConvertId0, 20); - assertEquals(List.of(GLOBAL_STATE_MESSAGE2), stateManager.flushStates()); + final Map stateWithStats2 = stateManager.flushStates().stream() + .collect(Collectors.toMap(PartialStateWithDestinationStats::stateMessage, PartialStateWithDestinationStats::stats)); + assertEquals(List.of(GLOBAL_STATE_MESSAGE2), stateWithStats2.keySet().stream().toList()); + assertEquals(List.of(new AirbyteStateStats().withRecordCount(20.0)), stateWithStats2.values().stream().toList()); } } @@ -152,10 +185,13 @@ void testEmptyQueues() { final GlobalAsyncStateManager stateManager = new GlobalAsyncStateManager(new GlobalMemoryManager(TOTAL_QUEUES_MAX_SIZE_LIMIT_BYTES)); // GLOBAL - stateManager.trackState(STREAM1_STATE_MESSAGE1, STATE_MSG_SIZE); - assertEquals(List.of(STREAM1_STATE_MESSAGE1), stateManager.flushStates()); + stateManager.trackState(STREAM1_STATE_MESSAGE1, STATE_MSG_SIZE, DEFAULT_NAMESPACE); + final Map stateWithStats = stateManager.flushStates().stream() + .collect(Collectors.toMap(PartialStateWithDestinationStats::stateMessage, PartialStateWithDestinationStats::stats)); + assertEquals(List.of(STREAM1_STATE_MESSAGE1), stateWithStats.keySet().stream().toList()); + assertEquals(List.of(new AirbyteStateStats().withRecordCount(0.0)), stateWithStats.values().stream().toList()); - assertThrows(IllegalArgumentException.class, () -> stateManager.trackState(GLOBAL_STATE_MESSAGE1, STATE_MSG_SIZE)); + assertThrows(IllegalArgumentException.class, () -> stateManager.trackState(GLOBAL_STATE_MESSAGE1, STATE_MSG_SIZE, DEFAULT_NAMESPACE)); } @Test @@ -163,15 +199,20 @@ void testCorrectFlushingOneStream() { final GlobalAsyncStateManager stateManager = new GlobalAsyncStateManager(new GlobalMemoryManager(TOTAL_QUEUES_MAX_SIZE_LIMIT_BYTES)); var stateId = simulateIncomingRecords(STREAM1_DESC, 3, stateManager); - stateManager.trackState(STREAM1_STATE_MESSAGE1, STATE_MSG_SIZE); + stateManager.trackState(STREAM1_STATE_MESSAGE1, STATE_MSG_SIZE, DEFAULT_NAMESPACE); stateManager.decrement(stateId, 3); - assertEquals(List.of(STREAM1_STATE_MESSAGE1), stateManager.flushStates()); + final Map stateWithStats = stateManager.flushStates().stream() + .collect(Collectors.toMap(PartialStateWithDestinationStats::stateMessage, PartialStateWithDestinationStats::stats)); + assertEquals(List.of(STREAM1_STATE_MESSAGE1), stateWithStats.keySet().stream().toList()); + assertEquals(List.of(new AirbyteStateStats().withRecordCount(3.0)), stateWithStats.values().stream().toList()); stateId = simulateIncomingRecords(STREAM1_DESC, 10, stateManager); - stateManager.trackState(STREAM1_STATE_MESSAGE2, STATE_MSG_SIZE); + stateManager.trackState(STREAM1_STATE_MESSAGE2, STATE_MSG_SIZE, DEFAULT_NAMESPACE); stateManager.decrement(stateId, 10); - assertEquals(List.of(STREAM1_STATE_MESSAGE2), stateManager.flushStates()); - + final Map stateWithStats2 = stateManager.flushStates().stream() + .collect(Collectors.toMap(PartialStateWithDestinationStats::stateMessage, PartialStateWithDestinationStats::stats)); + assertEquals(List.of(STREAM1_STATE_MESSAGE2), stateWithStats2.keySet().stream().toList()); + assertEquals(List.of(new AirbyteStateStats().withRecordCount(10.0)), stateWithStats2.values().stream().toList()); } @Test @@ -181,16 +222,22 @@ void testCorrectFlushingManyStream() { final var stream1StateId = simulateIncomingRecords(STREAM1_DESC, 3, stateManager); final var stream2StateId = simulateIncomingRecords(STREAM2_DESC, 7, stateManager); - stateManager.trackState(STREAM1_STATE_MESSAGE1, STATE_MSG_SIZE); + stateManager.trackState(STREAM1_STATE_MESSAGE1, STATE_MSG_SIZE, DEFAULT_NAMESPACE); stateManager.decrement(stream1StateId, 3); - assertEquals(List.of(STREAM1_STATE_MESSAGE1), stateManager.flushStates()); + final Map stateWithStats = stateManager.flushStates().stream() + .collect(Collectors.toMap(PartialStateWithDestinationStats::stateMessage, PartialStateWithDestinationStats::stats)); + assertEquals(List.of(STREAM1_STATE_MESSAGE1), stateWithStats.keySet().stream().toList()); + assertEquals(List.of(new AirbyteStateStats().withRecordCount(3.0)), stateWithStats.values().stream().toList()); stateManager.decrement(stream2StateId, 4); assertEquals(List.of(), stateManager.flushStates()); - stateManager.trackState(STREAM1_STATE_MESSAGE2, STATE_MSG_SIZE); + stateManager.trackState(STREAM2_STATE_MESSAGE, STATE_MSG_SIZE, DEFAULT_NAMESPACE); stateManager.decrement(stream2StateId, 3); // only flush state if counter is 0. - assertEquals(List.of(STREAM1_STATE_MESSAGE2), stateManager.flushStates()); + final Map stateWithStats2 = stateManager.flushStates().stream() + .collect(Collectors.toMap(PartialStateWithDestinationStats::stateMessage, PartialStateWithDestinationStats::stats)); + assertEquals(List.of(STREAM2_STATE_MESSAGE), stateWithStats2.keySet().stream().toList()); + assertEquals(List.of(new AirbyteStateStats().withRecordCount(7.0)), stateWithStats2.values().stream().toList()); } } From d19061056c7b09daf4330c51855b5da33a8da7cb Mon Sep 17 00:00:00 2001 From: Stephane Geneix <147216312+stephane-airbyte@users.noreply.github.com> Date: Tue, 9 Jan 2024 11:26:10 -0800 Subject: [PATCH 028/574] bubble up debezium config error messages as exceptions (#33658) Looks like the logic of handling the result of the debezium engine was slightly wrong. Wee were assining the local `error` field, and then checking itf it was not null, and overriding it in that case. The truth is that the engine can fail without filling in the `error` field, but still fill in the `message` field. So the new logic uses the `message` field if `error` is null fixes #31579 --- airbyte-cdk/java/airbyte-cdk/README.md | 1 + .../core/src/main/resources/version.properties | 2 +- .../internals/DebeziumRecordPublisher.java | 14 ++++++++------ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/airbyte-cdk/java/airbyte-cdk/README.md b/airbyte-cdk/java/airbyte-cdk/README.md index 611e0bb074b67..ce700502cdea0 100644 --- a/airbyte-cdk/java/airbyte-cdk/README.md +++ b/airbyte-cdk/java/airbyte-cdk/README.md @@ -166,6 +166,7 @@ MavenLocal debugging steps: | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0.11.3 | 2023-01-09 | [\#33658](https://github.com/airbytehq/airbyte/pull/33658) | Always fail when debezium fails, even if it happened during the setup phase. | | 0.11.2 | 2024-01-09 | [\#33969](https://github.com/airbytehq/airbyte/pull/33969) | Destination state stats implementation | | 0.11.1 | 2024-01-04 | [\#33727](https://github.com/airbytehq/airbyte/pull/33727) | SSH bastion heartbeats for Destinations | | 0.11.0 | 2024-01-04 | [\#33730](https://github.com/airbytehq/airbyte/pull/33730) | DV2 T+D uses Sql struct to represent transactions; other T+D-related changes | diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties b/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties index 490fce0e6149f..8b9cd41d8c02e 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties @@ -1 +1 @@ -version=0.11.2 +version=0.11.3 diff --git a/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/debezium/internals/DebeziumRecordPublisher.java b/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/debezium/internals/DebeziumRecordPublisher.java index bc5a3ec037f62..2f7e76b295329 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/debezium/internals/DebeziumRecordPublisher.java +++ b/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/debezium/internals/DebeziumRecordPublisher.java @@ -86,12 +86,14 @@ public void start(final BlockingQueue> queue) { .using((success, message, error) -> { LOGGER.info("Debezium engine shutdown. Engine terminated successfully : {}", success); LOGGER.info(message); - thrownError.set(error); - // If debezium has not shutdown correctly, it can indicate an error with the connector configuration - // or a partial sync success. - // In situations like these, the preference is to fail loud and clear. - if (thrownError.get() != null && !success) { - thrownError.set(new RuntimeException(message)); + if (!success) { + if (error != null) { + thrownError.set(error); + } else { + // There are cases where Debezium doesn't succeed but only fills the message field. + // In that case, we still want to fail loud and clear + thrownError.set(new RuntimeException(message)); + } } engineLatch.countDown(); }) From eb3e158da69bc0e545b0774dbddb34d1487f5f0b Mon Sep 17 00:00:00 2001 From: Ella Rohm-Ensing Date: Tue, 9 Jan 2024 14:05:31 -0600 Subject: [PATCH 029/574] pin pendulum dependency for poetry2setup (#34055) --- airbyte-ci/connectors/metadata_service/orchestrator/poetry.lock | 2 +- .../connectors/metadata_service/orchestrator/pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/airbyte-ci/connectors/metadata_service/orchestrator/poetry.lock b/airbyte-ci/connectors/metadata_service/orchestrator/poetry.lock index 5fbc81446d369..616f8c9d5095d 100644 --- a/airbyte-ci/connectors/metadata_service/orchestrator/poetry.lock +++ b/airbyte-ci/connectors/metadata_service/orchestrator/poetry.lock @@ -4353,4 +4353,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "8c6fa8dc9750af9e32ac39bfb45a960721098d735bd81f5baf8134921127f16d" +content-hash = "8740e04661a4f3926650d1e905870688781b697a13851ab923817b705b7812fc" diff --git a/airbyte-ci/connectors/metadata_service/orchestrator/pyproject.toml b/airbyte-ci/connectors/metadata_service/orchestrator/pyproject.toml index 36eb30b42eff4..1b764780b4b17 100644 --- a/airbyte-ci/connectors/metadata_service/orchestrator/pyproject.toml +++ b/airbyte-ci/connectors/metadata_service/orchestrator/pyproject.toml @@ -30,6 +30,7 @@ sentry-sdk = "^1.28.1" semver = "^3.0.1" python-dateutil = "^2.8.2" humanize = "^4.7.0" +pendulum = "<3.0.0" [tool.poetry.group.dev.dependencies] From 25799bab198ffedb89bcc000cf6a65d10a244cbe Mon Sep 17 00:00:00 2001 From: Ella Rohm-Ensing Date: Tue, 9 Jan 2024 15:04:12 -0600 Subject: [PATCH 030/574] Use only required resources for sensor (#34056) --- .../metadata_service/orchestrator/orchestrator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-ci/connectors/metadata_service/orchestrator/orchestrator/__init__.py b/airbyte-ci/connectors/metadata_service/orchestrator/orchestrator/__init__.py index 81805b44a77d1..edec67baa4541 100644 --- a/airbyte-ci/connectors/metadata_service/orchestrator/orchestrator/__init__.py +++ b/airbyte-ci/connectors/metadata_service/orchestrator/orchestrator/__init__.py @@ -140,7 +140,7 @@ } SENSORS = [ - registry_updated_sensor(job=generate_registry_reports, resources_def=RESOURCES), + registry_updated_sensor(job=generate_registry_reports, resources_def=REGISTRY_RESOURCE_TREE), new_gcs_blobs_sensor( job=generate_oss_registry, resources_def=REGISTRY_ENTRY_RESOURCE_TREE, From 3de9dc99dca179568b918ddd0695f881eff80ca9 Mon Sep 17 00:00:00 2001 From: Ella Rohm-Ensing Date: Tue, 9 Jan 2024 18:34:57 -0600 Subject: [PATCH 031/574] [mitigation] pin airbyte ci version in master run (#34075) --- .github/workflows/format_check.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/format_check.yml b/.github/workflows/format_check.yml index 51ee6194b5f62..44ceac22e0b87 100644 --- a/.github/workflows/format_check.yml +++ b/.github/workflows/format_check.yml @@ -42,6 +42,9 @@ jobs: github_token: ${{ secrets.GH_PAT_MAINTENANCE_OCTAVIA }} tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }} subcommand: "format check all" + # Pin to a specific version of airbyte-ci to avoid transient failures + # Mentioned in issue https://github.com/airbytehq/airbyte/issues/34041 + airbyte_ci_binary_url: https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/2.14.1/airbyte-ci - name: Run airbyte-ci format check [PULL REQUEST] id: airbyte_ci_format_check_all_pr From b2902083910b904ee60bf4583b2221e2c1783253 Mon Sep 17 00:00:00 2001 From: Xiaohan Song Date: Tue, 9 Jan 2024 17:05:57 -0800 Subject: [PATCH 032/574] Add count in state message for incremental syncs (#33005) Co-authored-by: xiaohansong --- airbyte-cdk/java/airbyte-cdk/README.md | 1 + .../src/main/resources/version.properties | 2 +- .../DebeziumStateDecoratingIterator.java | 11 ++++++---- .../relationaldb/StateDecoratingIterator.java | 20 ++++++++++++++----- .../state/SourceStateIterator.java | 2 ++ .../StateDecoratingIteratorTest.java | 4 +++- .../integrations/debezium/CdcSourceTest.java | 2 ++ .../jdbc/test/JdbcSourceAcceptanceTest.java | 19 ++++++++++-------- .../connectors/source-mysql/build.gradle | 2 +- .../connectors/source-mysql/metadata.yaml | 2 +- .../MySqlInitialLoadGlobalStateManager.java | 3 ++- .../MySqlInitialLoadStateManager.java | 3 ++- .../source/mysql/CdcMysqlSourceTest.java | 15 ++++++++++++++ .../mysql/MySqlJdbcSourceAcceptanceTest.java | 8 +++++--- docs/integrations/sources/mysql.md | 3 ++- 15 files changed, 70 insertions(+), 27 deletions(-) diff --git a/airbyte-cdk/java/airbyte-cdk/README.md b/airbyte-cdk/java/airbyte-cdk/README.md index ce700502cdea0..6a26ce9905357 100644 --- a/airbyte-cdk/java/airbyte-cdk/README.md +++ b/airbyte-cdk/java/airbyte-cdk/README.md @@ -166,6 +166,7 @@ MavenLocal debugging steps: | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0.11.4 | 2024-01-09 | [\#33305](https://github.com/airbytehq/airbyte/pull/33305) | Source stats in incremental syncs | | 0.11.3 | 2023-01-09 | [\#33658](https://github.com/airbytehq/airbyte/pull/33658) | Always fail when debezium fails, even if it happened during the setup phase. | | 0.11.2 | 2024-01-09 | [\#33969](https://github.com/airbytehq/airbyte/pull/33969) | Destination state stats implementation | | 0.11.1 | 2024-01-04 | [\#33727](https://github.com/airbytehq/airbyte/pull/33727) | SSH bastion heartbeats for Destinations | diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties b/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties index 8b9cd41d8c02e..630a01b8fa3b3 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties @@ -1 +1 @@ -version=0.11.3 +version=0.11.4 diff --git a/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/debezium/internals/DebeziumStateDecoratingIterator.java b/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/debezium/internals/DebeziumStateDecoratingIterator.java index 6e98a6ca86ac3..29569515e999c 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/debezium/internals/DebeziumStateDecoratingIterator.java +++ b/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/debezium/internals/DebeziumStateDecoratingIterator.java @@ -12,6 +12,7 @@ import io.airbyte.cdk.integrations.debezium.internals.DebeziumPropertiesManager.DebeziumConnectorType; import io.airbyte.protocol.models.v0.AirbyteMessage; import io.airbyte.protocol.models.v0.AirbyteStateMessage; +import io.airbyte.protocol.models.v0.AirbyteStateStats; import io.airbyte.protocol.models.v0.ConfiguredAirbyteCatalog; import java.time.Duration; import java.time.Instant; @@ -144,7 +145,7 @@ protected AirbyteMessage computeNext() { if (cdcStateHandler.isCdcCheckpointEnabled() && sendCheckpointMessage) { LOGGER.info("Sending CDC checkpoint state message."); - final AirbyteMessage stateMessage = createStateMessage(checkpointOffsetToSend); + final AirbyteMessage stateMessage = createStateMessage(checkpointOffsetToSend, recordsLastSync); previousCheckpointOffset.clear(); previousCheckpointOffset.putAll(checkpointOffsetToSend); resetCheckpointValues(); @@ -182,7 +183,7 @@ protected AirbyteMessage computeNext() { } isSyncFinished = true; - return createStateMessage(offsetManager.read()); + return createStateMessage(offsetManager.read(), recordsLastSync); } /** @@ -201,7 +202,7 @@ private void resetCheckpointValues() { * * @return {@link AirbyteStateMessage} which includes offset and schema history if used. */ - private AirbyteMessage createStateMessage(final Map offset) { + private AirbyteMessage createStateMessage(final Map offset, final long recordCount) { if (trackSchemaHistory && schemaHistoryManager == null) { throw new RuntimeException("Schema History Tracking is true but manager is not initialised"); } @@ -209,7 +210,9 @@ private AirbyteMessage createStateMessage(final Map offset) { throw new RuntimeException("Offset can not be null"); } - return cdcStateHandler.saveState(offset, schemaHistoryManager != null ? schemaHistoryManager.read() : null); + final AirbyteMessage message = cdcStateHandler.saveState(offset, schemaHistoryManager != null ? schemaHistoryManager.read() : null); + message.getState().withSourceStats(new AirbyteStateStats().withRecordCount((double) recordCount)); + return message; } } diff --git a/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/source/relationaldb/StateDecoratingIterator.java b/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/source/relationaldb/StateDecoratingIterator.java index 5c8f7d638ebb1..667a0ceb8152f 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/source/relationaldb/StateDecoratingIterator.java +++ b/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/source/relationaldb/StateDecoratingIterator.java @@ -11,6 +11,7 @@ import io.airbyte.protocol.models.v0.AirbyteMessage; import io.airbyte.protocol.models.v0.AirbyteMessage.Type; import io.airbyte.protocol.models.v0.AirbyteStateMessage; +import io.airbyte.protocol.models.v0.AirbyteStateStats; import io.airbyte.protocol.models.v0.AirbyteStreamNameNamespacePair; import java.util.Iterator; import java.util.Objects; @@ -53,6 +54,8 @@ public class StateDecoratingIterator extends AbstractIterator im */ private final int stateEmissionFrequency; private int totalRecordCount = 0; + // In between each state message, recordCountInStateMessage will be reset to 0. + private int recordCountInStateMessage = 0; private boolean emitIntermediateState = false; private AirbyteMessage intermediateStateMessage = null; private boolean hasCaughtException = false; @@ -128,6 +131,7 @@ protected AirbyteMessage computeNext() { } totalRecordCount++; + recordCountInStateMessage++; // Use try-catch to catch Exception that could occur when connection to the database fails try { final AirbyteMessage message = messageIterator.next(); @@ -139,7 +143,7 @@ protected AirbyteMessage computeNext() { if (stateEmissionFrequency > 0 && !Objects.equals(currentMaxCursor, initialCursor) && messageIterator.hasNext()) { // Only create an intermediate state when it is not the first or last record message. // The last state message will be processed seperately. - intermediateStateMessage = createStateMessage(false, totalRecordCount); + intermediateStateMessage = createStateMessage(false, recordCountInStateMessage); } currentMaxCursor = cursorCandidate; currentMaxCursorRecordCount = 1L; @@ -164,7 +168,7 @@ protected AirbyteMessage computeNext() { return optionalIntermediateMessage.orElse(endOfData()); } } else if (!hasEmittedFinalState) { - return createStateMessage(true, totalRecordCount); + return createStateMessage(true, recordCountInStateMessage); } else { return endOfData(); } @@ -185,6 +189,7 @@ protected final Optional getIntermediateMessage() { if (emitIntermediateState && intermediateStateMessage != null) { final AirbyteMessage message = intermediateStateMessage; intermediateStateMessage = null; + recordCountInStateMessage = 0; emitIntermediateState = false; return Optional.of(message); } @@ -196,14 +201,15 @@ protected final Optional getIntermediateMessage() { * read up so far * * @param isFinalState marker for if the final state of the iterator has been reached - * @param totalRecordCount count of read messages + * @param recordCount count of read messages * @return AirbyteMessage which includes information on state of records read so far */ - public AirbyteMessage createStateMessage(final boolean isFinalState, final int totalRecordCount) { + public AirbyteMessage createStateMessage(final boolean isFinalState, final int recordCount) { final AirbyteStateMessage stateMessage = stateManager.updateAndEmit(pair, currentMaxCursor, currentMaxCursorRecordCount); final Optional cursorInfo = stateManager.getCursorInfo(pair); + // logging once every 100 messages to reduce log verbosity - if (totalRecordCount % 100 == 0) { + if (recordCount % 100 == 0) { LOGGER.info("State report for stream {} - original: {} = {} (count {}) -> latest: {} = {} (count {})", pair, cursorInfo.map(CursorInfo::getOriginalCursorField).orElse(null), @@ -213,6 +219,10 @@ public AirbyteMessage createStateMessage(final boolean isFinalState, final int t cursorInfo.map(CursorInfo::getCursor).orElse(null), cursorInfo.map(CursorInfo::getCursorRecordCount).orElse(null)); } + + if (stateMessage != null) { + stateMessage.withSourceStats(new AirbyteStateStats().withRecordCount((double) recordCount)); + } if (isFinalState) { hasEmittedFinalState = true; if (stateManager.getCursor(pair).isEmpty()) { diff --git a/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/source/relationaldb/state/SourceStateIterator.java b/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/source/relationaldb/state/SourceStateIterator.java index 3cea088be9c60..203244800b421 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/source/relationaldb/state/SourceStateIterator.java +++ b/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/source/relationaldb/state/SourceStateIterator.java @@ -63,6 +63,8 @@ protected AirbyteMessage computeNext() { } else if (!hasEmittedFinalState) { hasEmittedFinalState = true; final AirbyteStateMessage finalStateMessage = sourceStateIteratorManager.createFinalStateMessage(); + finalStateMessage.withSourceStats(new AirbyteStateStats().withRecordCount((double) recordCount)); + recordCount = 0L; return new AirbyteMessage() .withType(Type.STATE) .withState(finalStateMessage); diff --git a/airbyte-cdk/java/airbyte-cdk/db-sources/src/test/java/io/airbyte/cdk/integrations/source/relationaldb/StateDecoratingIteratorTest.java b/airbyte-cdk/java/airbyte-cdk/db-sources/src/test/java/io/airbyte/cdk/integrations/source/relationaldb/StateDecoratingIteratorTest.java index 8e6448b78d894..e2d64f8497482 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-sources/src/test/java/io/airbyte/cdk/integrations/source/relationaldb/StateDecoratingIteratorTest.java +++ b/airbyte-cdk/java/airbyte-cdk/db-sources/src/test/java/io/airbyte/cdk/integrations/source/relationaldb/StateDecoratingIteratorTest.java @@ -19,6 +19,7 @@ import io.airbyte.protocol.models.v0.AirbyteMessage.Type; import io.airbyte.protocol.models.v0.AirbyteRecordMessage; import io.airbyte.protocol.models.v0.AirbyteStateMessage; +import io.airbyte.protocol.models.v0.AirbyteStateStats; import io.airbyte.protocol.models.v0.AirbyteStreamNameNamespacePair; import java.sql.SQLException; import java.util.Collections; @@ -69,7 +70,8 @@ private static AirbyteMessage createStateMessage(final String recordValue) { return new AirbyteMessage() .withType(Type.STATE) .withState(new AirbyteStateMessage() - .withData(Jsons.jsonNode(ImmutableMap.of("cursor", recordValue)))); + .withData(Jsons.jsonNode(ImmutableMap.of("cursor", recordValue))) + .withSourceStats(new AirbyteStateStats().withRecordCount(1.0))); } private Iterator createExceptionIterator() { diff --git a/airbyte-cdk/java/airbyte-cdk/db-sources/src/testFixtures/java/io/airbyte/cdk/integrations/debezium/CdcSourceTest.java b/airbyte-cdk/java/airbyte-cdk/db-sources/src/testFixtures/java/io/airbyte/cdk/integrations/debezium/CdcSourceTest.java index a0ee71a226d0d..d05d54254ebeb 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-sources/src/testFixtures/java/io/airbyte/cdk/integrations/debezium/CdcSourceTest.java +++ b/airbyte-cdk/java/airbyte-cdk/db-sources/src/testFixtures/java/io/airbyte/cdk/integrations/debezium/CdcSourceTest.java @@ -142,6 +142,8 @@ protected ConfiguredAirbyteCatalog getConfiguredCatalog() { protected abstract void assertExpectedStateMessages(final List stateMessages); + protected abstract void assertExpectedStateMessagesWithTotalCount(final List stateMessages, final long totalRecordCount); + @BeforeEach protected void setup() { testdb = createTestDatabase(); diff --git a/airbyte-cdk/java/airbyte-cdk/db-sources/src/testFixtures/java/io/airbyte/cdk/integrations/source/jdbc/test/JdbcSourceAcceptanceTest.java b/airbyte-cdk/java/airbyte-cdk/db-sources/src/testFixtures/java/io/airbyte/cdk/integrations/source/jdbc/test/JdbcSourceAcceptanceTest.java index dda24e0dd2926..ae358d0f8e8de 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-sources/src/testFixtures/java/io/airbyte/cdk/integrations/source/jdbc/test/JdbcSourceAcceptanceTest.java +++ b/airbyte-cdk/java/airbyte-cdk/db-sources/src/testFixtures/java/io/airbyte/cdk/integrations/source/jdbc/test/JdbcSourceAcceptanceTest.java @@ -36,6 +36,7 @@ import io.airbyte.protocol.models.v0.AirbyteRecordMessage; import io.airbyte.protocol.models.v0.AirbyteStateMessage; import io.airbyte.protocol.models.v0.AirbyteStateMessage.AirbyteStateType; +import io.airbyte.protocol.models.v0.AirbyteStateStats; import io.airbyte.protocol.models.v0.AirbyteStream; import io.airbyte.protocol.models.v0.AirbyteStreamState; import io.airbyte.protocol.models.v0.CatalogHelpers; @@ -601,7 +602,7 @@ protected List getExpectedAirbyteMessagesSecondSync(final String .withCursorField(List.of(COL_ID)) .withCursor("5") .withCursorRecordCount(1L); - expectedMessages.addAll(createExpectedTestMessages(List.of(state))); + expectedMessages.addAll(createExpectedTestMessages(List.of(state), 2L)); return expectedMessages; } @@ -671,9 +672,9 @@ protected void testReadMultipleTablesIncrementally() throws Exception { .withCursorRecordCount(1L)); final List expectedMessagesFirstSync = new ArrayList<>(getTestMessages()); - expectedMessagesFirstSync.add(createStateMessage(expectedStateStreams1.get(0), expectedStateStreams1)); + expectedMessagesFirstSync.add(createStateMessage(expectedStateStreams1.get(0), expectedStateStreams1, 3L)); expectedMessagesFirstSync.addAll(secondStreamExpectedMessages); - expectedMessagesFirstSync.add(createStateMessage(expectedStateStreams2.get(1), expectedStateStreams2)); + expectedMessagesFirstSync.add(createStateMessage(expectedStateStreams2.get(1), expectedStateStreams2, 3L)); setEmittedAtToNull(actualMessagesFirstSync); @@ -854,7 +855,7 @@ protected void incrementalCursorCheck( final List expectedStreams = List.of(buildStreamState(airbyteStream, cursorField, endCursorValue)); final List expectedMessages = new ArrayList<>(expectedRecordMessages); - expectedMessages.addAll(createExpectedTestMessages(expectedStreams)); + expectedMessages.addAll(createExpectedTestMessages(expectedStreams, expectedRecordMessages.size())); assertEquals(expectedMessages.size(), actualMessages.size()); assertTrue(expectedMessages.containsAll(actualMessages)); @@ -934,7 +935,7 @@ protected List getTestMessages() { COL_UPDATED_AT, "2006-10-19"))))); } - protected List createExpectedTestMessages(final List states) { + protected List createExpectedTestMessages(final List states, final long numRecords) { return states.stream() .map(s -> new AirbyteMessage().withType(Type.STATE) .withState( @@ -942,7 +943,8 @@ protected List createExpectedTestMessages(final List legacyStates) { + protected AirbyteMessage createStateMessage(final DbStreamState dbStreamState, final List legacyStates, final long recordCount) { return new AirbyteMessage().withType(Type.STATE) .withState( new AirbyteStateMessage().withType(AirbyteStateType.STREAM) @@ -1070,7 +1072,8 @@ protected AirbyteMessage createStateMessage(final DbStreamState dbStreamState, f .withStreamDescriptor(new StreamDescriptor().withNamespace(dbStreamState.getStreamNamespace()) .withName(dbStreamState.getStreamName())) .withStreamState(Jsons.jsonNode(dbStreamState))) - .withData(Jsons.jsonNode(new DbState().withCdc(false).withStreams(legacyStates)))); + .withData(Jsons.jsonNode(new DbState().withCdc(false).withStreams(legacyStates))) + .withSourceStats(new AirbyteStateStats().withRecordCount((double) recordCount))); } protected List extractSpecificFieldFromCombinedMessages(final List messages, diff --git a/airbyte-integrations/connectors/source-mysql/build.gradle b/airbyte-integrations/connectors/source-mysql/build.gradle index 77f896ef719d9..77b7c8054490e 100644 --- a/airbyte-integrations/connectors/source-mysql/build.gradle +++ b/airbyte-integrations/connectors/source-mysql/build.gradle @@ -7,7 +7,7 @@ plugins { } airbyteJavaConnector { - cdkVersionRequired = '0.10.3' + cdkVersionRequired = '0.11.4' features = ['db-sources'] useLocalCdk = false } diff --git a/airbyte-integrations/connectors/source-mysql/metadata.yaml b/airbyte-integrations/connectors/source-mysql/metadata.yaml index 10430558ac71c..caaf7458309ba 100644 --- a/airbyte-integrations/connectors/source-mysql/metadata.yaml +++ b/airbyte-integrations/connectors/source-mysql/metadata.yaml @@ -9,7 +9,7 @@ data: connectorSubtype: database connectorType: source definitionId: 435bb9a5-7887-4809-aa58-28c27df0d7ad - dockerImageTag: 3.3.1 + dockerImageTag: 3.3.2 dockerRepository: airbyte/source-mysql documentationUrl: https://docs.airbyte.com/integrations/sources/mysql githubIssueLabel: source-mysql diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadGlobalStateManager.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadGlobalStateManager.java index 3393e68ae1245..e810d860e4c8f 100644 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadGlobalStateManager.java +++ b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadGlobalStateManager.java @@ -86,7 +86,8 @@ public void updatePrimaryKeyLoadState(final AirbyteStreamNameNamespacePair pair, } @Override - public AirbyteStateMessage createFinalStateMessage(final AirbyteStreamNameNamespacePair pair, final JsonNode streamStateForIncrementalRun) { + public AirbyteStateMessage createFinalStateMessage(final AirbyteStreamNameNamespacePair pair, + final JsonNode streamStateForIncrementalRun) { streamsThatHaveCompletedSnapshot.add(pair); final List streamStates = new ArrayList<>(); streamsThatHaveCompletedSnapshot.forEach(stream -> { diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadStateManager.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadStateManager.java index 7bb6a7b846ae3..be5cec5732948 100644 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadStateManager.java +++ b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadStateManager.java @@ -25,7 +25,8 @@ public interface MySqlInitialLoadStateManager { void updatePrimaryKeyLoadState(final AirbyteStreamNameNamespacePair pair, final PrimaryKeyLoadStatus pkLoadStatus); // Returns the final state message for the initial sync. - AirbyteStateMessage createFinalStateMessage(final AirbyteStreamNameNamespacePair pair, final JsonNode streamStateForIncrementalRun); + AirbyteStateMessage createFinalStateMessage(final AirbyteStreamNameNamespacePair pair, + final JsonNode streamStateForIncrementalRun); // Returns the previous state emitted, represented as a {@link PrimaryKeyLoadStatus} associated with // the stream. diff --git a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcMysqlSourceTest.java b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcMysqlSourceTest.java index e918e29e3da10..643acf6d0f72d 100644 --- a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcMysqlSourceTest.java +++ b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcMysqlSourceTest.java @@ -288,6 +288,15 @@ protected void assertExpectedStateMessages(final List state assertStateTypes(stateMessages, 4); } + @Override + protected void assertExpectedStateMessagesWithTotalCount(final List stateMessages, final long totalRecordCount) { + long actualRecordCount = 0L; + for (final AirbyteStateMessage message : stateMessages) { + actualRecordCount += message.getSourceStats().getRecordCount(); + } + assertEquals(actualRecordCount, totalRecordCount); + } + @Override protected void assertExpectedStateMessagesFromIncrementalSync(final List stateMessages) { assertEquals(1, stateMessages.size()); @@ -433,6 +442,7 @@ public void syncWouldWorkWithDBWithInvalidTimezone() throws Exception { assertExpectedRecords(new HashSet<>(MODEL_RECORDS), recordMessages); assertExpectedStateMessages(stateMessages); + assertExpectedStateMessagesWithTotalCount(stateMessages, 6); } @Test @@ -451,6 +461,7 @@ public void testCompositeIndexInitialLoad() throws Exception { final List stateMessages1 = extractStateMessages(actualRecords1); assertExpectedRecords(new HashSet<>(MODEL_RECORDS), recordMessages1); assertExpectedStateMessages(stateMessages1); + assertExpectedStateMessagesWithTotalCount(stateMessages1, 6); // Re-run the sync with state associated with record w/ id = 15 (second to last record). // We expect to read 2 records, since in the case of a composite PK we issue a >= query. @@ -514,6 +525,8 @@ public void testTwoStreamSync() throws Exception { final Set recordMessages1 = extractRecordMessages(actualRecords1); final List stateMessages1 = extractStateMessages(actualRecords1); assertEquals(13, stateMessages1.size()); + assertExpectedStateMessagesWithTotalCount(stateMessages1, 12); + JsonNode sharedState = null; StreamDescriptor firstStreamInState = null; for (int i = 0; i < stateMessages1.size(); i++) { @@ -582,6 +595,8 @@ public void testTwoStreamSync() throws Exception { final List stateMessages2 = extractStateMessages(actualRecords2); assertEquals(6, stateMessages2.size()); + // State was reset to the 7th; thus 5 remaining records were expected to be reloaded. + assertExpectedStateMessagesWithTotalCount(stateMessages2, 5); for (int i = 0; i < stateMessages2.size(); i++) { final AirbyteStateMessage stateMessage = stateMessages2.get(i); assertEquals(AirbyteStateType.GLOBAL, stateMessage.getType()); diff --git a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlJdbcSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlJdbcSourceAcceptanceTest.java index 50d81f0664e4e..d6597cd2b023c 100644 --- a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlJdbcSourceAcceptanceTest.java +++ b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlJdbcSourceAcceptanceTest.java @@ -38,6 +38,7 @@ import io.airbyte.protocol.models.v0.AirbyteRecordMessage; import io.airbyte.protocol.models.v0.AirbyteStateMessage; import io.airbyte.protocol.models.v0.AirbyteStateMessage.AirbyteStateType; +import io.airbyte.protocol.models.v0.AirbyteStateStats; import io.airbyte.protocol.models.v0.AirbyteStream; import io.airbyte.protocol.models.v0.AirbyteStreamState; import io.airbyte.protocol.models.v0.CatalogHelpers; @@ -402,7 +403,7 @@ protected List getExpectedAirbyteMessagesSecondSync(final String .withCursor("5") .withCursorRecordCount(1L); - expectedMessages.addAll(createExpectedTestMessages(List.of(state))); + expectedMessages.addAll(createExpectedTestMessages(List.of(state), 2L)); return expectedMessages; } @@ -477,14 +478,15 @@ protected AirbyteCatalog getCatalog(final String defaultNamespace) { // Override from parent class as we're no longer including the legacy Data field. @Override - protected List createExpectedTestMessages(final List states) { + protected List createExpectedTestMessages(final List states, final long numRecords) { return states.stream() .map(s -> new AirbyteMessage().withType(Type.STATE) .withState( new AirbyteStateMessage().withType(AirbyteStateType.STREAM) .withStream(new AirbyteStreamState() .withStreamDescriptor(new StreamDescriptor().withNamespace(s.getStreamNamespace()).withName(s.getStreamName())) - .withStreamState(Jsons.jsonNode(s))))) + .withStreamState(Jsons.jsonNode(s))) + .withSourceStats(new AirbyteStateStats().withRecordCount((double) numRecords)))) .collect( Collectors.toList()); } diff --git a/docs/integrations/sources/mysql.md b/docs/integrations/sources/mysql.md index 903833d8c3b70..37dbc1bfe7973 100644 --- a/docs/integrations/sources/mysql.md +++ b/docs/integrations/sources/mysql.md @@ -223,7 +223,8 @@ Any database or table encoding combination of charset and collation is supported | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------| -| 3.3.1 | 2024-01-03 | [33312](https://github.com/airbytehq/airbyte/pull/33312) | Adding count stats in AirbyteStateMessage | +| 3.3.2 | 2024-01-08 | [33005](https://github.com/airbytehq/airbyte/pull/33005) | Adding count stats for incremental sync in AirbyteStateMessage +| 3.3.1 | 2024-01-03 | [33312](https://github.com/airbytehq/airbyte/pull/33312) | Adding count stats in AirbyteStateMessage | | 3.3.0 | 2023-12-19 | [33436](https://github.com/airbytehq/airbyte/pull/33436) | Remove LEGACY state flag | | 3.2.4 | 2023-12-12 | [33356](https://github.com/airbytehq/airbyte/pull/33210) | Support for better debugging tools.. | | 3.2.3 | 2023-12-08 | [33210](https://github.com/airbytehq/airbyte/pull/33210) | Update MySql driver property value for zero date handling. | From 8305d05e52d374b1d3b22462812dd7a5e5467a31 Mon Sep 17 00:00:00 2001 From: Anton Karpets Date: Wed, 10 Jan 2024 12:44:58 +0200 Subject: [PATCH 033/574] =?UTF-8?q?=E2=9C=A8Airbyte=20CDK:=20add=20POST=20?= =?UTF-8?q?method=20to=20HttpMocker=20(#34001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: maxi297 --- .../airbyte_cdk/test/mock_http/mocker.py | 67 +++++++++----- .../airbyte_cdk/test/mock_http/request.py | 42 +++++++-- .../unit_tests/test/mock_http/test_mocker.py | 60 ++++++++++--- .../unit_tests/test/mock_http/test_request.py | 90 +++++++++++++++---- 4 files changed, 205 insertions(+), 54 deletions(-) diff --git a/airbyte-cdk/python/airbyte_cdk/test/mock_http/mocker.py b/airbyte-cdk/python/airbyte_cdk/test/mock_http/mocker.py index 5812c37132b52..c2ca734047ac0 100644 --- a/airbyte-cdk/python/airbyte_cdk/test/mock_http/mocker.py +++ b/airbyte-cdk/python/airbyte_cdk/test/mock_http/mocker.py @@ -2,6 +2,7 @@ import contextlib import functools +from enum import Enum from types import TracebackType from typing import Callable, List, Optional, Union @@ -9,9 +10,26 @@ from airbyte_cdk.test.mock_http import HttpRequest, HttpRequestMatcher, HttpResponse +class SupportedHttpMethods(str, Enum): + GET = "get" + POST = "post" + + class HttpMocker(contextlib.ContextDecorator): """ - WARNING: This implementation only works if the lib used to perform HTTP requests is `requests` + WARNING 1: This implementation only works if the lib used to perform HTTP requests is `requests`. + + WARNING 2: Given multiple requests that are not mutually exclusive, the request will match the first one. This can happen in scenarios + where the same request is added twice (in which case there will always be an exception because we will never match the second + request) or in a case like this: + ``` + http_mocker.get(HttpRequest(_A_URL, headers={"less_granular": "1", "more_granular": "2"}), <...>) + http_mocker.get(HttpRequest(_A_URL, headers={"less_granular": "1"}), <...>) + requests.get(_A_URL, headers={"less_granular": "1", "more_granular": "2"}) + ``` + In the example above, the matcher would match the second mock as requests_mock iterate over the matcher in reverse order (see + https://github.com/jamielennox/requests-mock/blob/c06f124a33f56e9f03840518e19669ba41b93202/requests_mock/adapter.py#L246) even + though the request sent is a better match for the first `http_mocker.get`. """ def __init__(self) -> None: @@ -30,35 +48,34 @@ def _validate_all_matchers_called(self) -> None: if not matcher.has_expected_match_count(): raise ValueError(f"Invalid number of matches for `{matcher}`") - def get(self, request: HttpRequest, responses: Union[HttpResponse, List[HttpResponse]]) -> None: - """ - WARNING: Given multiple requests that are not mutually exclusive, the request will match the first one. This can happen in scenarios - where the same request is added twice (in which case there will always be an exception because we will never match the second - request) or in a case like this: - ``` - http_mocker.get(HttpRequest(_A_URL, headers={"less_granular": "1", "more_granular": "2"}), <...>) - http_mocker.get(HttpRequest(_A_URL, headers={"less_granular": "1"}), <...>) - requests.get(_A_URL, headers={"less_granular": "1", "more_granular": "2"}) - ``` - In the example above, the matcher would match the second mock as requests_mock iterate over the matcher in reverse order (see - https://github.com/jamielennox/requests-mock/blob/c06f124a33f56e9f03840518e19669ba41b93202/requests_mock/adapter.py#L246) even - though the request sent is a better match for the first `http_mocker.get`. - """ + def _mock_request_method( + self, method: SupportedHttpMethods, request: HttpRequest, responses: Union[HttpResponse, List[HttpResponse]] + ) -> None: if isinstance(responses, HttpResponse): responses = [responses] matcher = HttpRequestMatcher(request, len(responses)) self._matchers.append(matcher) - self._mocker.get( + + getattr(self._mocker, method)( requests_mock.ANY, additional_matcher=self._matches_wrapper(matcher), response_list=[{"text": response.body, "status_code": response.status_code} for response in responses], ) - def _matches_wrapper(self, matcher: HttpRequestMatcher) -> Callable[[requests_mock.request._RequestObjectProxy], bool]: + def get(self, request: HttpRequest, responses: Union[HttpResponse, List[HttpResponse]]) -> None: + self._mock_request_method(SupportedHttpMethods.GET, request, responses) + + def post(self, request: HttpRequest, responses: Union[HttpResponse, List[HttpResponse]]) -> None: + self._mock_request_method(SupportedHttpMethods.POST, request, responses) + + @staticmethod + def _matches_wrapper(matcher: HttpRequestMatcher) -> Callable[[requests_mock.request._RequestObjectProxy], bool]: def matches(requests_mock_request: requests_mock.request._RequestObjectProxy) -> bool: # query_params are provided as part of `requests_mock_request.url` - http_request = HttpRequest(requests_mock_request.url, query_params={}, headers=requests_mock_request.headers) + http_request = HttpRequest( + requests_mock_request.url, query_params={}, headers=requests_mock_request.headers, body=requests_mock_request.body + ) return matcher.matches(http_request) return matches @@ -70,7 +87,8 @@ def assert_number_of_calls(self, request: HttpRequest, number_of_calls: int) -> assert corresponding_matchers[0].actual_number_of_matches == number_of_calls - def __call__(self, f): # type: ignore # trying to type that using callables provides the error `incompatible with return type "_F" in supertype "ContextDecorator"` + # trying to type that using callables provides the error `incompatible with return type "_F" in supertype "ContextDecorator"` + def __call__(self, f): # type: ignore @functools.wraps(f) def wrapper(*args, **kwargs): # type: ignore # this is a very generic wrapper that does not need to be typed with self: @@ -82,18 +100,21 @@ def wrapper(*args, **kwargs): # type: ignore # this is a very generic wrapper except requests_mock.NoMockAddress as no_mock_exception: matchers_as_string = "\n\t".join(map(lambda matcher: str(matcher.request), self._matchers)) raise ValueError( - f"No matcher matches {no_mock_exception.args[0]} with headers `{no_mock_exception.request.headers}`. Matchers currently configured are:\n\t{matchers_as_string}" + f"No matcher matches {no_mock_exception.args[0]} with headers `{no_mock_exception.request.headers}` " + f"and body `{no_mock_exception.request.body}`. " + f"Matchers currently configured are:\n\t{matchers_as_string}." ) from no_mock_exception except AssertionError as test_assertion: assertion_error = test_assertion - # We validate the matchers before raising the assertion error because we want to show the tester if a HTTP request wasn't + # We validate the matchers before raising the assertion error because we want to show the tester if an HTTP request wasn't # mocked correctly try: self._validate_all_matchers_called() except ValueError as http_mocker_exception: - # This seems useless as it catches ValueError and raises ValueError but without this, the prevaling error message in - # the output is the function call that failed the assertion, whereas raising `ValueError(http_mocker_exception)` like we do here provides additional context for the exception. + # This seems useless as it catches ValueError and raises ValueError but without this, the prevailing error message in + # the output is the function call that failed the assertion, whereas raising `ValueError(http_mocker_exception)` + # like we do here provides additional context for the exception. raise ValueError(http_mocker_exception) from None if assertion_error: raise assertion_error diff --git a/airbyte-cdk/python/airbyte_cdk/test/mock_http/request.py b/airbyte-cdk/python/airbyte_cdk/test/mock_http/request.py index 243701280de0f..a2b6bdb9430a1 100644 --- a/airbyte-cdk/python/airbyte_cdk/test/mock_http/request.py +++ b/airbyte-cdk/python/airbyte_cdk/test/mock_http/request.py @@ -1,5 +1,6 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. +import json from typing import Any, List, Mapping, Optional, Union from urllib.parse import parse_qs, urlencode, urlparse @@ -16,6 +17,7 @@ def __init__( url: str, query_params: Optional[Union[str, Mapping[str, Union[str, List[str]]]]] = None, headers: Optional[Mapping[str, str]] = None, + body: Optional[Union[str, bytes, Mapping[str, Any]]] = None, ) -> None: self._parsed_url = urlparse(url) self._query_params = query_params @@ -25,31 +27,61 @@ def __init__( raise ValueError("If query params are provided as part of the url, `query_params` should be empty") self._headers = headers or {} + self._body = body - def _encode_qs(self, query_params: Union[str, Mapping[str, Union[str, List[str]]]]) -> str: + @staticmethod + def _encode_qs(query_params: Union[str, Mapping[str, Union[str, List[str]]]]) -> str: if isinstance(query_params, str): return query_params return urlencode(query_params, doseq=True) def matches(self, other: Any) -> bool: """ - Note that headers only need to be a subset of `other` in order to match + If the body of any request is a Mapping, we compare as Mappings which means that the order is not important. + If the body is a string, encoding ISO-8859-1 will be assumed + Headers only need to be a subset of `other` in order to match """ if isinstance(other, HttpRequest): + # if `other` is a mapping, we match as an object and formatting is not considers + if isinstance(self._body, Mapping) or isinstance(other._body, Mapping): + body_match = self._to_mapping(self._body) == self._to_mapping(other._body) + else: + body_match = self._to_bytes(self._body) == self._to_bytes(other._body) + return ( self._parsed_url.scheme == other._parsed_url.scheme and self._parsed_url.hostname == other._parsed_url.hostname and self._parsed_url.path == other._parsed_url.path and ( - ANY_QUERY_PARAMS in [self._query_params, other._query_params] + ANY_QUERY_PARAMS in (self._query_params, other._query_params) or parse_qs(self._parsed_url.query) == parse_qs(other._parsed_url.query) ) and _is_subdict(other._headers, self._headers) + and body_match ) return False + @staticmethod + def _to_mapping(body: Optional[Union[str, bytes, Mapping[str, Any]]]) -> Optional[Mapping[str, Any]]: + if isinstance(body, Mapping): + return body + elif isinstance(body, bytes): + return json.loads(body.decode()) # type: ignore # assumes return type of Mapping[str, Any] + elif isinstance(body, str): + return json.loads(body) # type: ignore # assumes return type of Mapping[str, Any] + return None + + @staticmethod + def _to_bytes(body: Optional[Union[str, bytes]]) -> bytes: + if isinstance(body, bytes): + return body + elif isinstance(body, str): + # `ISO-8859-1` is the default encoding used by requests + return body.encode("ISO-8859-1") + return b"" + def __str__(self) -> str: - return f"{self._parsed_url} with headers {self._headers})" + return f"{self._parsed_url} with headers {self._headers} and body {self._body!r})" def __repr__(self) -> str: - return f"HttpRequest(request={self._parsed_url}, headers={self._headers})" + return f"HttpRequest(request={self._parsed_url}, headers={self._headers}, body={self._body!r})" diff --git a/airbyte-cdk/python/unit_tests/test/mock_http/test_mocker.py b/airbyte-cdk/python/unit_tests/test/mock_http/test_mocker.py index e270bb4ef3781..cec689566e904 100644 --- a/airbyte-cdk/python/unit_tests/test/mock_http/test_mocker.py +++ b/airbyte-cdk/python/unit_tests/test/mock_http/test_mocker.py @@ -10,51 +10,89 @@ # see https://github.com/psf/requests/blob/0b4d494192de489701d3a2e32acef8fb5d3f042e/src/requests/models.py#L424-L429 _A_URL = "http://test.com/" _ANOTHER_URL = "http://another-test.com/" -_A_BODY = "a body" -_ANOTHER_BODY = "another body" +_A_RESPONSE_BODY = "a body" +_ANOTHER_RESPONSE_BODY = "another body" _A_RESPONSE = HttpResponse("any response") _SOME_QUERY_PARAMS = {"q1": "query value"} _SOME_HEADERS = {"h1": "header value"} +_SOME_REQUEST_BODY_MAPPING = {"first_field": "first_value", "second_field": 2} +_SOME_REQUEST_BODY_STR = "some_request_body" class HttpMockerTest(TestCase): @HttpMocker() - def test_given_request_match_when_decorate_then_return_response(self, http_mocker): + def test_given_get_request_match_when_decorate_then_return_response(self, http_mocker): http_mocker.get( HttpRequest(_A_URL, _SOME_QUERY_PARAMS, _SOME_HEADERS), - HttpResponse(_A_BODY, 474), + HttpResponse(_A_RESPONSE_BODY, 474), ) response = requests.get(_A_URL, params=_SOME_QUERY_PARAMS, headers=_SOME_HEADERS) - assert response.text == _A_BODY + assert response.text == _A_RESPONSE_BODY assert response.status_code == 474 @HttpMocker() - def test_given_multiple_responses_when_decorate_then_return_response(self, http_mocker): + def test_given_loose_headers_matching_when_decorate_then_match(self, http_mocker): http_mocker.get( HttpRequest(_A_URL, _SOME_QUERY_PARAMS, _SOME_HEADERS), - [HttpResponse(_A_BODY, 1), HttpResponse(_ANOTHER_BODY, 2)], + HttpResponse(_A_RESPONSE_BODY, 474), + ) + + requests.get(_A_URL, params=_SOME_QUERY_PARAMS, headers=_SOME_HEADERS | {"more strict query param key": "any value"}) + + @HttpMocker() + def test_given_post_request_match_when_decorate_then_return_response(self, http_mocker): + http_mocker.post( + HttpRequest(_A_URL, _SOME_QUERY_PARAMS, _SOME_HEADERS, _SOME_REQUEST_BODY_STR), + HttpResponse(_A_RESPONSE_BODY, 474), + ) + + response = requests.post(_A_URL, params=_SOME_QUERY_PARAMS, headers=_SOME_HEADERS, data=_SOME_REQUEST_BODY_STR) + + assert response.text == _A_RESPONSE_BODY + assert response.status_code == 474 + + @HttpMocker() + def test_given_multiple_responses_when_decorate_get_request_then_return_response(self, http_mocker): + http_mocker.get( + HttpRequest(_A_URL, _SOME_QUERY_PARAMS, _SOME_HEADERS), + [HttpResponse(_A_RESPONSE_BODY, 1), HttpResponse(_ANOTHER_RESPONSE_BODY, 2)], ) first_response = requests.get(_A_URL, params=_SOME_QUERY_PARAMS, headers=_SOME_HEADERS) second_response = requests.get(_A_URL, params=_SOME_QUERY_PARAMS, headers=_SOME_HEADERS) - assert first_response.text == _A_BODY + assert first_response.text == _A_RESPONSE_BODY + assert first_response.status_code == 1 + assert second_response.text == _ANOTHER_RESPONSE_BODY + assert second_response.status_code == 2 + + @HttpMocker() + def test_given_multiple_responses_when_decorate_post_request_then_return_response(self, http_mocker): + http_mocker.post( + HttpRequest(_A_URL, _SOME_QUERY_PARAMS, _SOME_HEADERS, _SOME_REQUEST_BODY_STR), + [HttpResponse(_A_RESPONSE_BODY, 1), HttpResponse(_ANOTHER_RESPONSE_BODY, 2)], + ) + + first_response = requests.post(_A_URL, params=_SOME_QUERY_PARAMS, headers=_SOME_HEADERS, data=_SOME_REQUEST_BODY_STR) + second_response = requests.post(_A_URL, params=_SOME_QUERY_PARAMS, headers=_SOME_HEADERS, data=_SOME_REQUEST_BODY_STR) + + assert first_response.text == _A_RESPONSE_BODY assert first_response.status_code == 1 - assert second_response.text == _ANOTHER_BODY + assert second_response.text == _ANOTHER_RESPONSE_BODY assert second_response.status_code == 2 @HttpMocker() def test_given_more_requests_than_responses_when_decorate_then_raise_error(self, http_mocker): http_mocker.get( HttpRequest(_A_URL, _SOME_QUERY_PARAMS, _SOME_HEADERS), - [HttpResponse(_A_BODY, 1), HttpResponse(_ANOTHER_BODY, 2)], + [HttpResponse(_A_RESPONSE_BODY, 1), HttpResponse(_ANOTHER_RESPONSE_BODY, 2)], ) last_response = [requests.get(_A_URL, params=_SOME_QUERY_PARAMS, headers=_SOME_HEADERS) for _ in range(10)][-1] - assert last_response.text == _ANOTHER_BODY + assert last_response.text == _ANOTHER_RESPONSE_BODY assert last_response.status_code == 2 @HttpMocker() diff --git a/airbyte-cdk/python/unit_tests/test/mock_http/test_request.py b/airbyte-cdk/python/unit_tests/test/mock_http/test_request.py index e724894b40023..a5a94ea05580d 100644 --- a/airbyte-cdk/python/unit_tests/test/mock_http/test_request.py +++ b/airbyte-cdk/python/unit_tests/test/mock_http/test_request.py @@ -18,40 +18,100 @@ def test_given_query_params_in_url_and_also_provided_then_raise_error(self): def test_given_same_url_query_params_and_subset_headers_when_matches_then_return_true(self): request_to_match = HttpRequest("mock://test.com/path", {"a_query_param": "q1"}, {"first_header": "h1"}) - request_received = HttpRequest("mock://test.com/path", {"a_query_param": "q1"}, {"first_header": "h1", "second_header": "h2"}) - assert request_received.matches(request_to_match) + actual_request = HttpRequest("mock://test.com/path", {"a_query_param": "q1"}, {"first_header": "h1", "second_header": "h2"}) + assert actual_request.matches(request_to_match) def test_given_url_differs_when_matches_then_return_false(self): assert not HttpRequest("mock://test.com/another_path").matches(HttpRequest("mock://test.com/path")) def test_given_query_params_differs_when_matches_then_return_false(self): request_to_match = HttpRequest("mock://test.com/path", {"a_query_param": "q1"}) - request_received = HttpRequest("mock://test.com/path", {"another_query_param": "q2"}) - assert not request_received.matches(request_to_match) + actual_request = HttpRequest("mock://test.com/path", {"another_query_param": "q2"}) + assert not actual_request.matches(request_to_match) def test_given_query_params_is_subset_differs_when_matches_then_return_false(self): request_to_match = HttpRequest("mock://test.com/path", {"a_query_param": "q1"}) - request_received = HttpRequest("mock://test.com/path", {"a_query_param": "q1", "another_query_param": "q2"}) - assert not request_received.matches(request_to_match) + actual_request = HttpRequest("mock://test.com/path", {"a_query_param": "q1", "another_query_param": "q2"}) + assert not actual_request.matches(request_to_match) def test_given_headers_is_subset_differs_when_matches_then_return_true(self): request_to_match = HttpRequest("mock://test.com/path", headers={"first_header": "h1"}) - request_received = HttpRequest("mock://test.com/path", headers={"first_header": "h1", "second_header": "h2"}) - assert request_received.matches(request_to_match) + actual_request = HttpRequest("mock://test.com/path", headers={"first_header": "h1", "second_header": "h2"}) + assert actual_request.matches(request_to_match) def test_given_headers_value_does_not_match_differs_when_matches_then_return_false(self): request_to_match = HttpRequest("mock://test.com/path", headers={"first_header": "h1"}) - request_received = HttpRequest("mock://test.com/path", headers={"first_header": "value does not match"}) - assert not request_received.matches(request_to_match) + actual_request = HttpRequest("mock://test.com/path", headers={"first_header": "value does not match"}) + assert not actual_request.matches(request_to_match) + + def test_given_same_body_mappings_value_when_matches_then_return_true(self): + request_to_match = HttpRequest("mock://test.com/path", body={"first_field": "first_value", "second_field": 2}) + actual_request = HttpRequest("mock://test.com/path", body={"first_field": "first_value", "second_field": 2}) + assert actual_request.matches(request_to_match) + + def test_given_bodies_are_mapping_and_differs_when_matches_then_return_false(self): + request_to_match = HttpRequest("mock://test.com/path", body={"first_field": "first_value"}) + actual_request = HttpRequest("mock://test.com/path", body={"first_field": "value does not match"}) + assert not actual_request.matches(request_to_match) + + def test_given_same_mapping_and_bytes_when_matches_then_return_true(self): + request_to_match = HttpRequest("mock://test.com/path", body={"first_field": "first_value"}) + actual_request = HttpRequest("mock://test.com/path", body=b'{"first_field": "first_value"}') + assert actual_request.matches(request_to_match) + + def test_given_different_mapping_and_bytes_when_matches_then_return_false(self): + request_to_match = HttpRequest("mock://test.com/path", body={"first_field": "first_value"}) + actual_request = HttpRequest("mock://test.com/path", body=b'{"first_field": "another value"}') + assert not actual_request.matches(request_to_match) + + def test_given_same_mapping_and_str_when_matches_then_return_true(self): + request_to_match = HttpRequest("mock://test.com/path", body={"first_field": "first_value"}) + actual_request = HttpRequest("mock://test.com/path", body='{"first_field": "first_value"}') + assert actual_request.matches(request_to_match) + + def test_given_different_mapping_and_str_when_matches_then_return_false(self): + request_to_match = HttpRequest("mock://test.com/path", body={"first_field": "first_value"}) + actual_request = HttpRequest("mock://test.com/path", body='{"first_field": "another value"}') + assert not actual_request.matches(request_to_match) + + def test_given_same_bytes_and_mapping_when_matches_then_return_true(self): + request_to_match = HttpRequest("mock://test.com/path", body=b'{"first_field": "first_value"}') + actual_request = HttpRequest("mock://test.com/path", body={"first_field": "first_value"}) + assert actual_request.matches(request_to_match) + + def test_given_different_bytes_and_mapping_when_matches_then_return_false(self): + request_to_match = HttpRequest("mock://test.com/path", body=b'{"first_field": "first_value"}') + actual_request = HttpRequest("mock://test.com/path", body={"first_field": "another value"}) + assert not actual_request.matches(request_to_match) + + def test_given_same_str_and_mapping_when_matches_then_return_true(self): + request_to_match = HttpRequest("mock://test.com/path", body='{"first_field": "first_value"}') + actual_request = HttpRequest("mock://test.com/path", body={"first_field": "first_value"}) + assert actual_request.matches(request_to_match) + + def test_given_different_str_and_mapping_when_matches_then_return_false(self): + request_to_match = HttpRequest("mock://test.com/path", body='{"first_field": "first_value"}') + actual_request = HttpRequest("mock://test.com/path", body={"first_field": "another value"}) + assert not actual_request.matches(request_to_match) + + def test_given_same_body_str_value_when_matches_then_return_true(self): + request_to_match = HttpRequest("mock://test.com/path", body="some_request_body") + actual_request = HttpRequest("mock://test.com/path", body="some_request_body") + assert actual_request.matches(request_to_match) + + def test_given_body_str_value_differs_when_matches_then_return_false(self): + request_to_match = HttpRequest("mock://test.com/path", body="some_request_body") + actual_request = HttpRequest("mock://test.com/path", body="another_request_body") + assert not actual_request.matches(request_to_match) def test_given_any_matcher_for_query_param_when_matches_then_return_true(self): request_to_match = HttpRequest("mock://test.com/path", {"a_query_param": "q1"}) - request_received = HttpRequest("mock://test.com/path", ANY_QUERY_PARAMS) + actual_request = HttpRequest("mock://test.com/path", ANY_QUERY_PARAMS) - assert request_received.matches(request_to_match) - assert request_to_match.matches(request_received) + assert actual_request.matches(request_to_match) + assert request_to_match.matches(actual_request) def test_given_any_matcher_for_both_when_matches_then_return_true(self): request_to_match = HttpRequest("mock://test.com/path", ANY_QUERY_PARAMS) - request_received = HttpRequest("mock://test.com/path", ANY_QUERY_PARAMS) - assert request_received.matches(request_to_match) + actual_request = HttpRequest("mock://test.com/path", ANY_QUERY_PARAMS) + assert actual_request.matches(request_to_match) From c084212f5bdbc6d8f360e747a5a607cdbe148ff9 Mon Sep 17 00:00:00 2001 From: askarpets Date: Wed, 10 Jan 2024 10:51:33 +0000 Subject: [PATCH 034/574] =?UTF-8?q?=F0=9F=A4=96=20Bump=20patch=20version?= =?UTF-8?q?=20of=20Python=20CDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airbyte-cdk/python/.bumpversion.cfg | 2 +- airbyte-cdk/python/CHANGELOG.md | 3 +++ airbyte-cdk/python/Dockerfile | 4 ++-- airbyte-cdk/python/setup.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/airbyte-cdk/python/.bumpversion.cfg b/airbyte-cdk/python/.bumpversion.cfg index 1ff825346ad44..dabdb23d3791f 100644 --- a/airbyte-cdk/python/.bumpversion.cfg +++ b/airbyte-cdk/python/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.58.3 +current_version = 0.58.4 commit = False [bumpversion:file:setup.py] diff --git a/airbyte-cdk/python/CHANGELOG.md b/airbyte-cdk/python/CHANGELOG.md index a4fbf2da64af6..4e4a9b6f5125c 100644 --- a/airbyte-cdk/python/CHANGELOG.md +++ b/airbyte-cdk/python/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 0.58.4 +Add POST method to HttpMocker + ## 0.58.3 fix declarative oauth initialization diff --git a/airbyte-cdk/python/Dockerfile b/airbyte-cdk/python/Dockerfile index 313adf12dfbce..688955e48c5f7 100644 --- a/airbyte-cdk/python/Dockerfile +++ b/airbyte-cdk/python/Dockerfile @@ -10,7 +10,7 @@ RUN apk --no-cache upgrade \ && apk --no-cache add tzdata build-base # install airbyte-cdk -RUN pip install --prefix=/install airbyte-cdk==0.58.3 +RUN pip install --prefix=/install airbyte-cdk==0.58.4 # build a clean environment FROM base @@ -32,5 +32,5 @@ ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] # needs to be the same as CDK -LABEL io.airbyte.version=0.58.3 +LABEL io.airbyte.version=0.58.4 LABEL io.airbyte.name=airbyte/source-declarative-manifest diff --git a/airbyte-cdk/python/setup.py b/airbyte-cdk/python/setup.py index 5d1cb1cadedff..3a2ed5f1a2b67 100644 --- a/airbyte-cdk/python/setup.py +++ b/airbyte-cdk/python/setup.py @@ -36,7 +36,7 @@ name="airbyte-cdk", # The version of the airbyte-cdk package is used at runtime to validate manifests. That validation must be # updated if our semver format changes such as using release candidate versions. - version="0.58.3", + version="0.58.4", description="A framework for writing Airbyte Connectors.", long_description=README, long_description_content_type="text/markdown", From 1ac50295bfb24347f4021d805b2ffad71dfe34ef Mon Sep 17 00:00:00 2001 From: Artem Inzhyyants <36314070+artem1205@users.noreply.github.com> Date: Wed, 10 Jan 2024 12:20:08 +0100 Subject: [PATCH 035/574] =?UTF-8?q?=F0=9F=9A=A8=F0=9F=9A=A8=20Source=20JIR?= =?UTF-8?q?A:=20Save=20state=20for=20Boards=20Issues=20per=20board=20(#337?= =?UTF-8?q?15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration_tests/abnormal_state.json | 10 +++- .../connectors/source-jira/metadata.yaml | 10 +++- .../source-jira/source_jira/streams.py | 59 +++++++++++++++---- .../source-jira/unit_tests/test_streams.py | 8 +-- docs/integrations/sources/jira-migrations.md | 27 +++++++++ docs/integrations/sources/jira.md | 1 + 6 files changed, 97 insertions(+), 18 deletions(-) create mode 100644 docs/integrations/sources/jira-migrations.md diff --git a/airbyte-integrations/connectors/source-jira/integration_tests/abnormal_state.json b/airbyte-integrations/connectors/source-jira/integration_tests/abnormal_state.json index af6f2b9ee0502..727d1fa0207e5 100644 --- a/airbyte-integrations/connectors/source-jira/integration_tests/abnormal_state.json +++ b/airbyte-integrations/connectors/source-jira/integration_tests/abnormal_state.json @@ -6,7 +6,15 @@ "name": "board_issues" }, "stream_state": { - "updated": "2122-01-01T00:00:00Z" + "1": { + "updated": "2122-01-01T00:00:00Z" + }, + "17": { + "updated": "2122-01-01T00:00:00Z" + }, + "58": { + "updated": "2122-01-01T00:00:00Z" + } } } }, diff --git a/airbyte-integrations/connectors/source-jira/metadata.yaml b/airbyte-integrations/connectors/source-jira/metadata.yaml index b4424d655e13e..a46d1dad743d6 100644 --- a/airbyte-integrations/connectors/source-jira/metadata.yaml +++ b/airbyte-integrations/connectors/source-jira/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: 68e63de2-bb83-4c7e-93fa-a8a9051e3993 - dockerImageTag: 0.14.1 + dockerImageTag: 1.0.0 dockerRepository: airbyte/source-jira documentationUrl: https://docs.airbyte.com/integrations/sources/jira githubIssueLabel: source-jira @@ -24,6 +24,14 @@ data: oss: enabled: true releaseStage: generally_available + releases: + breakingChanges: + 1.0.0: + message: "Stream state will be saved for every board in stream `Boards Issues`. Customers who use stream `Board Issues` in Incremental Sync mode must take action with their connections." + upgradeDeadline: "2024-01-25" + scopedImpact: + - scopeType: stream + impactedScopes: ["board_issues"] suggestedStreams: streams: - issues diff --git a/airbyte-integrations/connectors/source-jira/source_jira/streams.py b/airbyte-integrations/connectors/source-jira/source_jira/streams.py index a93b2fbf3bbe8..542ea67ea59d8 100644 --- a/airbyte-integrations/connectors/source-jira/source_jira/streams.py +++ b/airbyte-integrations/connectors/source-jira/source_jira/streams.py @@ -246,7 +246,7 @@ def transform(self, record: MutableMapping[str, Any], stream_slice: Mapping[str, return record -class BoardIssues(IncrementalJiraStream): +class BoardIssues(StartDateJiraStream): """ https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-issue-get """ @@ -254,9 +254,11 @@ class BoardIssues(IncrementalJiraStream): cursor_field = "updated" extract_field = "issues" api_v1 = True + state_checkpoint_interval = 50 # default page size is 50 def __init__(self, **kwargs): super().__init__(**kwargs) + self._starting_point_cache = {} self.boards_stream = Boards(authenticator=self.authenticator, domain=self._domain, projects=self._projects) def path(self, stream_slice: Mapping[str, Any], **kwargs) -> str: @@ -270,11 +272,17 @@ def request_params( ) -> MutableMapping[str, Any]: params = super().request_params(stream_state=stream_state, stream_slice=stream_slice, next_page_token=next_page_token) params["fields"] = ["key", "created", "updated"] - jql = self.jql_compare_date(stream_state) + jql = self.jql_compare_date(stream_state, stream_slice) if jql: params["jql"] = jql return params + def jql_compare_date(self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any]) -> Optional[str]: + compare_date = self.get_starting_point(stream_state, stream_slice) + if compare_date: + compare_date = compare_date.strftime("%Y/%m/%d %H:%M") + return f"{self.cursor_field} >= '{compare_date}'" + def _is_board_error(self, response): """Check if board has error and should be skipped""" if response.status_code == 500: @@ -288,17 +296,44 @@ def should_retry(self, response: requests.Response) -> bool: # for all other HTTP errors the default handling is applied return super().should_retry(response) + def stream_slices(self, **kwargs) -> Iterable[Optional[Mapping[str, Any]]]: + yield from read_full_refresh(self.boards_stream) + def read_records(self, stream_slice: Optional[Mapping[str, Any]] = None, **kwargs) -> Iterable[Mapping[str, Any]]: - for board in read_full_refresh(self.boards_stream): - try: - yield from super().read_records(stream_slice={"board_id": board["id"]}, **kwargs) - except HTTPError as e: - if self._is_board_error(e.response): - # Wrong board is skipped - self.logger.warning(f"Board {board['id']} has no columns with a mapped status. Skipping.") - continue - else: - raise + try: + yield from super().read_records(stream_slice={"board_id": stream_slice["id"]}, **kwargs) + except HTTPError as e: + if self._is_board_error(e.response): + # Wrong board is skipped + self.logger.warning(f"Board {stream_slice['id']} has no columns with a mapped status. Skipping.") + else: + raise + + def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]): + updated_state = latest_record[self.cursor_field] + board_id = str(latest_record["boardId"]) + stream_state_value = current_stream_state.get(board_id, {}).get(self.cursor_field) + if stream_state_value: + updated_state = max(updated_state, stream_state_value) + current_stream_state.setdefault(board_id, {})[self.cursor_field] = updated_state + return current_stream_state + + def get_starting_point(self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any]) -> Optional[pendulum.DateTime]: + board_id = str(stream_slice["board_id"]) + if self.cursor_field not in self._starting_point_cache: + self._starting_point_cache.setdefault(board_id, {})[self.cursor_field] = self._get_starting_point( + stream_state=stream_state, stream_slice=stream_slice + ) + return self._starting_point_cache[board_id][self.cursor_field] + + def _get_starting_point(self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any]) -> Optional[pendulum.DateTime]: + if stream_state: + board_id = str(stream_slice["board_id"]) + stream_state_value = stream_state.get(board_id, {}).get(self.cursor_field) + if stream_state_value: + stream_state_value = pendulum.parse(stream_state_value) - self._lookback_window_minutes + return safe_max(stream_state_value, self._start_date) + return self._start_date def transform(self, record: MutableMapping[str, Any], stream_slice: Mapping[str, Any], **kwargs) -> MutableMapping[str, Any]: record["boardId"] = stream_slice["board_id"] diff --git a/airbyte-integrations/connectors/source-jira/unit_tests/test_streams.py b/airbyte-integrations/connectors/source-jira/unit_tests/test_streams.py index 7b5f693a4feeb..00675fa25ab19 100644 --- a/airbyte-integrations/connectors/source-jira/unit_tests/test_streams.py +++ b/airbyte-integrations/connectors/source-jira/unit_tests/test_streams.py @@ -381,7 +381,7 @@ def test_board_issues_stream(config, mock_board_response, board_issues_response) authenticator = SourceJira().get_authenticator(config=config) args = {"authenticator": authenticator, "domain": config["domain"], "projects": config.get("projects", [])} stream = BoardIssues(**args) - records = [r for r in stream.read_records(sync_mode=SyncMode.incremental)] + records = list(read_full_refresh(stream)) assert len(records) == 1 assert len(responses.calls) == 4 @@ -391,10 +391,10 @@ def test_stream_updated_state(config): args = {"authenticator": authenticator, "domain": config["domain"], "projects": config.get("projects", [])} stream = BoardIssues(**args) - current_stream_state = {"updated": "09.11.2023"} - latest_record = {"updated": "10.11.2023"} + current_stream_state = {"22": {"updated": "2023-10-01T00:00:00Z"}} + latest_record = {"boardId": 22, "updated": "2023-09-01T00:00:00Z"} - assert {"updated": "10.11.2023"} == stream.get_updated_state(current_stream_state=current_stream_state, latest_record=latest_record) + assert {"22": {"updated": "2023-10-01T00:00:00Z"}} == stream.get_updated_state(current_stream_state=current_stream_state, latest_record=latest_record) @responses.activate diff --git a/docs/integrations/sources/jira-migrations.md b/docs/integrations/sources/jira-migrations.md new file mode 100644 index 0000000000000..9dc0955b49d28 --- /dev/null +++ b/docs/integrations/sources/jira-migrations.md @@ -0,0 +1,27 @@ +# Jira Migration Guide + +## Upgrading to 1.0.0 + +Note: this change is only breaking if you are using the `Boards Issues` stream in Incremental Sync mode. + +This is a breaking change because Stream State for `Boards Issues` will be changed, so please follow the instructions below to migrate to version 1.0.0: + +1. Select **Connections** in the main navbar. +1.1 Select the connection(s) affected by the update. +2. Select the **Replication** tab. +2.1 Select **Refresh source schema**. + ```note + Any detected schema changes will be listed for your review. + ``` +2.2 Select **OK**. +3. Select **Save changes** at the bottom of the page. +3.1 Ensure the **Reset affected streams** option is checked. + ```note + Depending on destination type you may not be prompted to reset your data + ``` +4. Select **Save connection**. + ```note + This will reset the data in your destination and initiate a fresh sync. + ``` + +For more information on resetting your data in Airbyte, see [this page](https://docs.airbyte.com/operator-guides/reset). \ No newline at end of file diff --git a/docs/integrations/sources/jira.md b/docs/integrations/sources/jira.md index e66486659c30b..1e579b009d0b3 100644 --- a/docs/integrations/sources/jira.md +++ b/docs/integrations/sources/jira.md @@ -124,6 +124,7 @@ The Jira connector should not run into Jira API limitations under normal usage. | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------| +| 1.0.0 | 2024-01-01 | [33682](https://github.com/airbytehq/airbyte/pull/33682) | Save state for stream `Board Issues` per `board` | | 0.14.1 | 2023-12-19 | [33625](https://github.com/airbytehq/airbyte/pull/33625) | Skip 404 error | | 0.14.0 | 2023-12-15 | [33532](https://github.com/airbytehq/airbyte/pull/33532) | Add lookback window | | 0.13.0 | 2023-12-12 | [33353](https://github.com/airbytehq/airbyte/pull/33353) | Fix check command to check access for all available streams | From 076f95a87aa1dacafc2e1c10cc599a6d17f1d78a Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 10 Jan 2024 14:48:03 +0100 Subject: [PATCH 036/574] Source Zendesk Support: Convert to airbyte-lib (#34010) --- .../connectors/source-zendesk-support/main.py | 9 ++------- .../source-zendesk-support/metadata.yaml | 2 +- .../connectors/source-zendesk-support/setup.py | 5 +++++ .../source_zendesk_support/run.py | 14 ++++++++++++++ docs/integrations/sources/zendesk-support.md | 3 ++- 5 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 airbyte-integrations/connectors/source-zendesk-support/source_zendesk_support/run.py diff --git a/airbyte-integrations/connectors/source-zendesk-support/main.py b/airbyte-integrations/connectors/source-zendesk-support/main.py index d3b005c42d350..88eed5ec56afa 100644 --- a/airbyte-integrations/connectors/source-zendesk-support/main.py +++ b/airbyte-integrations/connectors/source-zendesk-support/main.py @@ -2,12 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_zendesk_support import SourceZendeskSupport +from source_zendesk_support.run import run if __name__ == "__main__": - source = SourceZendeskSupport() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-zendesk-support/metadata.yaml b/airbyte-integrations/connectors/source-zendesk-support/metadata.yaml index 4a13cd6b6ace8..a52ad62c5f27c 100644 --- a/airbyte-integrations/connectors/source-zendesk-support/metadata.yaml +++ b/airbyte-integrations/connectors/source-zendesk-support/metadata.yaml @@ -11,7 +11,7 @@ data: connectorSubtype: api connectorType: source definitionId: 79c1aa37-dae3-42ae-b333-d1c105477715 - dockerImageTag: 2.2.4 + dockerImageTag: 2.2.5 dockerRepository: airbyte/source-zendesk-support documentationUrl: https://docs.airbyte.com/integrations/sources/zendesk-support githubIssueLabel: source-zendesk-support diff --git a/airbyte-integrations/connectors/source-zendesk-support/setup.py b/airbyte-integrations/connectors/source-zendesk-support/setup.py index 519b1350c9142..e0466040c015f 100644 --- a/airbyte-integrations/connectors/source-zendesk-support/setup.py +++ b/airbyte-integrations/connectors/source-zendesk-support/setup.py @@ -10,6 +10,11 @@ TEST_REQUIREMENTS = ["freezegun", "pytest~=6.1", "pytest-mock~=3.6", "requests-mock==1.9.3"] setup( + entry_points={ + "console_scripts": [ + "source-zendesk-support=source_zendesk_support.run:run", + ], + }, version="0.1.0", name="source_zendesk_support", description="Source implementation for Zendesk Support.", diff --git a/airbyte-integrations/connectors/source-zendesk-support/source_zendesk_support/run.py b/airbyte-integrations/connectors/source-zendesk-support/source_zendesk_support/run.py new file mode 100644 index 0000000000000..95b88323a18cc --- /dev/null +++ b/airbyte-integrations/connectors/source-zendesk-support/source_zendesk_support/run.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_zendesk_support import SourceZendeskSupport + + +def run(): + source = SourceZendeskSupport() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/zendesk-support.md b/docs/integrations/sources/zendesk-support.md index 3050fdb28c4ad..9eda60be0aaf8 100644 --- a/docs/integrations/sources/zendesk-support.md +++ b/docs/integrations/sources/zendesk-support.md @@ -158,6 +158,7 @@ The Zendesk connector ideally should not run into Zendesk API limitations under | Version | Date | Pull Request | Subject | |:---------|:-----------|:---------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 2.2.5 | 2024-01-08 | [34010](https://github.com/airbytehq/airbyte/pull/34010) | prepare for airbyte-lib | | `2.2.4` | 2023-12-20 | [33680](https://github.com/airbytehq/airbyte/pull/33680) | Fix pagination issue for streams related to incremental export sync | | `2.2.3` | 2023-12-14 | [33435](https://github.com/airbytehq/airbyte/pull/33435) | Fix 504 Error for stream Ticket Audits | | `2.2.2` | 2023-12-01 | [33012](https://github.com/airbytehq/airbyte/pull/33012) | Increase number of retries for backoff policy to 10 | @@ -240,4 +241,4 @@ The Zendesk connector ideally should not run into Zendesk API limitations under | `0.1.1` | 2021-09-02 | [5787](https://github.com/airbytehq/airbyte/pull/5787) | Fixed incremental logic for the ticket_comments stream | | `0.1.0` | 2021-07-21 | [4861](https://github.com/airbytehq/airbyte/pull/4861) | Created CDK native zendesk connector | - \ No newline at end of file + From 26deee7f896db0d1fdc877d8393f642aa4128bc3 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 10 Jan 2024 14:48:15 +0100 Subject: [PATCH 037/574] Source Google Analytics Universal: Convert to airbyte-lib (#34018) --- .../source-google-analytics-data-api/main.py | 13 ++----------- .../metadata.yaml | 2 +- .../source-google-analytics-data-api/setup.py | 5 +++++ .../source_google_analytics_data_api/run.py | 18 ++++++++++++++++++ .../sources/google-analytics-data-api.md | 1 + 5 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/run.py diff --git a/airbyte-integrations/connectors/source-google-analytics-data-api/main.py b/airbyte-integrations/connectors/source-google-analytics-data-api/main.py index 02cbd41ab7d54..93839ed0e51a5 100644 --- a/airbyte-integrations/connectors/source-google-analytics-data-api/main.py +++ b/airbyte-integrations/connectors/source-google-analytics-data-api/main.py @@ -2,16 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_google_analytics_data_api import SourceGoogleAnalyticsDataApi -from source_google_analytics_data_api.config_migrations import MigrateCustomReports, MigrateCustomReportsCohortSpec, MigratePropertyID +from source_google_analytics_data_api.run import run if __name__ == "__main__": - source = SourceGoogleAnalyticsDataApi() - MigratePropertyID.migrate(sys.argv[1:], source) - MigrateCustomReports.migrate(sys.argv[1:], source) - MigrateCustomReportsCohortSpec.migrate(sys.argv[1:], source) - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-google-analytics-data-api/metadata.yaml b/airbyte-integrations/connectors/source-google-analytics-data-api/metadata.yaml index c9e768b077b49..ed216435519ea 100644 --- a/airbyte-integrations/connectors/source-google-analytics-data-api/metadata.yaml +++ b/airbyte-integrations/connectors/source-google-analytics-data-api/metadata.yaml @@ -12,7 +12,7 @@ data: connectorSubtype: api connectorType: source definitionId: 3cc2eafd-84aa-4dca-93af-322d9dfeec1a - dockerImageTag: 2.1.0 + dockerImageTag: 2.1.1 dockerRepository: airbyte/source-google-analytics-data-api documentationUrl: https://docs.airbyte.com/integrations/sources/google-analytics-data-api githubIssueLabel: source-google-analytics-data-api diff --git a/airbyte-integrations/connectors/source-google-analytics-data-api/setup.py b/airbyte-integrations/connectors/source-google-analytics-data-api/setup.py index b11a793a8d5fa..f2a10ce3101c0 100644 --- a/airbyte-integrations/connectors/source-google-analytics-data-api/setup.py +++ b/airbyte-integrations/connectors/source-google-analytics-data-api/setup.py @@ -15,6 +15,11 @@ ] setup( + entry_points={ + "console_scripts": [ + "source-google-analytics-data-api=source_google_analytics_data_api.run:run", + ], + }, name="source_google_analytics_data_api", description="Source implementation for Google Analytics Data Api.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/run.py b/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/run.py new file mode 100644 index 0000000000000..ed4ec25e9250a --- /dev/null +++ b/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/run.py @@ -0,0 +1,18 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_google_analytics_data_api import SourceGoogleAnalyticsDataApi +from source_google_analytics_data_api.config_migrations import MigrateCustomReports, MigrateCustomReportsCohortSpec, MigratePropertyID + + +def run(): + source = SourceGoogleAnalyticsDataApi() + MigratePropertyID.migrate(sys.argv[1:], source) + MigrateCustomReports.migrate(sys.argv[1:], source) + MigrateCustomReportsCohortSpec.migrate(sys.argv[1:], source) + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/google-analytics-data-api.md b/docs/integrations/sources/google-analytics-data-api.md index cbf18822357fb..6923ec0801aa9 100644 --- a/docs/integrations/sources/google-analytics-data-api.md +++ b/docs/integrations/sources/google-analytics-data-api.md @@ -263,6 +263,7 @@ The Google Analytics connector is subject to Google Analytics Data API quotas. P | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:--------------------------------------------------------------------------------| +| 2.1.1 | 2024-01-08 | [1234](https://github.com/airbytehq/airbyte/pull/1234) | prepare for airbyte-lib | | 2.1.0 | 2023-12-28 | [33802](https://github.com/airbytehq/airbyte/pull/33802) | Add `CohortSpec` to custom report in specification | | 2.0.3 | 2023-11-03 | [32149](https://github.com/airbytehq/airbyte/pull/32149) | Fixed bug with missing `metadata` when the credentials are not valid | | 2.0.2 | 2023-11-02 | [32094](https://github.com/airbytehq/airbyte/pull/32094) | Added handling for `JSONDecodeError` while checking for `api qouta` limits | From d7e9ffa83c7b55b173fb5cdbf25a56d07251b526 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 10 Jan 2024 14:59:19 +0100 Subject: [PATCH 038/574] Source Smartsheets: Convert to airbyte-lib (#34012) --- .../connectors/source-smartsheets/Dockerfile | 2 +- .../connectors/source-smartsheets/main.py | 9 ++------- .../connectors/source-smartsheets/metadata.yaml | 2 +- .../connectors/source-smartsheets/setup.py | 5 +++++ .../source-smartsheets/source_smartsheets/run.py | 14 ++++++++++++++ docs/integrations/sources/smartsheets.md | 1 + 6 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 airbyte-integrations/connectors/source-smartsheets/source_smartsheets/run.py diff --git a/airbyte-integrations/connectors/source-smartsheets/Dockerfile b/airbyte-integrations/connectors/source-smartsheets/Dockerfile index 023118cbedd75..00a47d39b74fa 100644 --- a/airbyte-integrations/connectors/source-smartsheets/Dockerfile +++ b/airbyte-integrations/connectors/source-smartsheets/Dockerfile @@ -14,5 +14,5 @@ COPY $CODE_PATH ./$CODE_PATH ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] -LABEL io.airbyte.version=1.1.1 +LABEL io.airbyte.version=1.1.2 LABEL io.airbyte.name=airbyte/source-smartsheets diff --git a/airbyte-integrations/connectors/source-smartsheets/main.py b/airbyte-integrations/connectors/source-smartsheets/main.py index 3603f2be666a9..62f5650b92eac 100644 --- a/airbyte-integrations/connectors/source-smartsheets/main.py +++ b/airbyte-integrations/connectors/source-smartsheets/main.py @@ -2,12 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_smartsheets import SourceSmartsheets +from source_smartsheets.run import run if __name__ == "__main__": - source = SourceSmartsheets() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-smartsheets/metadata.yaml b/airbyte-integrations/connectors/source-smartsheets/metadata.yaml index 46e5bc5e55863..cf3f522c9bf47 100644 --- a/airbyte-integrations/connectors/source-smartsheets/metadata.yaml +++ b/airbyte-integrations/connectors/source-smartsheets/metadata.yaml @@ -9,7 +9,7 @@ data: connectorSubtype: api connectorType: source definitionId: 374ebc65-6636-4ea0-925c-7d35999a8ffc - dockerImageTag: 1.1.1 + dockerImageTag: 1.1.2 dockerRepository: airbyte/source-smartsheets documentationUrl: https://docs.airbyte.com/integrations/sources/smartsheets githubIssueLabel: source-smartsheets diff --git a/airbyte-integrations/connectors/source-smartsheets/setup.py b/airbyte-integrations/connectors/source-smartsheets/setup.py index f30812c4b62f4..661a68ca12bec 100644 --- a/airbyte-integrations/connectors/source-smartsheets/setup.py +++ b/airbyte-integrations/connectors/source-smartsheets/setup.py @@ -9,6 +9,11 @@ TEST_REQUIREMENTS = ["requests-mock~=1.9.3", "pytest", "pytest-mock~=3.6.1"] setup( + entry_points={ + "console_scripts": [ + "source-smartsheets=source_smartsheets.run:run", + ], + }, name="source_smartsheets", description="Source implementation for Smartsheets.", author="Nate Nowack", diff --git a/airbyte-integrations/connectors/source-smartsheets/source_smartsheets/run.py b/airbyte-integrations/connectors/source-smartsheets/source_smartsheets/run.py new file mode 100644 index 0000000000000..6195e74166ce2 --- /dev/null +++ b/airbyte-integrations/connectors/source-smartsheets/source_smartsheets/run.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_smartsheets import SourceSmartsheets + + +def run(): + source = SourceSmartsheets() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/smartsheets.md b/docs/integrations/sources/smartsheets.md index 4d9e62d85997d..52359b70e88ed 100644 --- a/docs/integrations/sources/smartsheets.md +++ b/docs/integrations/sources/smartsheets.md @@ -110,6 +110,7 @@ The remaining column datatypes supported by Smartsheets are more complex types ( | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:----------------------------------------------------------| +| 1.1.2 | 2024-01-08 | [1234](https://github.com/airbytehq/airbyte/pull/1234) | prepare for airbyte-lib | | 1.1.1 | 2023-06-06 | [27096](https://github.com/airbytehq/airbyte/pull/27096) | Fix error when optional metadata fields are not set | | 1.1.0 | 2023-06-02 | [22382](https://github.com/airbytehq/airbyte/pull/22382) | Add support for ingesting metadata fields | | 1.0.2 | 2023-05-12 | [26024](https://github.com/airbytehq/airbyte/pull/26024) | Fix dependencies conflict | From 24dfebc89dde22731ee80c11da8d9ecd3f0926e9 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 10 Jan 2024 14:59:34 +0100 Subject: [PATCH 039/574] Source Stripe: Convert to airbyte-lib (#33940) --- .../connectors/source-stripe/main.py | 40 +---------------- .../connectors/source-stripe/metadata.yaml | 2 +- .../connectors/source-stripe/setup.py | 5 +++ .../source-stripe/source_stripe/run.py | 45 +++++++++++++++++++ docs/integrations/sources/stripe.md | 1 + 5 files changed, 54 insertions(+), 39 deletions(-) create mode 100644 airbyte-integrations/connectors/source-stripe/source_stripe/run.py diff --git a/airbyte-integrations/connectors/source-stripe/main.py b/airbyte-integrations/connectors/source-stripe/main.py index a8ed671a82922..971f33a69dd10 100644 --- a/airbyte-integrations/connectors/source-stripe/main.py +++ b/airbyte-integrations/connectors/source-stripe/main.py @@ -3,43 +3,7 @@ # -import sys -import traceback -from datetime import datetime -from typing import List - -from airbyte_cdk.entrypoint import AirbyteEntrypoint, launch -from airbyte_cdk.models import AirbyteErrorTraceMessage, AirbyteMessage, AirbyteTraceMessage, TraceType, Type -from source_stripe import SourceStripe - - -def _get_source(args: List[str]): - catalog_path = AirbyteEntrypoint.extract_catalog(args) - config_path = AirbyteEntrypoint.extract_config(args) - try: - return SourceStripe( - SourceStripe.read_catalog(catalog_path) if catalog_path else None, - SourceStripe.read_config(config_path) if config_path else None, - ) - except Exception as error: - print( - AirbyteMessage( - type=Type.TRACE, - trace=AirbyteTraceMessage( - type=TraceType.ERROR, - emitted_at=int(datetime.now().timestamp() * 1000), - error=AirbyteErrorTraceMessage( - message=f"Error starting the sync. This could be due to an invalid configuration or catalog. Please contact Support for assistance. Error: {error}", - stack_trace=traceback.format_exc(), - ), - ), - ).json() - ) - return None - +from source_stripe.run import run if __name__ == "__main__": - _args = sys.argv[1:] - source = _get_source(_args) - if source: - launch(source, _args) + run() diff --git a/airbyte-integrations/connectors/source-stripe/metadata.yaml b/airbyte-integrations/connectors/source-stripe/metadata.yaml index 2a734e9f39873..ee417c24c7a94 100644 --- a/airbyte-integrations/connectors/source-stripe/metadata.yaml +++ b/airbyte-integrations/connectors/source-stripe/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: e094cb9a-26de-4645-8761-65c0c425d1de - dockerImageTag: 5.1.1 + dockerImageTag: 5.1.2 dockerRepository: airbyte/source-stripe documentationUrl: https://docs.airbyte.com/integrations/sources/stripe githubIssueLabel: source-stripe diff --git a/airbyte-integrations/connectors/source-stripe/setup.py b/airbyte-integrations/connectors/source-stripe/setup.py index aab9a737d197e..2d05b4aec1c85 100644 --- a/airbyte-integrations/connectors/source-stripe/setup.py +++ b/airbyte-integrations/connectors/source-stripe/setup.py @@ -20,4 +20,9 @@ extras_require={ "tests": TEST_REQUIREMENTS, }, + entry_points={ + "console_scripts": [ + "source-stripe=source_stripe.run:run", + ], + }, ) diff --git a/airbyte-integrations/connectors/source-stripe/source_stripe/run.py b/airbyte-integrations/connectors/source-stripe/source_stripe/run.py new file mode 100644 index 0000000000000..b72025a2c7267 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/source_stripe/run.py @@ -0,0 +1,45 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys +import traceback +from datetime import datetime +from typing import List + +from airbyte_cdk.entrypoint import AirbyteEntrypoint, launch +from airbyte_cdk.models import AirbyteErrorTraceMessage, AirbyteMessage, AirbyteTraceMessage, TraceType, Type +from source_stripe import SourceStripe + + +def _get_source(args: List[str]): + catalog_path = AirbyteEntrypoint.extract_catalog(args) + config_path = AirbyteEntrypoint.extract_config(args) + try: + return SourceStripe( + SourceStripe.read_catalog(catalog_path) if catalog_path else None, + SourceStripe.read_config(config_path) if config_path else None, + ) + except Exception as error: + print( + AirbyteMessage( + type=Type.TRACE, + trace=AirbyteTraceMessage( + type=TraceType.ERROR, + emitted_at=int(datetime.now().timestamp() * 1000), + error=AirbyteErrorTraceMessage( + message=f"Error starting the sync. This could be due to an invalid configuration or catalog. Please contact Support for assistance. Error: {error}", + stack_trace=traceback.format_exc(), + ), + ), + ).json() + ) + return None + + +def run(): + _args = sys.argv[1:] + source = _get_source(_args) + if source: + launch(source, _args) diff --git a/docs/integrations/sources/stripe.md b/docs/integrations/sources/stripe.md index 516ef4322ab64..2e517830bdf7b 100644 --- a/docs/integrations/sources/stripe.md +++ b/docs/integrations/sources/stripe.md @@ -216,6 +216,7 @@ Each record is marked with `is_deleted` flag when the appropriate event happens | Version | Date | Pull Request | Subject | |:--------|:-----------|:-------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 5.1.2 | 2024-01-04 | [33414](https://github.com/airbytehq/airbyte/pull/33414) | Prepare for airbyte-lib | | 5.1.1 | 2024-01-04 | [33926](https://github.com/airbytehq/airbyte/pull/33926/) | Update endpoint for `bank_accounts` stream | | 5.1.0 | 2023-12-11 | [32908](https://github.com/airbytehq/airbyte/pull/32908/) | Read full refresh streams concurrently | | 5.0.2 | 2023-12-01 | [33038](https://github.com/airbytehq/airbyte/pull/33038) | Add stream slice logging for SubStream | From 212f960294fb7ca318127c44a3cbb68de7ec24f8 Mon Sep 17 00:00:00 2001 From: jdpgrailsdev Date: Wed, 10 Jan 2024 14:19:57 +0000 Subject: [PATCH 040/574] Bump Airbyte version from 0.50.41 to 0.50.42 --- .bumpversion.cfg | 2 +- docs/operator-guides/upgrading-airbyte.md | 2 +- gradle.properties | 2 +- run-ab-platform.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c0637870ec500..d4aab85e4032c 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.50.41 +current_version = 0.50.42 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-[a-z]+)? diff --git a/docs/operator-guides/upgrading-airbyte.md b/docs/operator-guides/upgrading-airbyte.md index ab74613d251c4..9810dc0a4b34f 100644 --- a/docs/operator-guides/upgrading-airbyte.md +++ b/docs/operator-guides/upgrading-airbyte.md @@ -128,7 +128,7 @@ If you are upgrading from (i.e. your current version of Airbyte is) Airbyte vers Here's an example of what it might look like with the values filled in. It assumes that the downloaded `airbyte_archive.tar.gz` is in `/tmp`. ```bash - docker run --rm -v /tmp:/config airbyte/migration:0.50.41 --\ + docker run --rm -v /tmp:/config airbyte/migration:0.50.42 --\ --input /config/airbyte_archive.tar.gz\ --output /config/airbyte_archive_migrated.tar.gz ``` diff --git a/gradle.properties b/gradle.properties index 9c81f490a6478..7ce1e172f71a0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION=0.50.41 +VERSION=0.50.42 # NOTE: some of these values are overwritten in CI! # NOTE: if you want to override this for your local machine, set overrides in ~/.gradle/gradle.properties diff --git a/run-ab-platform.sh b/run-ab-platform.sh index 44139618aadda..eb117f305b627 100755 --- a/run-ab-platform.sh +++ b/run-ab-platform.sh @@ -1,6 +1,6 @@ #!/bin/bash -VERSION=0.50.41 +VERSION=0.50.42 # Run away from anything even a little scary set -o nounset # -u exit if a variable is not set set -o errexit # -f exit for any command failure" From 9c6aea19cdfcdcff237e29449f2b04ac3d1ed9a8 Mon Sep 17 00:00:00 2001 From: Artem Inzhyyants <36314070+artem1205@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:20:40 +0100 Subject: [PATCH 041/574] Airbyte CDK: handle private network exception as config error (#33751) --- airbyte-cdk/python/airbyte_cdk/entrypoint.py | 8 +++++--- .../connector_builder/test_connector_builder_handler.py | 6 +++--- .../python/unit_tests/sources/test_integration_source.py | 7 ++++--- airbyte-cdk/python/unit_tests/test_entrypoint.py | 7 ++++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/airbyte-cdk/python/airbyte_cdk/entrypoint.py b/airbyte-cdk/python/airbyte_cdk/entrypoint.py index f89e0ef0ec296..37b2590b7ea62 100644 --- a/airbyte-cdk/python/airbyte_cdk/entrypoint.py +++ b/airbyte-cdk/python/airbyte_cdk/entrypoint.py @@ -26,6 +26,7 @@ from airbyte_cdk.utils.airbyte_secrets_utils import get_secrets, update_secrets from airbyte_cdk.utils.constants import ENV_REQUEST_CACHE_PATH from airbyte_cdk.utils.traced_exception import AirbyteTracedException +from airbyte_protocol.models import FailureType from requests import PreparedRequest, Response, Session logger = init_logger("airbyte") @@ -236,9 +237,10 @@ def filtered_send(self: Any, request: PreparedRequest, **kwargs: Any) -> Respons try: is_private = _is_private_url(parsed_url.hostname, parsed_url.port) # type: ignore [arg-type] if is_private: - raise ValueError( - "Invalid URL endpoint: The endpoint that data is being requested from belongs to a private network. Source " - + "connectors only support requesting data from public API endpoints." + raise AirbyteTracedException( + internal_message=f"Invalid URL endpoint: `{parsed_url.hostname!r}` belongs to a private network", + failure_type=FailureType.config_error, + message="Invalid URL endpoint: The endpoint that data is being requested from belongs to a private network. Source connectors only support requesting data from public API endpoints.", ) except socket.gaierror as exception: # This is a special case where the developer specifies an IP address string that is not formatted correctly like trailing diff --git a/airbyte-cdk/python/unit_tests/connector_builder/test_connector_builder_handler.py b/airbyte-cdk/python/unit_tests/connector_builder/test_connector_builder_handler.py index a314a928b9040..190f8d4bcb56b 100644 --- a/airbyte-cdk/python/unit_tests/connector_builder/test_connector_builder_handler.py +++ b/airbyte-cdk/python/unit_tests/connector_builder/test_connector_builder_handler.py @@ -769,8 +769,8 @@ def test_read_source_single_page_single_slice(mock_http_stream): "deployment_mode, url_base, expected_error", [ pytest.param("CLOUD", "https://airbyte.com/api/v1/characters", None, id="test_cloud_read_with_public_endpoint"), - pytest.param("CLOUD", "https://10.0.27.27", "ValueError", id="test_cloud_read_with_private_endpoint"), - pytest.param("CLOUD", "https://localhost:80/api/v1/cast", "ValueError", id="test_cloud_read_with_localhost"), + pytest.param("CLOUD", "https://10.0.27.27", "AirbyteTracedException", id="test_cloud_read_with_private_endpoint"), + pytest.param("CLOUD", "https://localhost:80/api/v1/cast", "AirbyteTracedException", id="test_cloud_read_with_localhost"), pytest.param("CLOUD", "http://unsecured.protocol/api/v1", "InvalidSchema", id="test_cloud_read_with_unsecured_endpoint"), pytest.param("CLOUD", "https://domainwithoutextension", "Invalid URL", id="test_cloud_read_with_invalid_url_endpoint"), pytest.param("OSS", "https://airbyte.com/api/v1/", None, id="test_oss_read_with_public_endpoint"), @@ -820,7 +820,7 @@ def test_handle_read_external_requests(deployment_mode, url_base, expected_error "deployment_mode, token_url, expected_error", [ pytest.param("CLOUD", "https://airbyte.com/tokens/bearer", None, id="test_cloud_read_with_public_endpoint"), - pytest.param("CLOUD", "https://10.0.27.27/tokens/bearer", "ValueError", id="test_cloud_read_with_private_endpoint"), + pytest.param("CLOUD", "https://10.0.27.27/tokens/bearer", "AirbyteTracedException", id="test_cloud_read_with_private_endpoint"), pytest.param("CLOUD", "http://unsecured.protocol/tokens/bearer", "InvalidSchema", id="test_cloud_read_with_unsecured_endpoint"), pytest.param("CLOUD", "https://domainwithoutextension", "Invalid URL", id="test_cloud_read_with_invalid_url_endpoint"), pytest.param("OSS", "https://airbyte.com/tokens/bearer", None, id="test_oss_read_with_public_endpoint"), diff --git a/airbyte-cdk/python/unit_tests/sources/test_integration_source.py b/airbyte-cdk/python/unit_tests/sources/test_integration_source.py index 64b322eb53c3a..048864f12c908 100644 --- a/airbyte-cdk/python/unit_tests/sources/test_integration_source.py +++ b/airbyte-cdk/python/unit_tests/sources/test_integration_source.py @@ -9,6 +9,7 @@ import pytest import requests from airbyte_cdk.entrypoint import launch +from airbyte_cdk.utils import AirbyteTracedException from unit_tests.sources.fixtures.source_test_fixture import ( HttpTestStream, SourceFixtureOauthAuthenticator, @@ -22,8 +23,8 @@ [ pytest.param("CLOUD", "https://airbyte.com/api/v1/", [], None, id="test_cloud_read_with_public_endpoint"), pytest.param("CLOUD", "http://unsecured.com/api/v1/", [], ValueError, id="test_cloud_read_with_unsecured_url"), - pytest.param("CLOUD", "https://172.20.105.99/api/v1/", [], ValueError, id="test_cloud_read_with_private_endpoint"), - pytest.param("CLOUD", "https://localhost:80/api/v1/", [], ValueError, id="test_cloud_read_with_localhost"), + pytest.param("CLOUD", "https://172.20.105.99/api/v1/", [], AirbyteTracedException, id="test_cloud_read_with_private_endpoint"), + pytest.param("CLOUD", "https://localhost:80/api/v1/", [], AirbyteTracedException, id="test_cloud_read_with_localhost"), pytest.param("OSS", "https://airbyte.com/api/v1/", [], None, id="test_oss_read_with_public_endpoint"), pytest.param("OSS", "https://172.20.105.99/api/v1/", [], None, id="test_oss_read_with_private_endpoint"), ], @@ -47,7 +48,7 @@ def test_external_request_source(capsys, deployment_mode, url_base, expected_rec [ pytest.param("CLOUD", "https://airbyte.com/api/v1/", [], None, id="test_cloud_read_with_public_endpoint"), pytest.param("CLOUD", "http://unsecured.com/api/v1/", [], ValueError, id="test_cloud_read_with_unsecured_url"), - pytest.param("CLOUD", "https://172.20.105.99/api/v1/", [], ValueError, id="test_cloud_read_with_private_endpoint"), + pytest.param("CLOUD", "https://172.20.105.99/api/v1/", [], AirbyteTracedException, id="test_cloud_read_with_private_endpoint"), pytest.param("OSS", "https://airbyte.com/api/v1/", [], None, id="test_oss_read_with_public_endpoint"), pytest.param("OSS", "https://172.20.105.99/api/v1/", [], None, id="test_oss_read_with_private_endpoint"), ], diff --git a/airbyte-cdk/python/unit_tests/test_entrypoint.py b/airbyte-cdk/python/unit_tests/test_entrypoint.py index 7acceff69a190..61e7e7ec142a3 100644 --- a/airbyte-cdk/python/unit_tests/test_entrypoint.py +++ b/airbyte-cdk/python/unit_tests/test_entrypoint.py @@ -28,6 +28,7 @@ Type, ) from airbyte_cdk.sources import Source +from airbyte_cdk.utils import AirbyteTracedException class MockSource(Source): @@ -276,12 +277,12 @@ def test_invalid_command(entrypoint: AirbyteEntrypoint, config_mock): "deployment_mode, url, expected_error", [ pytest.param("CLOUD", "https://airbyte.com", None, id="test_cloud_public_endpoint_is_successful"), - pytest.param("CLOUD", "https://192.168.27.30", ValueError, id="test_cloud_private_ip_address_is_rejected"), - pytest.param("CLOUD", "https://localhost:8080/api/v1/cast", ValueError, id="test_cloud_private_endpoint_is_rejected"), + pytest.param("CLOUD", "https://192.168.27.30", AirbyteTracedException, id="test_cloud_private_ip_address_is_rejected"), + pytest.param("CLOUD", "https://localhost:8080/api/v1/cast", AirbyteTracedException, id="test_cloud_private_endpoint_is_rejected"), pytest.param("CLOUD", "http://past.lives.net/api/v1/inyun", ValueError, id="test_cloud_unsecured_endpoint_is_rejected"), pytest.param("CLOUD", "https://not:very/cash:443.money", ValueError, id="test_cloud_invalid_url_format"), pytest.param("CLOUD", "https://192.168.27.30 ", ValueError, id="test_cloud_incorrect_ip_format_is_rejected"), - pytest.param("cloud", "https://192.168.27.30", ValueError, id="test_case_insensitive_cloud_environment_variable"), + pytest.param("cloud", "https://192.168.27.30", AirbyteTracedException, id="test_case_insensitive_cloud_environment_variable"), pytest.param("OSS", "https://airbyte.com", None, id="test_oss_public_endpoint_is_successful"), pytest.param("OSS", "https://192.168.27.30", None, id="test_oss_private_endpoint_is_successful"), pytest.param("OSS", "https://localhost:8080/api/v1/cast", None, id="test_oss_private_endpoint_is_successful"), From 0b207f9405945ce1a4299e11ba78050b71669680 Mon Sep 17 00:00:00 2001 From: artem1205 Date: Wed, 10 Jan 2024 14:27:00 +0000 Subject: [PATCH 042/574] =?UTF-8?q?=F0=9F=A4=96=20Bump=20patch=20version?= =?UTF-8?q?=20of=20Python=20CDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airbyte-cdk/python/.bumpversion.cfg | 2 +- airbyte-cdk/python/CHANGELOG.md | 3 +++ airbyte-cdk/python/Dockerfile | 4 ++-- airbyte-cdk/python/setup.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/airbyte-cdk/python/.bumpversion.cfg b/airbyte-cdk/python/.bumpversion.cfg index dabdb23d3791f..daaecf3ee2f13 100644 --- a/airbyte-cdk/python/.bumpversion.cfg +++ b/airbyte-cdk/python/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.58.4 +current_version = 0.58.5 commit = False [bumpversion:file:setup.py] diff --git a/airbyte-cdk/python/CHANGELOG.md b/airbyte-cdk/python/CHANGELOG.md index 4e4a9b6f5125c..e23dd1840b8c1 100644 --- a/airbyte-cdk/python/CHANGELOG.md +++ b/airbyte-cdk/python/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 0.58.5 +Handle private network exception as config error + ## 0.58.4 Add POST method to HttpMocker diff --git a/airbyte-cdk/python/Dockerfile b/airbyte-cdk/python/Dockerfile index 688955e48c5f7..96c5069998e35 100644 --- a/airbyte-cdk/python/Dockerfile +++ b/airbyte-cdk/python/Dockerfile @@ -10,7 +10,7 @@ RUN apk --no-cache upgrade \ && apk --no-cache add tzdata build-base # install airbyte-cdk -RUN pip install --prefix=/install airbyte-cdk==0.58.4 +RUN pip install --prefix=/install airbyte-cdk==0.58.5 # build a clean environment FROM base @@ -32,5 +32,5 @@ ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] # needs to be the same as CDK -LABEL io.airbyte.version=0.58.4 +LABEL io.airbyte.version=0.58.5 LABEL io.airbyte.name=airbyte/source-declarative-manifest diff --git a/airbyte-cdk/python/setup.py b/airbyte-cdk/python/setup.py index 3a2ed5f1a2b67..553069c338407 100644 --- a/airbyte-cdk/python/setup.py +++ b/airbyte-cdk/python/setup.py @@ -36,7 +36,7 @@ name="airbyte-cdk", # The version of the airbyte-cdk package is used at runtime to validate manifests. That validation must be # updated if our semver format changes such as using release candidate versions. - version="0.58.4", + version="0.58.5", description="A framework for writing Airbyte Connectors.", long_description=README, long_description_content_type="text/markdown", From bbdd6d8f65f93fefcf96bbb60ac56f0b5de1d2d0 Mon Sep 17 00:00:00 2001 From: Marius Posta Date: Wed, 10 Jan 2024 08:48:36 -0800 Subject: [PATCH 043/574] airbyte-ci: remove connector secrets hack for --is-local (#33972) --- airbyte-ci/connectors/pipelines/README.md | 3 +- .../pipelines/airbyte_ci/metadata/commands.py | 4 +- .../pipelines/dagger/actions/secrets.py | 45 ++++--------------- .../pipelines/pipelines/helpers/utils.py | 13 +++--- .../connectors/pipelines/pyproject.toml | 2 +- 5 files changed, 19 insertions(+), 48 deletions(-) diff --git a/airbyte-ci/connectors/pipelines/README.md b/airbyte-ci/connectors/pipelines/README.md index 0eb970821c0b1..a0781b26c37d7 100644 --- a/airbyte-ci/connectors/pipelines/README.md +++ b/airbyte-ci/connectors/pipelines/README.md @@ -521,7 +521,8 @@ E.G.: running `pytest` on a specific test folder: | Version | PR | Description | | ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| 3.1.1 | [#33979](https://github.com/airbytehq/airbyte/pull/33979) | Fix AssertionError on report existence again | +| 3.1.2 | [#33972](https://github.com/airbytehq/airbyte/pull/33972) | Remove secrets scrubbing hack for --is-local and other small tweaks. | +| 3.1.1 | [#33979](https://github.com/airbytehq/airbyte/pull/33979) | Fix AssertionError on report existence again | | 3.1.0 | [#33994](https://github.com/airbytehq/airbyte/pull/33994) | Log more context information in CI. | | 3.0.2 | [#33987](https://github.com/airbytehq/airbyte/pull/33987) | Fix type checking issue when running --help | | 3.0.1 | [#33981](https://github.com/airbytehq/airbyte/pull/33981) | Fix issues with deploying dagster, pin pendulum version in dagster-cli install | diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/commands.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/commands.py index 24b196fc8a3fb..302da3b11c1bd 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/commands.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/commands.py @@ -3,7 +3,6 @@ # import asyncclick as click -from pipelines.airbyte_ci.metadata.pipeline import run_metadata_orchestrator_deploy_pipeline from pipelines.cli.dagger_pipeline_command import DaggerPipelineCommand # MAIN GROUP @@ -24,6 +23,9 @@ def deploy(ctx: click.Context) -> None: @deploy.command(cls=DaggerPipelineCommand, name="orchestrator", help="Deploy the metadata service orchestrator to production") @click.pass_context async def deploy_orchestrator(ctx: click.Context) -> None: + # Import locally to speed up CLI. + from pipelines.airbyte_ci.metadata.pipeline import run_metadata_orchestrator_deploy_pipeline + await run_metadata_orchestrator_deploy_pipeline( ctx.obj["is_local"], ctx.obj["git_branch"], diff --git a/airbyte-ci/connectors/pipelines/pipelines/dagger/actions/secrets.py b/airbyte-ci/connectors/pipelines/pipelines/dagger/actions/secrets.py index 91f713ebe36b4..e13ff7abd8a66 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/dagger/actions/secrets.py +++ b/airbyte-ci/connectors/pipelines/pipelines/dagger/actions/secrets.py @@ -137,45 +137,16 @@ async def get_connector_secrets(context: ConnectorContext) -> dict[str, Secret]: async def mounted_connector_secrets(context: ConnectorContext, secret_directory_path: str) -> Callable[[Container], Container]: - # By default, mount the secrets properly as dagger secret files. - # - # This will cause the contents of these files to be scrubbed from the logs. This scrubbing comes at the cost of - # unavoidable latency in the log output, see next paragraph for details as to why. This is fine in a CI environment - # however this becomes a nuisance locally: the developer wants the logs to be displayed to them in an as timely - # manner as possible. Since the secrets aren't really secret in that case anyway, we mount them in the container as - # regular files instead. - # - # The buffering behavior that comes into play when logs are scrubbed is both unavoidable and not configurable. - # It's fundamentally unavoidable because dagger needs to match a bunch of regexes (one per secret) and therefore - # needs to buffer at least as many bytes as the longest of all possible matches. Still, this isn't that long in - # practice in our case. The real problem is that the buffering is not configurable: dagger relies on a golang - # library called transform [1] to perform the regexp matching on a stream and this library hard-codes a buffer - # size of 4096 bytes for each regex [2]. - # - # Remove the special local case whenever dagger implements scrubbing differently [3,4]. - # - # [1] https://golang.org/x/text/transform - # [2] https://cs.opensource.google/go/x/text/+/refs/tags/v0.13.0:transform/transform.go;l=130 - # [3] https://github.com/dagger/dagger/blob/v0.6.4/cmd/shim/main.go#L294 - # [4] https://github.com/airbytehq/airbyte/issues/30394 - # - connector_secrets = await context.get_connector_secrets() - if context.is_local: - # Special case for local development. - # Query dagger for the contents of the secrets and mount these strings as files in the container. - contents = {} - for secret_file_name, secret in connector_secrets.items(): - contents[secret_file_name] = await secret.plaintext() + """Returns an argument for a dagger container's with_ method which mounts all connector secrets in it. - def with_secrets_mounted_as_regular_files(container: Container) -> Container: - container = container.with_exec(["mkdir", "-p", secret_directory_path], skip_entrypoint=True) - for secret_file_name, secret_content_str in contents.items(): - container = container.with_new_file( - f"{secret_directory_path}/{secret_file_name}", contents=secret_content_str, permissions=0o600 - ) - return container + Args: + context (ConnectorContext): The context providing a connector object and its secrets. + secret_directory_path (str): Container directory where the secrets will be mounted, as files. - return with_secrets_mounted_as_regular_files + Returns: + fn (Callable[[Container], Container]): A function to pass as argument to the connector container's with_ method. + """ + connector_secrets = await context.get_connector_secrets() def with_secrets_mounted_as_dagger_secrets(container: Container) -> Container: container = container.with_exec(["mkdir", "-p", secret_directory_path], skip_entrypoint=True) diff --git a/airbyte-ci/connectors/pipelines/pipelines/helpers/utils.py b/airbyte-ci/connectors/pipelines/pipelines/helpers/utils.py index aa83ceef43b82..c796e9f6e43b0 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/helpers/utils.py +++ b/airbyte-ci/connectors/pipelines/pipelines/helpers/utils.py @@ -18,7 +18,7 @@ import anyio import asyncclick as click import asyncer -from dagger import Client, Config, Container, ExecError, File, ImageLayerCompression, Platform, QueryError, Secret +from dagger import Client, Config, Container, ExecError, File, ImageLayerCompression, Platform, Secret from more_itertools import chunked if TYPE_CHECKING: @@ -101,13 +101,10 @@ async def get_file_contents(container: Container, path: str) -> Optional[str]: Returns: Optional[str]: The file content if the file exists in the container, None otherwise. """ - try: - return await container.file(path).contents() - except QueryError as e: - if "no such file or directory" not in str(e): - # this error could come from a network issue - raise - return None + dir_name, file_name = os.path.split(path) + if file_name not in set(await container.directory(dir_name).entries()): + return None + return await container.file(path).contents() @contextlib.contextmanager diff --git a/airbyte-ci/connectors/pipelines/pyproject.toml b/airbyte-ci/connectors/pipelines/pyproject.toml index d370f0a8e71bc..2411a03fe1dbe 100644 --- a/airbyte-ci/connectors/pipelines/pyproject.toml +++ b/airbyte-ci/connectors/pipelines/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pipelines" -version = "3.1.1" +version = "3.1.2" description = "Packaged maintained by the connector operations team to perform CI for connectors' pipelines" authors = ["Airbyte "] From e728128238b4687bccf407a36e4b149f4e3919e1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 10 Jan 2024 17:48:52 +0100 Subject: [PATCH 044/574] Azure Blob Storage: Fix unstructured format (#34084) --- .../source-azure-blob-storage/acceptance-test-config.yml | 2 ++ .../connectors/source-azure-blob-storage/metadata.yaml | 2 +- .../source_azure_blob_storage/stream_reader.py | 7 +------ docs/integrations/sources/azure-blob-storage.md | 1 + 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/airbyte-integrations/connectors/source-azure-blob-storage/acceptance-test-config.yml b/airbyte-integrations/connectors/source-azure-blob-storage/acceptance-test-config.yml index e15f5f60be541..71d40148b88f3 100644 --- a/airbyte-integrations/connectors/source-azure-blob-storage/acceptance-test-config.yml +++ b/airbyte-integrations/connectors/source-azure-blob-storage/acceptance-test-config.yml @@ -86,6 +86,8 @@ acceptance_tests: status: succeed - config_path: secrets/jsonl_newlines_config.json status: succeed + - config_path: secrets/unstructured_config.json + status: succeed discovery: tests: - config_path: secrets/config.json diff --git a/airbyte-integrations/connectors/source-azure-blob-storage/metadata.yaml b/airbyte-integrations/connectors/source-azure-blob-storage/metadata.yaml index aecde42730a17..839510e2e1598 100644 --- a/airbyte-integrations/connectors/source-azure-blob-storage/metadata.yaml +++ b/airbyte-integrations/connectors/source-azure-blob-storage/metadata.yaml @@ -7,7 +7,7 @@ data: connectorSubtype: file connectorType: source definitionId: fdaaba68-4875-4ed9-8fcd-4ae1e0a25093 - dockerImageTag: 0.3.0 + dockerImageTag: 0.3.1 dockerRepository: airbyte/source-azure-blob-storage documentationUrl: https://docs.airbyte.com/integrations/sources/azure-blob-storage githubIssueLabel: source-azure-blob-storage diff --git a/airbyte-integrations/connectors/source-azure-blob-storage/source_azure_blob_storage/stream_reader.py b/airbyte-integrations/connectors/source-azure-blob-storage/source_azure_blob_storage/stream_reader.py index 47a235c00f145..c751b72403bd8 100644 --- a/airbyte-integrations/connectors/source-azure-blob-storage/source_azure_blob_storage/stream_reader.py +++ b/airbyte-integrations/connectors/source-azure-blob-storage/source_azure_blob_storage/stream_reader.py @@ -59,7 +59,6 @@ def get_matching_files( if not globs or self.file_matches_globs(remote_file, globs): yield remote_file - @contextmanager def open_file(self, file: RemoteFile, mode: FileReadMode, encoding: Optional[str], logger: logging.Logger) -> IOBase: try: result = open( @@ -73,8 +72,4 @@ def open_file(self, file: RemoteFile, mode: FileReadMode, encoding: Optional[str f"We don't have access to {file.uri}. The file appears to have become unreachable during sync." f"Check whether key {file.uri} exists in `{self.config.azure_blob_storage_container_name}` container and/or has proper ACL permissions" ) - # see https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager for why we do this - try: - yield result - finally: - result.close() + return result diff --git a/docs/integrations/sources/azure-blob-storage.md b/docs/integrations/sources/azure-blob-storage.md index e846694499cb0..0a6dc5e5e8f7a 100644 --- a/docs/integrations/sources/azure-blob-storage.md +++ b/docs/integrations/sources/azure-blob-storage.md @@ -193,6 +193,7 @@ To perform the text extraction from PDF and Docx files, the connector uses the [ | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:--------------------------------------------------------------------------------| +| 0.3.1 | 2024-01-10 | [34084](https://github.com/airbytehq/airbyte/pull/34084) | Fix bug for running check with document file format | | 0.3.0 | 2023-12-14 | [33411](https://github.com/airbytehq/airbyte/pull/33411) | Bump CDK version to auto-set primary key for document file streams and support raw txt files | | 0.2.5 | 2023-12-06 | [33187](https://github.com/airbytehq/airbyte/pull/33187) | Bump CDK version to hide source-defined primary key | | 0.2.4 | 2023-11-16 | [32608](https://github.com/airbytehq/airbyte/pull/32608) | Improve document file type parser | From 177b0c33d7b49e8b2251ef6b3a7ef0bcd4fa1115 Mon Sep 17 00:00:00 2001 From: colesnodgrass Date: Wed, 10 Jan 2024 18:43:45 +0000 Subject: [PATCH 045/574] Bump Airbyte version from 0.50.42 to 0.50.43 --- .bumpversion.cfg | 2 +- docs/operator-guides/upgrading-airbyte.md | 2 +- gradle.properties | 2 +- run-ab-platform.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index d4aab85e4032c..9a2f045a1537d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.50.42 +current_version = 0.50.43 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-[a-z]+)? diff --git a/docs/operator-guides/upgrading-airbyte.md b/docs/operator-guides/upgrading-airbyte.md index 9810dc0a4b34f..6029020327f41 100644 --- a/docs/operator-guides/upgrading-airbyte.md +++ b/docs/operator-guides/upgrading-airbyte.md @@ -128,7 +128,7 @@ If you are upgrading from (i.e. your current version of Airbyte is) Airbyte vers Here's an example of what it might look like with the values filled in. It assumes that the downloaded `airbyte_archive.tar.gz` is in `/tmp`. ```bash - docker run --rm -v /tmp:/config airbyte/migration:0.50.42 --\ + docker run --rm -v /tmp:/config airbyte/migration:0.50.43 --\ --input /config/airbyte_archive.tar.gz\ --output /config/airbyte_archive_migrated.tar.gz ``` diff --git a/gradle.properties b/gradle.properties index 7ce1e172f71a0..4d7f4ccd22283 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION=0.50.42 +VERSION=0.50.43 # NOTE: some of these values are overwritten in CI! # NOTE: if you want to override this for your local machine, set overrides in ~/.gradle/gradle.properties diff --git a/run-ab-platform.sh b/run-ab-platform.sh index eb117f305b627..ffd721a29b640 100755 --- a/run-ab-platform.sh +++ b/run-ab-platform.sh @@ -1,6 +1,6 @@ #!/bin/bash -VERSION=0.50.42 +VERSION=0.50.43 # Run away from anything even a little scary set -o nounset # -u exit if a variable is not set set -o errexit # -f exit for any command failure" From f9bfd6df503efcc0115fce8da3318a765fb1ab16 Mon Sep 17 00:00:00 2001 From: Alexandre Cuoci Date: Wed, 10 Jan 2024 13:56:58 -0500 Subject: [PATCH 046/574] MongoDB Oplog Troubleshooting (#34122) --- docs/integrations/sources/mongodb-v2.md | 57 +++++++++++++------------ 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/docs/integrations/sources/mongodb-v2.md b/docs/integrations/sources/mongodb-v2.md index 180937e0162a1..bc9ffb1820e29 100644 --- a/docs/integrations/sources/mongodb-v2.md +++ b/docs/integrations/sources/mongodb-v2.md @@ -128,30 +128,6 @@ on discovering a self-hosted deployment connection string. To configure the Airbyte MongoDB source, use the database credentials and connection string from steps 1 and 2, respectively. The source will test the connection to the MongoDB instance upon creation. -### Upgrade From Previous Version - -:::caution - -The 1.0.0 version of the MongoDB V2 source connector contains breaking changes from previous versions of the connector. - -::: - -The quickest upgrade path is to click upgrade on any out-of-date connection in the UI. These connections will display -the following message banner: - -> **Action Required** -> There is a pending upgrade for **MongoDB**. -> -> **Version 1.0.0:** -> **We advise against upgrading until you have run a test upgrade as outlined [here](https://docs.airbyte.com/integrations/sources/mongodb-v2-migrations).** This version brings a host of updates to the MongoDB source connector, significantly increasing its scalability and reliability, especially for large collections. As of this version with checkpointing, [CDC incremental updates](https://docs.airbyte.com/understanding-airbyte/cdc) and improved schema discovery, this connector is also now [certified](https://docs.airbyte.com/integrations/). Selecting `Upgrade` will upgrade **all** connections using this source, require you to reconfigure the source, then run a full reset on **all** of your connections. -> -> Upgrade **MongoDB** by **Dec 1, 2023** to continue syncing with this source. For more information, see this [guide](https://docs.airbyte.com/integrations/sources/mongodb-v2). - -After upgrading to the latest version of the MongoDB V2 source connector, users will be required to manually re-configure -existing MongoDB V2 source connector configurations. The required [configuration parameter](#configuration-parameters) values can be discovered -using the [quick start](#quick-start) steps in this documentation. - - ## Replication Methods The MongoDB source utilizes change data capture (CDC) as a reliable way to keep your data up to date. @@ -180,16 +156,43 @@ When Schema is not enforced there is not way to deselect fields as all fields ar ## Limitations & Troubleshooting +### MongoDB Oplog and Change Streams + +[MongoDB's Change Streams](https://www.mongodb.com/docs/manual/changeStreams/) are based on the [Replica Set Oplog](https://www.mongodb.com/docs/manual/core/replica-set-oplog/). This has retention limitations. Syncs that run less frequently than the retention period of the Oplog may encounter issues with missing data. + +We recommend adjusting the Oplog size for your MongoDB cluster to ensure it holds at least 24 hours of changes. For optimal results, we suggest expanding it to maintain a week's worth of data. To adjust your Oplog size, see the corresponding tutorials for [MongoDB Atlas](https://www.mongodb.com/docs/atlas/cluster-additional-settings/#set-oplog-size) (fully-managed) and [MongoDB shell](https://www.mongodb.com/docs/manual/tutorial/change-oplog-size/) (self-hosted). + +If you are running into an issue similar to "invalid resume token", it may mean you need to: +1. Increase the Oplog retention period. +2. Increase the Oplog size. +3. Increase the Airbyte sync frequency. + +You can run the commands outlined [in this tutorial](https://www.mongodb.com/docs/manual/tutorial/troubleshoot-replica-sets/#check-the-size-of-the-oplog) to verify the current of your Oplog. The expect output is: + +```yaml +configured oplog size: 10.10546875MB +log length start to end: 94400 (26.22hrs) +oplog first event time: Mon Mar 19 2012 13:50:38 GMT-0400 (EDT) +oplog last event time: Wed Oct 03 2012 14:59:10 GMT-0400 (EDT) +now: Wed Oct 03 2012 15:00:21 GMT-0400 (EDT) +``` + +When importing a large MongoDB collection for the first time, the import duration might exceed the Oplog retention period. The Oplog is crucial for incremental updates, and an invalid resume token will require the MongoDB collection to be re-imported to ensure no source updates were missed. + +### Supported MongoDB Clusters + * Only supports [replica set](https://www.mongodb.com/docs/manual/replication/) cluster type. -* Schema discovery uses [sampling](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sample/) of the documents to collect all distinct top-level fields. This value is universally applied to all collections discovered in the target database. The approach is modelled after [MongoDB Compass sampling](https://www.mongodb.com/docs/compass/current/sampling/) and is used for efficiency. By default, 10,000 documents are sampled. This value can be increased up to 100,000 documents to increase the likelihood that all fields will be discovered. However, the trade-off is time, as a higher value will take the process longer to sample the collection. -* When Running with Schema Enforced set to `false` there is no attempt to discover any schema. See more in [Schema Enforcement](#Schema-Enforcement). * TLS/SSL is required by this connector. TLS/SSL is enabled by default for MongoDB Atlas clusters. To enable TSL/SSL connection for a self-hosted MongoDB instance, please refer to [MongoDb Documentation](https://docs.mongodb.com/manual/tutorial/configure-ssl/). * Views, capped collections and clustered collections are not supported. * Empty collections are excluded from schema discovery. * Collections with different data types for the values in the `_id` field among the documents in a collection are not supported. All `_id` values within the collection must be the same data type. -* [MongoDB's change streams](https://www.mongodb.com/docs/manual/changeStreams/) are based on the [Replica Set Oplog](https://www.mongodb.com/docs/manual/core/replica-set-oplog/), which has retention limitations. Syncs that run less frequently than the retention period of the oplog may encounter issues with missing data. * Atlas DB cluster are only supported in a dedicated M10 tier and above. Lower tiers may fail during connection setup. +### Schema Discovery & Enforcement + +* Schema discovery uses [sampling](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sample/) of the documents to collect all distinct top-level fields. This value is universally applied to all collections discovered in the target database. The approach is modelled after [MongoDB Compass sampling](https://www.mongodb.com/docs/compass/current/sampling/) and is used for efficiency. By default, 10,000 documents are sampled. This value can be increased up to 100,000 documents to increase the likelihood that all fields will be discovered. However, the trade-off is time, as a higher value will take the process longer to sample the collection. +* When Running with Schema Enforced set to `false` there is no attempt to discover any schema. See more in [Schema Enforcement](#Schema-Enforcement). + ## Configuration Parameters | Parameter Name | Description | From 44f9f2b46276001a4addc09b05484f3e7839cda0 Mon Sep 17 00:00:00 2001 From: Christo Grabowski <108154848+ChristoGrab@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:03:20 -0500 Subject: [PATCH 047/574] =?UTF-8?q?=F0=9F=9A=A8=F0=9F=9A=A8=20Source=20Mic?= =?UTF-8?q?rosoft=20Teams:=20Update=20Schemas=20(#33959)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../source-microsoft-teams/Dockerfile | 2 +- .../source-microsoft-teams/metadata.yaml | 10 +- .../sample_files/configured_catalog.json | 750 +----------------- .../source-microsoft-teams/setup.py | 2 +- .../source_microsoft_teams/client.py | 29 +- .../schemas/channel.json | 19 +- .../schemas/channel_members.json | 34 +- .../schemas/channel_message_replies.json | 203 +++-- .../schemas/channel_messages.json | 55 +- .../schemas/channel_tabs.json | 30 +- .../schemas/channels.json | 18 +- .../schemas/conversation_posts.json | 44 +- .../schemas/conversation_threads.json | 20 +- .../schemas/conversations.json | 26 +- .../schemas/group_members.json | 34 +- .../schemas/group_owners.json | 36 +- .../schemas/groups.json | 143 ++-- .../schemas/team_device_usage_report.json | 37 +- .../schemas/team_drives.json | 34 +- .../source_microsoft_teams/schemas/users.json | 31 +- .../source_microsoft_teams/spec.json | 8 +- .../sources/microsoft-teams-migrations.md | 38 + docs/integrations/sources/microsoft-teams.md | 43 +- 23 files changed, 495 insertions(+), 1151 deletions(-) create mode 100644 docs/integrations/sources/microsoft-teams-migrations.md diff --git a/airbyte-integrations/connectors/source-microsoft-teams/Dockerfile b/airbyte-integrations/connectors/source-microsoft-teams/Dockerfile index 3cdb20113e747..4b206258d0b3e 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/Dockerfile +++ b/airbyte-integrations/connectors/source-microsoft-teams/Dockerfile @@ -34,5 +34,5 @@ COPY source_microsoft_teams ./source_microsoft_teams ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] -LABEL io.airbyte.version=0.2.5 +LABEL io.airbyte.version=1.0.0 LABEL io.airbyte.name=airbyte/source-microsoft-teams diff --git a/airbyte-integrations/connectors/source-microsoft-teams/metadata.yaml b/airbyte-integrations/connectors/source-microsoft-teams/metadata.yaml index cf90d7d465495..a554cf83e32ee 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/metadata.yaml +++ b/airbyte-integrations/connectors/source-microsoft-teams/metadata.yaml @@ -2,7 +2,7 @@ data: connectorSubtype: api connectorType: source definitionId: eaf50f04-21dd-4620-913b-2a83f5635227 - dockerImageTag: 0.2.5 + dockerImageTag: 1.0.0 dockerRepository: airbyte/source-microsoft-teams githubIssueLabel: source-microsoft-teams icon: microsoft-teams.svg @@ -13,6 +13,14 @@ data: enabled: true oss: enabled: true + releases: + breakingChanges: + 1.0.0: + message: + Version 1.0.0 introduces breaking schema changes to all streams. + A full schema refresh is required to upgrade to this version. + For more details, see our migration guide. + upgradeDeadline: "2024-01-24" releaseStage: alpha documentationUrl: https://docs.airbyte.com/integrations/sources/microsoft-teams tags: diff --git a/airbyte-integrations/connectors/source-microsoft-teams/sample_files/configured_catalog.json b/airbyte-integrations/connectors/source-microsoft-teams/sample_files/configured_catalog.json index ca4a5fc9076cb..a395f1f9b46ae 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/sample_files/configured_catalog.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/sample_files/configured_catalog.json @@ -5,55 +5,7 @@ "name": "users", "supported_sync_modes": ["full_refresh"], "source_defined_cursor": false, - "json_schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "business_phones": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] - }, - "display_name": { - "type": ["null", "string"] - }, - "given_name": { - "type": ["null", "string"] - }, - "job_title": { - "type": ["null", "string"] - }, - "mail": { - "type": ["null", "string"] - }, - "mobile_phone": { - "type": ["null", "string"] - }, - "office_location": { - "type": ["null", "string"] - }, - "preferred_language": { - "type": ["null", "string"] - }, - "surname": { - "type": ["null", "string"] - }, - "user_principal_name": { - "type": ["null", "string"] - }, - "id": { - "type": ["null", "string"] - } - } - } + "json_schema": {} }, "sync_mode": "full_refresh", "destination_sync_mode": "overwrite" @@ -63,159 +15,7 @@ "name": "groups", "supported_sync_modes": ["full_refresh"], "source_defined_cursor": false, - "json_schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "id": { - "type": ["null", "string"] - }, - "deleted_date_time": { - "type": ["null", "string"], - "format": "date-time" - }, - "classification": { - "type": ["null", "string"] - }, - "created_date_time": { - "type": ["null", "string"], - "format": "date-time" - }, - "creation_options": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] - }, - "description": { - "type": ["null", "string"] - }, - "display_name": { - "type": ["null", "string"] - }, - "expiration_date_time": { - "type": ["null", "string"], - "format": "date-time" - }, - "group_types": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] - }, - "is_assignable_to_role": { - "type": ["null", "boolean"] - }, - "mail": { - "type": ["null", "string"] - }, - "mail_enabled": { - "type": ["null", "boolean"] - }, - "mail_nickname": { - "type": ["null", "string"] - }, - "membership_rule": { - "type": ["null", "string"] - }, - "membership_rule_processing_state": { - "type": ["null", "string"] - }, - "onPremises_domain_name": { - "type": ["null", "string"] - }, - "on_premises_last_sync_date_time": { - "type": ["null", "string"], - "format": "date-time" - }, - "on_premises_net_bios_name": { - "type": ["null", "string"] - }, - "on_premises_sam_account_name": { - "type": ["null", "string"] - }, - "on_premises_security_identifier": { - "type": ["null", "string"] - }, - "on_premises_sync_enabled": { - "type": ["null", "boolean"] - }, - "preferred_data_location": { - "type": ["null", "string"] - }, - "preferred_language": { - "type": ["null", "string"] - }, - "proxy_addresses": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] - }, - "renewed_date_time": { - "type": ["null", "string"], - "format": "date-time" - }, - "resource_behavior_options": { - "type": ["null", "array"], - "items": { - "type": ["null", "string"] - } - }, - "resource_provisioning_options": { - "type": ["null", "array"], - "items": { - "type": ["null", "string"] - } - }, - "security_enabled": { - "type": ["null", "boolean"] - }, - "security_edentifier": { - "type": ["null", "string"] - }, - "theme": { - "type": ["null", "string"] - }, - "visibility": { - "type": ["null", "string"] - }, - "on_premises_provisioning_errors": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] - } - } - } + "json_schema": {} }, "sync_mode": "full_refresh", "destination_sync_mode": "overwrite" @@ -225,55 +25,7 @@ "name": "group_members", "supported_sync_modes": ["full_refresh"], "source_defined_cursor": false, - "json_schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "id": { - "type": ["null", "string"] - }, - "business_phones": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] - }, - "display_name": { - "type": ["null", "string"] - }, - "given_name": { - "type": ["null", "string"] - }, - "job_title": { - "type": ["null", "string"] - }, - "mail": { - "type": ["null", "string"] - }, - "mobile_phone": { - "type": ["null", "string"] - }, - "office_location": { - "type": ["null", "string"] - }, - "preferred_language": { - "type": ["null", "string"] - }, - "surname": { - "type": ["null", "string"] - }, - "user_principal_name": { - "type": ["null", "string"] - } - } - } + "json_schema": {} }, "sync_mode": "full_refresh", "destination_sync_mode": "overwrite" @@ -283,58 +35,7 @@ "name": "group_owners", "supported_sync_modes": ["full_refresh"], "source_defined_cursor": false, - "json_schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "id": { - "type": ["null", "string"] - }, - "group_id": { - "type": ["null", "string"] - }, - "business_phones": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] - }, - "display_name": { - "type": ["null", "string"] - }, - "given_name": { - "type": ["null", "string"] - }, - "job_title": { - "type": ["null", "string"] - }, - "mail": { - "type": ["null", "string"] - }, - "mobile_phone": { - "type": ["null", "string"] - }, - "office_location": { - "type": ["null", "string"] - }, - "preferred_language": { - "type": ["null", "string"] - }, - "surname": { - "type": ["null", "string"] - }, - "user_principal_name": { - "type": ["null", "string"] - } - } - } + "json_schema": {} }, "sync_mode": "full_refresh", "destination_sync_mode": "overwrite" @@ -344,27 +45,7 @@ "name": "channels", "supported_sync_modes": ["full_refresh"], "source_defined_cursor": false, - "json_schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "id": { - "type": ["null", "string"] - }, - "display_name": { - "type": ["null", "string"] - }, - "description": { - "type": ["null", "string"] - }, - "email": { - "type": ["null", "string"] - }, - "web_url": { - "type": ["null", "string"] - } - } - } + "json_schema": {} }, "sync_mode": "full_refresh", "destination_sync_mode": "overwrite" @@ -374,40 +55,7 @@ "name": "channel_members", "supported_sync_modes": ["full_refresh"], "source_defined_cursor": false, - "json_schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "id": { - "type": ["null", "string"] - }, - "display_name": { - "type": ["null", "string"] - }, - "roles": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] - }, - "user_id": { - "type": ["null", "string"] - }, - "email": { - "type": ["null", "string"] - }, - "channel_id": { - "type": ["null", "string"] - } - } - } + "json_schema": {} }, "sync_mode": "full_refresh", "destination_sync_mode": "overwrite" @@ -417,72 +65,7 @@ "name": "channel_tabs", "supported_sync_modes": ["full_refresh"], "source_defined_cursor": false, - "json_schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "id": { - "type": ["null", "string"] - }, - "group_id": { - "type": ["null", "string"] - }, - "channel_id": { - "type": ["null", "string"] - }, - "display_name": { - "type": ["null", "string"] - }, - "web_url": { - "type": ["null", "string"] - }, - "sort_order_index": { - "type": ["null", "string"] - }, - "teams_app": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "id": { - "type": ["null", "string"] - }, - "display_name": { - "type": ["null", "string"] - }, - "distribution_method": { - "type": ["null", "string"] - } - } - }, - "configuration": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "entity_id": { - "type": ["null", "string"] - }, - "content_url": { - "type": ["null", "string"] - }, - "remove_url": { - "type": ["null", "string"] - }, - "website_url": { - "type": ["null", "string"] - }, - "wiki_tab_id": { - "type": ["null", "integer"] - }, - "wiki_default_tab": { - "type": ["null", "boolean"] - }, - "has_content": { - "type": ["null", "boolean"] - } - } - } - } - } + "json_schema": {} }, "sync_mode": "full_refresh", "destination_sync_mode": "overwrite" @@ -492,72 +75,7 @@ "name": "conversations", "supported_sync_modes": ["full_refresh"], "source_defined_cursor": false, - "json_schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "id": { - "type": ["null", "string"] - }, - "group_id": { - "type": ["null", "string"] - }, - "channel_id": { - "type": ["null", "string"] - }, - "display_name": { - "type": ["null", "string"] - }, - "web_url": { - "type": ["null", "string"] - }, - "sort_order_index": { - "type": ["null", "string"] - }, - "teams_app": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "id": { - "type": ["null", "string"] - }, - "display_name": { - "type": ["null", "string"] - }, - "distribution_method": { - "type": ["null", "string"] - } - } - }, - "configuration": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "entity_id": { - "type": ["null", "string"] - }, - "content_url": { - "type": ["null", "string"] - }, - "remove_url": { - "type": ["null", "string"] - }, - "website_url": { - "type": ["null", "string"] - }, - "wiki_tab_id": { - "type": ["null", "integer"] - }, - "wiki_default_tab": { - "type": ["null", "boolean"] - }, - "has_content": { - "type": ["null", "boolean"] - } - } - } - } - } + "json_schema": {} }, "sync_mode": "full_refresh", "destination_sync_mode": "overwrite" @@ -567,40 +85,7 @@ "name": "conversation_threads", "supported_sync_modes": ["full_refresh"], "source_defined_cursor": false, - "json_schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "id": { - "type": ["null", "string"] - }, - "group_id": { - "type": ["null", "string"] - }, - "conversation_id": { - "type": ["null", "string"] - }, - "topic": { - "type": ["null", "string"] - }, - "has_attachments": { - "type": ["null", "boolean"] - }, - "last_delivered_date_time": { - "type": ["null", "string"], - "format": "date-time" - }, - "unique_senders": { - "type": ["null", "string"] - }, - "preview": { - "type": ["null", "string"] - }, - "is_locked": { - "type": ["null", "boolean"] - } - } - } + "json_schema": {} }, "sync_mode": "full_refresh", "destination_sync_mode": "overwrite" @@ -610,100 +95,7 @@ "name": "conversation_posts", "supported_sync_modes": ["full_refresh"], "source_defined_cursor": false, - "json_schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "id": { - "type": ["null", "string"] - }, - "thread_id": { - "type": ["null", "string"] - }, - "conversation_id": { - "type": ["null", "string"] - }, - "created_date_time": { - "type": ["null", "string"], - "format": "date-time" - }, - "last_modified_date_time": { - "type": ["null", "string"], - "format": "date-time" - }, - "change_key": { - "type": ["null", "string"] - }, - "categories": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] - }, - "received_date_time": { - "type": ["null", "string"], - "format": "date-time" - }, - "has_attachments": { - "type": ["null", "boolean"] - }, - "body": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "content_type": { - "type": ["null", "string"] - }, - "content": { - "type": ["null", "string"] - } - } - }, - "from": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "emailAddress": { - "type": ["null", "object"], - "additionalProperties": false, - "properties": { - "name": { - "type": ["null", "string"] - }, - "address": { - "type": ["null", "string"] - } - } - } - } - }, - "sender": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "emailAddress": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "name": { - "type": ["null", "string"] - }, - "address": { - "type": ["null", "string"] - } - } - } - } - } - } - } + "json_schema": {} }, "sync_mode": "full_refresh", "destination_sync_mode": "overwrite" @@ -713,84 +105,7 @@ "name": "team_drives", "supported_sync_modes": ["full_refresh"], "source_defined_cursor": false, - "json_schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "id": { - "type": ["null", "string"] - }, - "last_modified_date_time": { - "type": ["null", "string"] - }, - "name": { - "type": ["null", "string"] - }, - "web_url": { - "type": ["null", "string"] - }, - "drive_type": { - "type": ["null", "string"] - }, - "created_by": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "user": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "display_name": { - "type": ["null", "string"] - } - } - } - } - }, - "owner": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "group": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "email": { - "type": ["null", "string"] - }, - "id": { - "type": ["null", "string"] - }, - "display_name": { - "type": ["null", "string"] - } - } - } - } - }, - "quota": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "deleted": { - "type": ["null", "integer"] - }, - "remaining": { - "type": ["null", "number"] - }, - "state": { - "type": ["null", "string"] - }, - "total": { - "type": ["null", "number"] - }, - "used": { - "type": ["null", "integer"] - } - } - } - } - } + "json_schema": {} }, "sync_mode": "full_refresh", "destination_sync_mode": "overwrite" @@ -800,48 +115,7 @@ "name": "team_device_usage_report", "supported_sync_modes": ["full_refresh"], "source_defined_cursor": false, - "json_schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "report_refresh_date": { - "type": ["null", "string"] - }, - "user_principal_name": { - "type": ["null", "string"] - }, - "last_activity_date": { - "type": ["null", "string"] - }, - "is_deleted": { - "type": ["null", "string"] - }, - "deleted_date": { - "type": ["null", "string"] - }, - "used_web": { - "type": ["null", "string"] - }, - "used_windows_phone": { - "type": ["null", "string"] - }, - "used_i_os": { - "type": ["null", "string"] - }, - "used_mac": { - "type": ["null", "string"] - }, - "used_android_phone": { - "type": ["null", "string"] - }, - "used_windows": { - "type": ["null", "string"] - }, - "report_period": { - "type": ["null", "string"] - } - } - } + "json_schema": {} }, "sync_mode": "full_refresh", "destination_sync_mode": "overwrite" diff --git a/airbyte-integrations/connectors/source-microsoft-teams/setup.py b/airbyte-integrations/connectors/source-microsoft-teams/setup.py index 1867013845c2c..6cc04d3f3b073 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/setup.py +++ b/airbyte-integrations/connectors/source-microsoft-teams/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages, setup MAIN_REQUIREMENTS = [ - "airbyte-cdk~=0.1", + "airbyte-cdk", "requests", "msal==1.7.0", "backoff", diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/client.py b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/client.py index 8a3d4893fa772..c16459025f986 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/client.py +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/client.py @@ -243,19 +243,24 @@ def get_team_device_usage_report(self): csv_response.readline() with io.TextIOWrapper(csv_response, encoding="utf-8-sig") as text_file: field_names = [ - "report_refresh_date", - "user_principal_name", - "last_activity_date", - "is_deleted", - "deleted_date", - "used_web", - "used_windows_phone", - "used_i_os", - "used_mac", - "used_android_phone", - "used_windows", - "report_period", + "reportRefreshDate", + "userId", + "userPrincipalName", + "lastActivityDate", + "isDeleted", + "deletedDate", + "usedWeb", + "usedWindowsPhone", + "usedIOs", + "usedMac", + "usedAndroidPhone", + "usedWindows", + "usedChromeOS", + "usedLinux", + "isLisenced", + "reportPeriod", ] + reader = csv.DictReader(text_file, fieldnames=field_names) for row in reader: yield [ diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel.json index 536c1efc8116f..b99d57b039407 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel.json @@ -6,21 +6,14 @@ "id": { "type": ["null", "string"] }, - "display_name": { + "displayName": { "type": ["null", "string"] }, "roles": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] + "type": ["null", "array"], + "items": { + "type": ["null", "string"] + } }, "description": { "type": ["null", "string"] @@ -28,7 +21,7 @@ "email": { "type": ["null", "string"] }, - "web_url": { + "webUrl": { "type": ["null", "string"] } } diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_members.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_members.json index 0f72d22d63c5c..3c236063c1c61 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_members.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_members.json @@ -6,30 +6,34 @@ "id": { "type": ["null", "string"] }, - "display_name": { + "@odata.type": { + "type": ["null", "string"] + }, + "displayName": { "type": ["null", "string"] }, "roles": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] - }, - "user_id": { + "type": ["null", "array"], + "items": { + "type": ["null", "string"] + } + }, + "userId": { "type": ["null", "string"] }, "email": { "type": ["null", "string"] }, - "channel_id": { + "channelId": { "type": ["null", "string"] + }, + "tenantId": { + "type": ["null", "string"] + }, + "visibleHistoryStartDateTime": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" } } } diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_message_replies.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_message_replies.json index 9e58d5f02fbe6..ac7cbb06d0023 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_message_replies.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_message_replies.json @@ -6,26 +6,34 @@ "id": { "type": ["null", "string"] }, - "reply_to_id": { + "replyToId": { "type": ["null", "string"] }, "etag": { "type": ["null", "string"] }, - "message_type": { + "messageType": { "type": ["null", "string"] }, - "created_date_time": { + "createdDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, - "last_modified_date_time": { + "lastModifiedDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, - "deleted_date_time": { + "lastEditedDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" + }, + "deletedDateTime": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, "subject": { "type": ["null", "string"] @@ -33,7 +41,7 @@ "summary": { "type": ["null", "string"] }, - "chat_id": { + "chatId": { "type": ["null", "string"] }, "importance": { @@ -42,10 +50,10 @@ "locale": { "type": ["null", "string"] }, - "web_url": { + "webUrl": { "type": ["null", "string"] }, - "policy_violation": { + "policyViolation": { "type": ["null", "string"] }, "from": { @@ -68,10 +76,13 @@ "id": { "type": ["null", "string"] }, - "display_name": { + "displayName": { "type": ["null", "string"] }, - "user_identity_type": { + "userIdentityType": { + "type": ["null", "string"] + }, + "tenantId": { "type": ["null", "string"] } } @@ -82,7 +93,7 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "content_type": { + "contentType": { "type": ["null", "string"] }, "content": { @@ -90,156 +101,136 @@ } } }, - "channel_identity": { + "channelIdentity": { "type": ["null", "object"], "additionalProperties": true, "properties": { "teamId": { "type": ["null", "string"] }, - "channel_id": { + "channelId": { "type": ["null", "string"] } } }, "attachments": { - "anyOf": [ - { - "type": "array", - "items": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "id": { - "type": ["null", "string"] - }, - "content_type": { - "type": ["null", "string"] - }, - "content_url": { - "type": ["null", "string"] - }, - "content": { - "type": ["null", "string"] - }, - "name": { - "type": ["null", "string"] - }, - "thumbnail_url": { - "type": ["null", "string"] - } - } + "type": ["null", "array"], + "items": { + "type": ["null", "object"], + "additionalProperties": true, + "properties": { + "id": { + "type": ["null", "string"] + }, + "contentType": { + "type": ["null", "string"] + }, + "contentUrl": { + "type": ["null", "string"] + }, + "content": { + "type": ["null", "string"] + }, + "name": { + "type": ["null", "string"] + }, + "thumbnailUrl": { + "type": ["null", "string"] } - }, - { - "type": "null" } - ] + } }, "mentions": { - "anyOf": [ - { - "type": "array", - "items": { + "type": ["null", "array"], + "items": { + "type": ["null", "object"], + "additionalProperties": true, + "properties": { + "id": { + "type": ["null", "integer"] + }, + "mentionText": { + "type": ["null", "string"] + }, + "mentioned": { "type": ["null", "object"], "additionalProperties": true, "properties": { - "id": { - "type": ["null", "integer"] + "application": { + "type": ["null", "string"] }, - "mention_text": { + "device": { "type": ["null", "string"] }, - "mentioned": { + "conversation": { + "type": ["null", "string"] + }, + "user": { "type": ["null", "object"], "additionalProperties": true, "properties": { - "application": { + "id": { "type": ["null", "string"] }, - "device": { + "displayName": { "type": ["null", "string"] }, - "conversation": { + "userIdentityType": { "type": ["null", "string"] - }, - "user": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "id": { - "type": ["null", "string"] - }, - "display_name": { - "type": ["null", "string"] - }, - "user_identity_type": { - "type": ["null", "string"] - } - } } } } } } - }, - { - "type": "null" } - ] + } }, "reactions": { - "anyOf": [ - { - "type": "array", - "items": { + "type": ["null", "array"], + "items": { + "type": ["null", "object"], + "additionalProperties": true, + "properties": { + "reactionType": { + "type": ["null", "string"] + }, + "createdDateTime": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" + }, + "user": { "type": ["null", "object"], "additionalProperties": true, "properties": { - "reaction_type": { + "application": { + "type": ["null", "string"] + }, + "device": { "type": ["null", "string"] }, - "created_date_time": { - "type": ["null", "string"], - "format": "date-time" + "conversation": { + "type": ["null", "string"] }, "user": { "type": ["null", "object"], "additionalProperties": true, "properties": { - "application": { + "id": { "type": ["null", "string"] }, - "device": { + "displayName": { "type": ["null", "string"] }, - "conversation": { + "userIdentityType": { "type": ["null", "string"] - }, - "user": { - "type": ["null", "object"], - "additionalProperties": true, - "properties": { - "id": { - "type": ["null", "string"] - }, - "display_name": { - "type": ["null", "string"] - }, - "user_identity_type": { - "type": ["null", "string"] - } - } } } } } } - }, - { - "type": "null" } - ] + } } } } diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_messages.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_messages.json index 3b72078dfbb90..ced93bf8121bb 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_messages.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_messages.json @@ -6,26 +6,29 @@ "id": { "type": ["null", "string"] }, - "reply_to_id": { + "replyToId": { "type": ["null", "string"] }, "etag": { "type": ["null", "string"] }, - "message_type": { + "messageType": { "type": ["null", "string"] }, - "created_date_time": { + "createdDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, - "lastModified_date_time": { + "lastModifiedDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, - "deleted_date_time": { + "deletedDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, "subject": { "type": ["null", "string"] @@ -33,7 +36,7 @@ "summary": { "type": ["null", "string"] }, - "chat_id": { + "chatId": { "type": ["null", "string"] }, "importance": { @@ -42,10 +45,10 @@ "locale": { "type": ["null", "string"] }, - "web_url": { + "webUrl": { "type": ["null", "string"] }, - "policy_violation": { + "policyViolation": { "type": ["null", "string"] }, "from": { @@ -68,10 +71,10 @@ "id": { "type": ["null", "string"] }, - "display_name": { + "displayName": { "type": ["null", "string"] }, - "user_identity_type": { + "userIdentityType": { "type": ["null", "string"] } } @@ -82,7 +85,7 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "content_type": { + "contentType": { "type": ["null", "string"] }, "content": { @@ -90,14 +93,14 @@ } } }, - "channel_identity": { + "channelIdentity": { "type": ["null", "object"], "additionalProperties": true, "properties": { "teamId": { "type": ["null", "string"] }, - "channel_id": { + "channelId": { "type": ["null", "string"] } } @@ -113,10 +116,10 @@ "id": { "type": ["null", "string"] }, - "content_type": { + "contentType": { "type": ["null", "string"] }, - "content_url": { + "contentUrl": { "type": ["null", "string"] }, "content": { @@ -125,7 +128,7 @@ "name": { "type": ["null", "string"] }, - "thumbnail_url": { + "thumbnailUrl": { "type": ["null", "string"] } } @@ -147,7 +150,7 @@ "id": { "type": ["null", "integer"] }, - "mention_text": { + "mentionText": { "type": ["null", "string"] }, "mentioned": { @@ -170,10 +173,10 @@ "id": { "type": ["null", "string"] }, - "display_name": { + "displayName": { "type": ["null", "string"] }, - "userIdentity_type": { + "userIdentityType": { "type": ["null", "string"] } } @@ -196,10 +199,10 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "reaction_type": { + "reactionType": { "type": ["null", "string"] }, - "created_date_time": { + "createdDateTime": { "type": ["null", "string"], "format": "date-time" }, @@ -223,10 +226,10 @@ "id": { "type": ["null", "string"] }, - "display_name": { + "displayName": { "type": ["null", "string"] }, - "user_identity_type": { + "userIdentityType": { "type": ["null", "string"] } } diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_tabs.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_tabs.json index c66b4bf72179c..ed0867b1e4c77 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_tabs.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channel_tabs.json @@ -6,32 +6,32 @@ "id": { "type": ["null", "string"] }, - "group_id": { + "groupId": { "type": ["null", "string"] }, - "channel_id": { + "channelId": { "type": ["null", "string"] }, - "display_name": { + "displayName": { "type": ["null", "string"] }, - "web_url": { + "webUrl": { "type": ["null", "string"] }, - "sort_order_index": { + "sortOrderIndex": { "type": ["null", "string"] }, - "teams_app": { + "teamsApp": { "type": ["null", "object"], "additionalProperties": true, "properties": { "id": { "type": ["null", "string"] }, - "display_name": { + "displayName": { "type": ["null", "string"] }, - "distribution_method": { + "distributionMethod": { "type": ["null", "string"] } } @@ -40,25 +40,25 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "entity_id": { + "entityId": { "type": ["null", "string"] }, - "content_url": { + "contentUrl": { "type": ["null", "string"] }, - "remove_url": { + "removeUrl": { "type": ["null", "string"] }, - "website_url": { + "websiteUrl": { "type": ["null", "string"] }, - "wiki_tab_id": { + "wikiTabId": { "type": ["null", "integer"] }, - "wiki_default_tab": { + "wikiDefaultTab": { "type": ["null", "boolean"] }, - "has_content": { + "hasContent": { "type": ["null", "boolean"] } } diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channels.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channels.json index 156390fc505ee..999eae607c23a 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channels.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/channels.json @@ -6,7 +6,12 @@ "id": { "type": ["null", "string"] }, - "display_name": { + "createdDateTime": { + "type": "string", + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" + }, + "displayName": { "type": ["null", "string"] }, "description": { @@ -15,7 +20,16 @@ "email": { "type": ["null", "string"] }, - "web_url": { + "isFavoriteByDefault": { + "type": ["null", "boolean"] + }, + "membershipType": { + "type": ["null", "string"] + }, + "tenantId": { + "type": ["null", "string"] + }, + "webUrl": { "type": ["null", "string"] } } diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/conversation_posts.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/conversation_posts.json index 24bf5cd4268e5..4389b581962aa 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/conversation_posts.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/conversation_posts.json @@ -6,48 +6,47 @@ "id": { "type": ["null", "string"] }, - "thread_id": { + "threadId": { "type": ["null", "string"] }, - "conversation_id": { + "conversationId": { "type": ["null", "string"] }, - "created_date_time": { + "createdDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, - "last_modified_date_time": { + "lastModifiedDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, - "change_key": { + "@odata.etag": { + "type": ["null", "string"] + }, + "changeKey": { "type": ["null", "string"] }, "categories": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] + "type": ["null", "array"], + "items": { + "type": ["null", "string"] + } }, - "received_date_time": { + "receivedDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, - "has_attachments": { + "hasAttachments": { "type": ["null", "boolean"] }, "body": { "type": ["null", "object"], "additionalProperties": true, "properties": { - "content_type": { + "contentType": { "type": ["null", "string"] }, "content": { @@ -61,7 +60,6 @@ "properties": { "emailAddress": { "type": ["null", "object"], - "additionalProperties": false, "properties": { "name": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/conversation_threads.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/conversation_threads.json index 35078c5f4d891..54c27157062e4 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/conversation_threads.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/conversation_threads.json @@ -6,29 +6,33 @@ "id": { "type": ["null", "string"] }, - "group_id": { + "groupId": { "type": ["null", "string"] }, - "conversation_id": { + "conversationId": { "type": ["null", "string"] }, "topic": { "type": ["null", "string"] }, - "has_attachments": { + "hasAttachments": { "type": ["null", "boolean"] }, - "last_delivered_date_time": { + "lastDeliveredDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, - "unique_senders": { - "type": ["null", "string"] + "uniqueSenders": { + "type": ["null", "array"], + "items": { + "type": ["null", "string"] + } }, "preview": { "type": ["null", "string"] }, - "is_locked": { + "isLocked": { "type": ["null", "boolean"] } } diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/conversations.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/conversations.json index 58a88cffff10e..e9045284dd5cd 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/conversations.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/conversations.json @@ -6,31 +6,25 @@ "id": { "type": ["null", "string"] }, - "group_id": { + "groupId": { "type": ["null", "string"] }, "topic": { "type": ["null", "string"] }, - "has_attachments": { + "hasAttachments": { "type": ["null", "boolean"] }, - "last_delivered_date_time": { + "lastDeliveredDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, - "unique_senders": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] + "uniqueSenders": { + "type": ["null", "array"], + "items": { + "type": ["null", "string"] + } }, "preview": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/group_members.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/group_members.json index ef6a3bb260284..2bf02fd72977b 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/group_members.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/group_members.json @@ -6,44 +6,40 @@ "id": { "type": ["null", "string"] }, - "business_phones": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] + "@odata.type": { + "type": ["null", "string"] + }, + "businessPhones": { + "type": ["null", "array"], + "items": { + "type": ["null", "string"] + } }, - "display_name": { + "displayName": { "type": ["null", "string"] }, - "given_name": { + "givenName": { "type": ["null", "string"] }, - "job_title": { + "jobTitle": { "type": ["null", "string"] }, "mail": { "type": ["null", "string"] }, - "mobile_phone": { + "mobilePhone": { "type": ["null", "string"] }, - "office_location": { + "officeLocation": { "type": ["null", "string"] }, - "preferred_language": { + "preferredLanguage": { "type": ["null", "string"] }, "surname": { "type": ["null", "string"] }, - "user_principal_name": { + "userPrincipalName": { "type": ["null", "string"] } } diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/group_owners.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/group_owners.json index dc222c183354e..aa1b8915682d9 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/group_owners.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/group_owners.json @@ -6,47 +6,43 @@ "id": { "type": ["null", "string"] }, - "group_id": { + "groupId": { "type": ["null", "string"] }, - "business_phones": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] + "@odata.type": { + "type": ["null", "string"] + }, + "businessPhones": { + "type": ["null", "array"], + "items": { + "type": ["null", "string"] + } }, - "display_name": { + "displayName": { "type": ["null", "string"] }, - "given_name": { + "givenName": { "type": ["null", "string"] }, - "job_title": { + "jobTitle": { "type": ["null", "string"] }, "mail": { "type": ["null", "string"] }, - "mobile_phone": { + "mobilePhone": { "type": ["null", "string"] }, - "office_location": { + "officeLocation": { "type": ["null", "string"] }, - "preferred_language": { + "preferredLanguage": { "type": ["null", "string"] }, "surname": { "type": ["null", "string"] }, - "user_principal_name": { + "userPrincipalName": { "type": ["null", "string"] } } diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/groups.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/groups.json index a348b77436037..2876585b6fad0 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/groups.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/groups.json @@ -6,149 +6,144 @@ "id": { "type": ["null", "string"] }, - "deleted_date_time": { + "deletedDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, "classification": { "type": ["null", "string"] }, - "created_date_time": { + "createdDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, - "creation_options": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] + "creationOptions": { + "type": ["null", "array"], + "items": { + "type": ["null", "string"] + } }, "description": { "type": ["null", "string"] }, - "display_name": { + "displayName": { "type": ["null", "string"] }, - "expiration_date_time": { + "expirationDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, - "group_types": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] + "groupTypes": { + "type": ["null", "array"], + "items": { + "type": ["null", "string"] + } }, - "is_assignable_to_role": { + "isAssignableToRole": { "type": ["null", "boolean"] }, "mail": { "type": ["null", "string"] }, - "mail_enabled": { + "mailEnabled": { "type": ["null", "boolean"] }, - "mail_nickname": { + "mailNickname": { "type": ["null", "string"] }, - "membership_rule": { + "membershipRule": { "type": ["null", "string"] }, - "membership_rule_processing_state": { + "membershipRuleProcessingState": { "type": ["null", "string"] }, - "onPremises_domain_name": { + "onPremisesDomainName": { "type": ["null", "string"] }, - "on_premises_last_sync_date_time": { + "onPremisesLastSyncDateTime": { "type": ["null", "string"], "format": "date-time" }, - "on_premises_net_bios_name": { + "onPremisesNetBiosName": { "type": ["null", "string"] }, - "on_premises_sam_account_name": { + "onPremisesSamAccountName": { "type": ["null", "string"] }, - "on_premises_security_identifier": { + "onPremisesSecurityIdentifier": { "type": ["null", "string"] }, - "on_premises_sync_enabled": { + "onPremisesSyncEnabled": { "type": ["null", "boolean"] }, - "preferred_data_location": { + "preferredDataLocation": { "type": ["null", "string"] }, - "preferred_language": { + "preferredLanguage": { "type": ["null", "string"] }, - "proxy_addresses": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] + "proxyAddresses": { + "type": ["null", "array"], + "items": { + "type": ["null", "string"] + } }, - "renewed_date_time": { + "renewedDateTime": { "type": ["null", "string"], - "format": "date-time" + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, - "resource_behavior_options": { + "resourceBehaviorOptions": { "type": ["null", "array"], "items": { "type": ["null", "string"] } }, - "resource_provisioning_options": { + "resourceProvisioningOptions": { "type": ["null", "array"], "items": { "type": ["null", "string"] } }, - "security_enabled": { + "securityEnabled": { "type": ["null", "boolean"] }, - "security_edentifier": { + "securityIdentifier": { "type": ["null", "string"] }, + "serviceProvisioningErrors": { + "type": ["null", "array"], + "items": { + "type": ["null", "object"], + "properties": { + "createdDateTime": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" + }, + "isResolved": { + "type": ["null", "boolean"] + }, + "serviceInstance": { + "type": ["null", "string"] + } + } + } + }, "theme": { "type": ["null", "string"] }, "visibility": { "type": ["null", "string"] }, - "on_premises_provisioning_errors": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] + "onPremisesProvisioningErrors": { + "type": ["null", "array"], + "items": { + "type": ["null", "string"] + } } } } diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/team_device_usage_report.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/team_device_usage_report.json index 40066ae4fc4d6..8ae6a571f5d3b 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/team_device_usage_report.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/team_device_usage_report.json @@ -3,40 +3,53 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "report_refresh_date": { + "reportRefreshDate": { + "type": ["null", "string"], + "format": "date" + }, + "userId": { + "type": ["null", "string"] + }, + "userPrincipalName": { + "type": ["null", "string"] + }, + "lastActivityDate": { + "type": ["null", "string"] + }, + "isDeleted": { "type": ["null", "string"] }, - "user_principal_name": { + "deletedDate": { "type": ["null", "string"] }, - "last_activity_date": { + "usedWeb": { "type": ["null", "string"] }, - "is_deleted": { + "usedWindowsPhone": { "type": ["null", "string"] }, - "deleted_date": { + "usedIOs": { "type": ["null", "string"] }, - "used_web": { + "usedMac": { "type": ["null", "string"] }, - "used_windows_phone": { + "usedAndroidPhone": { "type": ["null", "string"] }, - "used_i_os": { + "usedWindows": { "type": ["null", "string"] }, - "used_mac": { + "usedChromeOS": { "type": ["null", "string"] }, - "used_android_phone": { + "usedLinux": { "type": ["null", "string"] }, - "used_windows": { + "isLisenced": { "type": ["null", "string"] }, - "report_period": { + "reportPeriod": { "type": ["null", "string"] } } diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/team_drives.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/team_drives.json index 0b39515c620ec..fbb40f7dcf972 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/team_drives.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/team_drives.json @@ -6,19 +6,41 @@ "id": { "type": ["null", "string"] }, - "last_modified_date_time": { + "createdDateTime": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" + }, + "description": { "type": ["null", "string"] }, + "lastModifiedBy": { + "type": ["null", "object"], + "additionalProperties": true, + "properties": { + "displayName": { + "type": ["null", "string"] + }, + "id": { + "type": ["null", "string"] + } + } + }, + "lastModifiedDateTime": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" + }, "name": { "type": ["null", "string"] }, - "web_url": { + "webUrl": { "type": ["null", "string"] }, - "drive_type": { + "driveType": { "type": ["null", "string"] }, - "created_by": { + "createdBy": { "type": ["null", "object"], "additionalProperties": true, "properties": { @@ -26,7 +48,7 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "display_name": { + "displayName": { "type": ["null", "string"] } } @@ -47,7 +69,7 @@ "id": { "type": ["null", "string"] }, - "display_name": { + "displayName": { "type": ["null", "string"] } } diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/users.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/users.json index b5853c0d396e9..e02d86a531062 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/users.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/schemas/users.json @@ -3,44 +3,37 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "business_phones": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] + "businessPhones": { + "type": ["null", "array"], + "items": { + "type": ["null", "string"] + } }, - "display_name": { + "displayName": { "type": ["null", "string"] }, - "given_name": { + "givenName": { "type": ["null", "string"] }, - "job_title": { + "jobTitle": { "type": ["null", "string"] }, "mail": { "type": ["null", "string"] }, - "mobile_phone": { + "mobilePhone": { "type": ["null", "string"] }, - "office_location": { + "officeLocation": { "type": ["null", "string"] }, - "preferred_language": { + "preferredLanguage": { "type": ["null", "string"] }, "surname": { "type": ["null", "string"] }, - "user_principal_name": { + "userPrincipalName": { "type": ["null", "string"] }, "id": { diff --git a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/spec.json b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/spec.json index ab4af0e7d1ed8..39de5a8b8a96d 100644 --- a/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/spec.json +++ b/airbyte-integrations/connectors/source-microsoft-teams/source_microsoft_teams/spec.json @@ -27,7 +27,6 @@ "client_secret", "refresh_token" ], - "additionalProperties": false, "properties": { "auth_type": { "type": "string", @@ -39,7 +38,8 @@ "tenant_id": { "title": "Directory (tenant) ID", "type": "string", - "description": "A globally unique identifier (GUID) that is different than your organization name or domain. Follow these steps to obtain: open one of the Teams where you belong inside the Teams Application -> Click on the … next to the Team title -> Click on Get link to team -> Copy the link to the team and grab the tenant ID form the URL" + "description": "A globally unique identifier (GUID) that is different than your organization name or domain. Follow these steps to obtain: open one of the Teams where you belong inside the Teams Application -> Click on the … next to the Team title -> Click on Get link to team -> Copy the link to the team and grab the tenant ID form the URL", + "airbyte_secret": true }, "client_id": { "title": "Client ID", @@ -64,7 +64,6 @@ "type": "object", "title": "Authenticate via Microsoft", "required": ["tenant_id", "client_id", "client_secret"], - "additionalProperties": false, "properties": { "auth_type": { "type": "string", @@ -76,7 +75,8 @@ "tenant_id": { "title": "Directory (tenant) ID", "type": "string", - "description": "A globally unique identifier (GUID) that is different than your organization name or domain. Follow these steps to obtain: open one of the Teams where you belong inside the Teams Application -> Click on the … next to the Team title -> Click on Get link to team -> Copy the link to the team and grab the tenant ID form the URL" + "description": "A globally unique identifier (GUID) that is different than your organization name or domain. Follow these steps to obtain: open one of the Teams where you belong inside the Teams Application -> Click on the … next to the Team title -> Click on Get link to team -> Copy the link to the team and grab the tenant ID form the URL", + "airbyte_secret": true }, "client_id": { "title": "Client ID", diff --git a/docs/integrations/sources/microsoft-teams-migrations.md b/docs/integrations/sources/microsoft-teams-migrations.md new file mode 100644 index 0000000000000..5610ecd721adf --- /dev/null +++ b/docs/integrations/sources/microsoft-teams-migrations.md @@ -0,0 +1,38 @@ +# Microsoft teams Migration Guide + +## Upgrading to 1.0.0 + +Version 1.0.0 of the Microsoft Teams source connector introduces breaking changes to the schemas of all streams. A full schema refresh is required to ensure a seamless upgrade to this version. + +### Refresh schemas and reset data + +1. Select **Connections** in the main navbar. +2. From the list of your existing connections, select the connection(s) affected by the update. +3. Select the **Replication** tab, then select **Refresh source schema**. + +:::note +Any detected schema changes will be listed for your review. Select **OK** when you are ready to proceed. +::: + +4. At the bottom of the page, select **Save changes**. + +:::caution +Depending on your destination, you may be prompted to **Reset all streams**. Although this step is not required to proceed, it is highly recommended for users who have selected `Full Refresh | Append` sync mode, as the updated schema may lead to inconsistencies in the data structure within the destination. +::: + +5. Select **Save connection**. This will reset the data in your destination (if selected) and initiate a fresh sync. + +For more information on resetting your data in Airbyte, see [this page](https://docs.airbyte.com/operator-guides/reset). + +### Changes in 1.0.0 + +- The naming convention for field names in previous versions used "snake_case", which is not aligned with the "camelCase" convention used by the Microsoft Graph API. For example: + +`user_id` -> `userId` +`created_date` -> `createdDate` + +With the update to "camelCase", fields that may have been unrecognized or omitted in earlier versions will now be properly mapped and included in the data synchronization process, enhancing the accuracy and completeness of your data. + +- The `team_device_usage_report` stream contained a fatal bug that could lead to crashes during syncs. You should now be able to reliably use this stream during syncs. + +- `Date` and `date-time` fields have been typed as airbyte_type `date` and `timestamp_without_timezone`, respectively. diff --git a/docs/integrations/sources/microsoft-teams.md b/docs/integrations/sources/microsoft-teams.md index 1adde32957789..cc3846a489d30 100644 --- a/docs/integrations/sources/microsoft-teams.md +++ b/docs/integrations/sources/microsoft-teams.md @@ -29,22 +29,24 @@ Some APIs aren't supported in v1.0, e.g. channel messages and channel messages r ### Data type mapping -| Integration Type | Airbyte Type | Notes | -| :--- | :--- | :--- | -| `string` | `string` | | -| `number` | `number` | | -| `array` | `array` | | -| `object` | `object` | | +| Integration Type | Airbyte Type | +| :--------------- | :--------------------------- | +| `string` | `string` | +| `number` | `number` | +| `date` | `date` | +| `datetime` | `timestamp_without_timezone` | +| `array` | `array` | +| `object` | `object` | ### Features -| Feature | Supported?\(Yes/No\) | Notes | -| :--- | :--- | :--- | -| Full Refresh Sync | Yes | | -| Incremental Sync | Coming soon | | -| Replicate Incremental Deletes | Coming soon | | -| SSL connection | Yes | | -| Namespaces | No | | +| Feature | Supported? | +| :---------------------------- | :--------- | +| Full Refresh Sync | Yes | +| Incremental Sync | No | +| Replicate Incremental Deletes | No | +| SSL connection | Yes | +| Namespaces | No | ### Performance considerations @@ -54,9 +56,9 @@ The connector is restricted by normal Microsoft Graph [requests limitation](http ### Requirements -* Application \(client\) ID +* Application \(client\) ID * Directory \(tenant\) ID -* Client secrets +* Client secrets ### Setup guide @@ -157,8 +159,9 @@ Token acquiring implemented by [instantiate](https://docs.microsoft.com/en-us/az ## CHANGELOG -| Version | Date | Pull Request | Subject | -|:--------|:-----------| :--- | :--- | -| 0.2.5 | 2021-12-14 | [8429](https://github.com/airbytehq/airbyte/pull/8429) | Update titles and descriptions | -| 0.2.4 | 2021-12-07 | [7807](https://github.com/airbytehq/airbyte/pull/7807) | Implement OAuth support | -| 0.2.3 | 2021-12-06 | [8469](https://github.com/airbytehq/airbyte/pull/8469) | Migrate to the CDK | +| Version | Date | Pull Request | Subject | +|:------- |:---------- | :------------------------------------------------------- | :----------------------------- | +| 1.0.0 | 2024-01-04 | [33959](https://github.com/airbytehq/airbyte/pull/33959) | Schema updates | +| 0.2.5 | 2021-12-14 | [8429](https://github.com/airbytehq/airbyte/pull/8429) | Update titles and descriptions | +| 0.2.4 | 2021-12-07 | [7807](https://github.com/airbytehq/airbyte/pull/7807) | Implement OAuth support | +| 0.2.3 | 2021-12-06 | [8469](https://github.com/airbytehq/airbyte/pull/8469) | Migrate to the CDK | From ff81b95ecc5134029ba07e6918b1d79023ca4958 Mon Sep 17 00:00:00 2001 From: Marius Posta Date: Wed, 10 Jan 2024 12:14:40 -0800 Subject: [PATCH 048/574] java cdk: remove wal2json support (#34119) --- airbyte-cdk/java/airbyte-cdk/README.md | 1 + .../airbyte-cdk/core/src/main/resources/version.properties | 2 +- .../internals/postgres/PostgresDebeziumStateUtil.java | 7 ------- .../airbyte/cdk/integrations/debezium/CdcSourceTest.java | 2 -- .../connectors/source-postgres/build.gradle | 2 +- .../connectors/source-postgres/metadata.yaml | 2 +- .../source/postgres/PostgresJdbcSourceAcceptanceTest.java | 2 +- docs/integrations/sources/postgres.md | 1 + 8 files changed, 6 insertions(+), 13 deletions(-) diff --git a/airbyte-cdk/java/airbyte-cdk/README.md b/airbyte-cdk/java/airbyte-cdk/README.md index 6a26ce9905357..363532eadfbcc 100644 --- a/airbyte-cdk/java/airbyte-cdk/README.md +++ b/airbyte-cdk/java/airbyte-cdk/README.md @@ -166,6 +166,7 @@ MavenLocal debugging steps: | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0.11.5 | 2024-01-10 | [\#34119](https://github.com/airbytehq/airbyte/pull/34119) | Remove wal2json support for postgres+debezium. | | 0.11.4 | 2024-01-09 | [\#33305](https://github.com/airbytehq/airbyte/pull/33305) | Source stats in incremental syncs | | 0.11.3 | 2023-01-09 | [\#33658](https://github.com/airbytehq/airbyte/pull/33658) | Always fail when debezium fails, even if it happened during the setup phase. | | 0.11.2 | 2024-01-09 | [\#33969](https://github.com/airbytehq/airbyte/pull/33969) | Destination state stats implementation | diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties b/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties index 630a01b8fa3b3..25d4002e6b5da 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties @@ -1 +1 @@ -version=0.11.4 +version=0.11.5 diff --git a/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/debezium/internals/postgres/PostgresDebeziumStateUtil.java b/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/debezium/internals/postgres/PostgresDebeziumStateUtil.java index 174c03893fa24..e94212bcce433 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/debezium/internals/postgres/PostgresDebeziumStateUtil.java +++ b/airbyte-cdk/java/airbyte-cdk/db-sources/src/main/java/io/airbyte/cdk/integrations/debezium/internals/postgres/PostgresDebeziumStateUtil.java @@ -135,13 +135,6 @@ private ChainedLogicalStreamBuilder addSlotOption(final String publicationName, if (pgConnection.haveMinimumServerVersion(140000)) { streamBuilder = streamBuilder.withSlotOption("messages", true); } - } else if (plugin.equalsIgnoreCase("wal2json")) { - streamBuilder = streamBuilder - .withSlotOption("pretty-print", 1) - .withSlotOption("write-in-chunks", 1) - .withSlotOption("include-xids", 1) - .withSlotOption("include-timestamp", 1) - .withSlotOption("include-not-null", "true"); } else { throw new RuntimeException("Unknown plugin value : " + plugin); } diff --git a/airbyte-cdk/java/airbyte-cdk/db-sources/src/testFixtures/java/io/airbyte/cdk/integrations/debezium/CdcSourceTest.java b/airbyte-cdk/java/airbyte-cdk/db-sources/src/testFixtures/java/io/airbyte/cdk/integrations/debezium/CdcSourceTest.java index d05d54254ebeb..a0ee71a226d0d 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-sources/src/testFixtures/java/io/airbyte/cdk/integrations/debezium/CdcSourceTest.java +++ b/airbyte-cdk/java/airbyte-cdk/db-sources/src/testFixtures/java/io/airbyte/cdk/integrations/debezium/CdcSourceTest.java @@ -142,8 +142,6 @@ protected ConfiguredAirbyteCatalog getConfiguredCatalog() { protected abstract void assertExpectedStateMessages(final List stateMessages); - protected abstract void assertExpectedStateMessagesWithTotalCount(final List stateMessages, final long totalRecordCount); - @BeforeEach protected void setup() { testdb = createTestDatabase(); diff --git a/airbyte-integrations/connectors/source-postgres/build.gradle b/airbyte-integrations/connectors/source-postgres/build.gradle index bc074430bd22d..6fadebd3ed372 100644 --- a/airbyte-integrations/connectors/source-postgres/build.gradle +++ b/airbyte-integrations/connectors/source-postgres/build.gradle @@ -13,7 +13,7 @@ java { } airbyteJavaConnector { - cdkVersionRequired = '0.7.7' + cdkVersionRequired = '0.11.5' features = ['db-sources'] useLocalCdk = false } diff --git a/airbyte-integrations/connectors/source-postgres/metadata.yaml b/airbyte-integrations/connectors/source-postgres/metadata.yaml index f9f23553d933d..23d5517fe512a 100644 --- a/airbyte-integrations/connectors/source-postgres/metadata.yaml +++ b/airbyte-integrations/connectors/source-postgres/metadata.yaml @@ -9,7 +9,7 @@ data: connectorSubtype: database connectorType: source definitionId: decd338e-5647-4c0b-adf4-da0e75f5a750 - dockerImageTag: 3.3.0 + dockerImageTag: 3.3.1 dockerRepository: airbyte/source-postgres documentationUrl: https://docs.airbyte.com/integrations/sources/postgres githubIssueLabel: source-postgres diff --git a/airbyte-integrations/connectors/source-postgres/src/test/java/io/airbyte/integrations/source/postgres/PostgresJdbcSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-postgres/src/test/java/io/airbyte/integrations/source/postgres/PostgresJdbcSourceAcceptanceTest.java index 5f7a16a915d12..705cf416fdc4d 100644 --- a/airbyte-integrations/connectors/source-postgres/src/test/java/io/airbyte/integrations/source/postgres/PostgresJdbcSourceAcceptanceTest.java +++ b/airbyte-integrations/connectors/source-postgres/src/test/java/io/airbyte/integrations/source/postgres/PostgresJdbcSourceAcceptanceTest.java @@ -684,7 +684,7 @@ protected List getExpectedAirbyteMessagesSecondSync(final String .withCursor("5") .withCursorRecordCount(1L); - expectedMessages.addAll(createExpectedTestMessages(List.of(state))); + expectedMessages.addAll(createExpectedTestMessages(List.of(state), 2)); return expectedMessages; } diff --git a/docs/integrations/sources/postgres.md b/docs/integrations/sources/postgres.md index 56cc7447728fe..9ad73aae10f85 100644 --- a/docs/integrations/sources/postgres.md +++ b/docs/integrations/sources/postgres.md @@ -291,6 +291,7 @@ According to Postgres [documentation](https://www.postgresql.org/docs/14/datatyp | Version | Date | Pull Request | Subject | |---------|------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 3.3.1 | 2024-01-10 | [34119](https://github.com/airbytehq/airbyte/pull/34119) | Adopt java CDK version 0.11.5. | | 3.3.0 | 2023-12-19 | [33437](https://github.com/airbytehq/airbyte/pull/33437) | Remove LEGACY state flag | | 3.2.27 | 2023-12-18 | [33605](https://github.com/airbytehq/airbyte/pull/33605) | Advance Postgres LSN for PG 14 & below. | | 3.2.26 | 2023-12-11 | [33027](https://github.com/airbytehq/airbyte/pull/32961) | Support for better debugging tools. | From cb8a2edce77d4d30d84a782ddd5bff670aaef8f0 Mon Sep 17 00:00:00 2001 From: Cynthia Yin Date: Wed, 10 Jan 2024 12:17:57 -0800 Subject: [PATCH 049/574] Destination Databricks: pin cloud + OSS version to 1.1.0 (#34125) --- .../connectors/destination-databricks/metadata.yaml | 2 ++ docs/integrations/destinations/databricks.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/airbyte-integrations/connectors/destination-databricks/metadata.yaml b/airbyte-integrations/connectors/destination-databricks/metadata.yaml index 5b414a96a17a0..8d7eeeb33ee73 100644 --- a/airbyte-integrations/connectors/destination-databricks/metadata.yaml +++ b/airbyte-integrations/connectors/destination-databricks/metadata.yaml @@ -11,8 +11,10 @@ data: registries: cloud: enabled: true + dockerImageTag: 1.1.0 # pinning due to CDK incompatibility, see https://github.com/airbytehq/alpha-beta-issues/issues/2596 oss: enabled: true + dockerImageTag: 1.1.0 # pinning due to CDK incompatibility, see https://github.com/airbytehq/alpha-beta-issues/issues/2596 releaseStage: alpha documentationUrl: https://docs.airbyte.com/integrations/destinations/databricks tags: diff --git a/docs/integrations/destinations/databricks.md b/docs/integrations/destinations/databricks.md index 39defbd2912ff..d39e64084c44e 100644 --- a/docs/integrations/destinations/databricks.md +++ b/docs/integrations/destinations/databricks.md @@ -345,7 +345,7 @@ Delta Lake tables are created. You may want to consult the tutorial on | Version | Date | Pull Request | Subject | | :------ | :--------- | :------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------- | -| 1.1.1 | 2024-01-03 | [#33924](https://github.com/airbytehq/airbyte/pull/33924) | Add new ap-southeast-3 AWS region | +| 1.1.1 | 2024-01-03 | [#33924](https://github.com/airbytehq/airbyte/pull/33924) | (incompatible with CDK, do not use) Add new ap-southeast-3 AWS region | | 1.1.0 | 2023-06-02 | [\#26942](https://github.com/airbytehq/airbyte/pull/26942) | Support schema evolution | | 1.0.2 | 2023-04-20 | [\#25366](https://github.com/airbytehq/airbyte/pull/25366) | Fix default catalog to be `hive_metastore` | | 1.0.1 | 2023-03-30 | [\#24657](https://github.com/airbytehq/airbyte/pull/24657) | Fix support for external tables on S3 | From 3be28afecbce1f130cd288c64697f5cd86fde586 Mon Sep 17 00:00:00 2001 From: Anatolii Yatsuk <35109939+tolik0@users.noreply.github.com> Date: Wed, 10 Jan 2024 23:12:35 +0200 Subject: [PATCH 050/574] =?UTF-8?q?=E2=9C=A8=20Source=20Facebook=20Marketi?= =?UTF-8?q?ng:=20Add=20support=20for=20multiple=20Account=20IDs=20(#33538)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration_tests/conftest.py | 7 +- .../integration_tests/expected_records.jsonl | 2 +- .../integration_tests/spec.json | 19 +- .../source-facebook-marketing/metadata.yaml | 2 +- .../source_facebook_marketing/api.py | 16 +- .../config_migrations.py | 82 ++++++ .../source_facebook_marketing/run.py | 2 + .../schemas/activities.json | 3 + .../schemas/videos.json | 3 + .../source_facebook_marketing/source.py | 41 ++- .../source_facebook_marketing/spec.py | 14 +- .../streams/async_job_manager.py | 5 +- .../streams/base_insight_streams.py | 173 +++++++----- .../streams/base_streams.py | 140 ++++++++-- .../streams/streams.py | 95 ++++--- .../unit_tests/conftest.py | 8 +- .../unit_tests/test_api.py | 2 +- .../unit_tests/test_async_job_manager.py | 28 +- .../unit_tests/test_base_insight_streams.py | 261 +++++++++++++----- .../unit_tests/test_base_streams.py | 53 +++- .../unit_tests/test_client.py | 42 +-- .../unit_tests/test_config_migrations.py | 87 ++++++ .../unit_tests/test_errors.py | 41 +-- .../test_migrations/test_new_config.json | 14 + .../test_migrations/test_old_config.json | 14 + .../test_migrations/test_upgraded_config.json | 15 + .../unit_tests/test_source.py | 14 +- .../unit_tests/test_streams.py | 27 +- .../sources/facebook-marketing.md | 259 ++++++++--------- 29 files changed, 1049 insertions(+), 420 deletions(-) create mode 100644 airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/config_migrations.py create mode 100644 airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_config_migrations.py create mode 100644 airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/test_new_config.json create mode 100644 airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/test_old_config.json create mode 100644 airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/test_upgraded_config.json diff --git a/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/conftest.py b/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/conftest.py index f58ee7224089d..b2e2c416bc3c9 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/conftest.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/conftest.py @@ -6,12 +6,15 @@ import json import pytest +from source_facebook_marketing.config_migrations import MigrateAccountIdToArray @pytest.fixture(scope="session", name="config") def config_fixture(): with open("secrets/config.json", "r") as config_file: - return json.load(config_file) + config = json.load(config_file) + migrated_config = MigrateAccountIdToArray.transform(config) + return migrated_config @pytest.fixture(scope="session", name="config_with_wrong_token") @@ -21,7 +24,7 @@ def config_with_wrong_token_fixture(config): @pytest.fixture(scope="session", name="config_with_wrong_account") def config_with_wrong_account_fixture(config): - return {**config, "account_id": "WRONG_ACCOUNT"} + return {**config, "account_ids": ["WRONG_ACCOUNT"]} @pytest.fixture(scope="session", name="config_with_include_deleted") diff --git a/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/expected_records.jsonl b/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/expected_records.jsonl index 6bed2dde636d5..92fc12d2f7103 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/expected_records.jsonl +++ b/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/expected_records.jsonl @@ -4,7 +4,7 @@ {"stream":"campaigns","data":{"id":"23846542053890398","account_id":"212551616838260","budget_rebalance_flag":false,"budget_remaining":0.0,"buying_type":"AUCTION","created_time":"2021-01-18T21:36:42-0800","configured_status":"PAUSED","effective_status":"PAUSED","name":"Fake Campaign 0","objective":"MESSAGES","smart_promotion_type":"GUIDED_CREATION","source_campaign_id":0.0,"special_ad_category":"NONE","start_time":"1969-12-31T15:59:59-0800","status":"PAUSED","updated_time":"2021-02-18T01:00:02-0800"},"emitted_at":1694795155769} {"stream": "custom_audiences", "data": {"id": "23853683587660398", "account_id": "212551616838260", "approximate_count_lower_bound": 4700, "approximate_count_upper_bound": 5500, "customer_file_source": "PARTNER_PROVIDED_ONLY", "data_source": {"type": "UNKNOWN", "sub_type": "ANYTHING", "creation_params": "[]"}, "delivery_status": {"code": 200, "description": "This audience is ready for use."}, "description": "Custom Audience-Web Traffic [ALL] - _copy", "is_value_based": false, "name": "Web Traffic [ALL] - _copy", "operation_status": {"code": 200, "description": "Normal"}, "permission_for_actions": {"can_edit": true, "can_see_insight": "True", "can_share": "True", "subtype_supports_lookalike": "True", "supports_recipient_lookalike": "False"}, "retention_days": 0, "subtype": "CUSTOM", "time_content_updated": 1679433484, "time_created": 1679433479, "time_updated": 1679433484}, "emitted_at": 1698925454024} {"stream":"ad_creatives","data":{"id":"23844568440620398","account_id":"212551616838260","actor_id":"112704783733939","asset_feed_spec":{"images":[{"adlabels":[{"name":"placement_asset_fb19ee1baacc68_1586830094862","id":"23844521781280398"}],"hash":"7394ffb578c53e8761b6498d3008725b","image_crops":{"191x100":[[0,411],[589,719]]}},{"adlabels":[{"name":"placement_asset_f1f518506ae7e68_1586830094842","id":"23844521781340398"}],"hash":"7394ffb578c53e8761b6498d3008725b","image_crops":{"100x100":[[12,282],[574,844]]}},{"adlabels":[{"name":"placement_asset_f311b79c14a30c_1586830094845","id":"23844521781330398"}],"hash":"7394ffb578c53e8761b6498d3008725b","image_crops":{"90x160":[[14,72],[562,1046]]}},{"adlabels":[{"name":"placement_asset_f2c2fe4f20af66c_1586830157386","id":"23844521783780398"}],"hash":"7394ffb578c53e8761b6498d3008725b","image_crops":{"90x160":[[0,0],[589,1047]]}}],"bodies":[{"adlabels":[{"name":"placement_asset_f2d65f15340e594_1586830094852","id":"23844521781260398"},{"name":"placement_asset_f1f97c3e3a63d74_1586830094858","id":"23844521781300398"},{"name":"placement_asset_f14cee2ab5d786_1586830094863","id":"23844521781370398"},{"name":"placement_asset_f14877915fb5acc_1586830157387","id":"23844521783760398"}],"text":""}],"call_to_action_types":["LEARN_MORE"],"descriptions":[{"text":"Unmatched attribution, ad performances, and lead conversion, by unlocking your ad-blocked traffic across all your tools."}],"link_urls":[{"adlabels":[{"name":"placement_asset_f309294689f2c6c_1586830094864","id":"23844521781290398"},{"name":"placement_asset_f136a02466f2bc_1586830094856","id":"23844521781310398"},{"name":"placement_asset_fa79b032b68274_1586830094860","id":"23844521781320398"},{"name":"placement_asset_f28a128696c7428_1586830157387","id":"23844521783790398"}],"website_url":"http://dataline.io/","display_url":""}],"titles":[{"adlabels":[{"name":"placement_asset_f1013e29f89c38_1586830094864","id":"23844521781350398"},{"name":"placement_asset_fcb53b78a11574_1586830094859","id":"23844521781360398"},{"name":"placement_asset_f1a3b3d525f4998_1586830094854","id":"23844521781380398"},{"name":"placement_asset_f890656071c9ac_1586830157387","id":"23844521783770398"}],"text":"Unblock all your adblocked traffic"}],"ad_formats":["AUTOMATIC_FORMAT"],"asset_customization_rules":[{"customization_spec":{"age_max":65,"age_min":13,"publisher_platforms":["instagram","audience_network","messenger"],"instagram_positions":["story"],"messenger_positions":["story"],"audience_network_positions":["classic"]},"image_label":{"name":"placement_asset_f311b79c14a30c_1586830094845","id":"23844521781330398"},"body_label":{"name":"placement_asset_f1f97c3e3a63d74_1586830094858","id":"23844521781300398"},"link_url_label":{"name":"placement_asset_fa79b032b68274_1586830094860","id":"23844521781320398"},"title_label":{"name":"placement_asset_fcb53b78a11574_1586830094859","id":"23844521781360398"},"priority":1},{"customization_spec":{"age_max":65,"age_min":13,"publisher_platforms":["facebook"],"facebook_positions":["right_hand_column","instant_article","search"]},"image_label":{"name":"placement_asset_fb19ee1baacc68_1586830094862","id":"23844521781280398"},"body_label":{"name":"placement_asset_f14cee2ab5d786_1586830094863","id":"23844521781370398"},"link_url_label":{"name":"placement_asset_f309294689f2c6c_1586830094864","id":"23844521781290398"},"title_label":{"name":"placement_asset_f1013e29f89c38_1586830094864","id":"23844521781350398"},"priority":2},{"customization_spec":{"age_max":65,"age_min":13,"publisher_platforms":["facebook"],"facebook_positions":["story"]},"image_label":{"name":"placement_asset_f2c2fe4f20af66c_1586830157386","id":"23844521783780398"},"body_label":{"name":"placement_asset_f14877915fb5acc_1586830157387","id":"23844521783760398"},"link_url_label":{"name":"placement_asset_f28a128696c7428_1586830157387","id":"23844521783790398"},"title_label":{"name":"placement_asset_f890656071c9ac_1586830157387","id":"23844521783770398"},"priority":3},{"customization_spec":{"age_max":65,"age_min":13},"image_label":{"name":"placement_asset_f1f518506ae7e68_1586830094842","id":"23844521781340398"},"body_label":{"name":"placement_asset_f2d65f15340e594_1586830094852","id":"23844521781260398"},"link_url_label":{"name":"placement_asset_f136a02466f2bc_1586830094856","id":"23844521781310398"},"title_label":{"name":"placement_asset_f1a3b3d525f4998_1586830094854","id":"23844521781380398"},"priority":4}],"optimization_type":"PLACEMENT","reasons_to_shop":false,"shops_bundle":false,"additional_data":{"multi_share_end_card":false,"is_click_to_message":false}},"effective_object_story_id":"112704783733939_117519556585795","name":"{{product.name}} 2020-04-21-49cbe5bd90ed9861ea68bb38f7d6fc7c","instagram_actor_id":"3437258706290825","object_story_spec":{"page_id":"112704783733939","instagram_actor_id":"3437258706290825"},"object_type":"SHARE","status":"ACTIVE","thumbnail_url":"https://scontent-dus1-1.xx.fbcdn.net/v/t45.1600-4/93287504_23844521781140398_125048020067680256_n.jpg?_nc_cat=108&ccb=1-7&_nc_sid=a3999f&_nc_ohc=-TT4Z0FkPeYAX97qejq&_nc_ht=scontent-dus1-1.xx&edm=AAT1rw8EAAAA&stp=c0.5000x0.5000f_dst-emg0_p64x64_q75&ur=58080a&oh=00_AfBjMrayWFyOLmIgVt8Owtv2fBSJVyCmtNuPLpCQyggdpg&oe=64E18154"},"emitted_at":1692180825964} -{"stream":"activities","data":{"actor_id":"122043039268043192","actor_name":"Payments RTU Processor","application_id":"0","date_time_in_timezone":"03/13/2023 at 6:30 AM","event_time":"2023-03-13T13:30:47+0000","event_type":"ad_account_billing_charge","extra_data":"{\"currency\":\"USD\",\"new_value\":1188,\"transaction_id\":\"5885578541558696-11785530\",\"action\":67,\"type\":\"payment_amount\"}","object_id":"212551616838260","object_name":"Airbyte","object_type":"ACCOUNT","translated_event_type":"Account billed"},"emitted_at":1696931251153} +{"stream":"activities","data":{"account_id":"212551616838260","actor_id":"122043039268043192","actor_name":"Payments RTU Processor","application_id":"0","date_time_in_timezone":"03/13/2023 at 6:30 AM","event_time":"2023-03-13T13:30:47+0000","event_type":"ad_account_billing_charge","extra_data":"{\"currency\":\"USD\",\"new_value\":1188,\"transaction_id\":\"5885578541558696-11785530\",\"action\":67,\"type\":\"payment_amount\"}","object_id":"212551616838260","object_name":"Airbyte","object_type":"ACCOUNT","translated_event_type":"Account billed"},"emitted_at":1696931251153} {"stream":"custom_conversions","data":{"id":"694166388077667","account_id":"212551616838260","creation_time":"2020-04-22T01:36:00+0000","custom_event_type":"CONTACT","data_sources":[{"id":"2667253716886462","source_type":"PIXEL","name":"Dataline's Pixel"}],"default_conversion_value":0,"event_source_type":"pixel","is_archived":true,"is_unavailable":false,"name":"SubscribedButtonClick","retention_days":0,"rule":"{\"and\":[{\"event\":{\"eq\":\"PageView\"}},{\"or\":[{\"URL\":{\"i_contains\":\"SubscribedButtonClick\"}}]}]}"},"emitted_at":1692180839174} {"stream":"images","data":{"id":"212551616838260:c1e94a8768a405f0f212d71fe8336647","account_id":"212551616838260","name":"Audience_1_Ad_3_1200x1200_blue_CTA_arrow.png_105","creatives":["23853630775340398","23853630871360398","23853666124200398"],"original_height":1200,"original_width":1200,"permalink_url":"https://www.facebook.com/ads/image/?d=AQIDNjjLb7VzVJ26jXb_HpudCEUJqbV_lLF2JVsdruDcBxnXQEKfzzd21VVJnkm0B-JLosUXNNg1BH78y7FxnK3AH-0D_lnk7kn39_bIcOMK7Z9HYyFInfsVY__adup3A5zGTIcHC9Y98Je5qK-yD8F6","status":"ACTIVE","url":"https://scontent-dus1-1.xx.fbcdn.net/v/t45.1600-4/335907140_23853620220420398_4375584095210967511_n.png?_nc_cat=104&ccb=1-7&_nc_sid=2aac32&_nc_ohc=xdjrPpbRGNAAX8Dck01&_nc_ht=scontent-dus1-1.xx&edm=AJcBmwoEAAAA&oh=00_AfDCqQ6viqrgLcfbO3O5-n030Usq7Zyt2c1TmsatqnYf7Q&oe=64E2779A","created_time":"2023-03-16T13:13:17-0700","hash":"c1e94a8768a405f0f212d71fe8336647","url_128":"https://scontent-dus1-1.xx.fbcdn.net/v/t45.1600-4/335907140_23853620220420398_4375584095210967511_n.png?stp=dst-png_s128x128&_nc_cat=104&ccb=1-7&_nc_sid=2aac32&_nc_ohc=xdjrPpbRGNAAX8Dck01&_nc_ht=scontent-dus1-1.xx&edm=AJcBmwoEAAAA&oh=00_AfAY50CMpox2s4w_f18IVx7sZuXlg4quF6YNIJJ8D4PZew&oe=64E2779A","is_associated_creatives_in_adgroups":true,"updated_time":"2023-03-17T08:09:56-0700","height":1200,"width":1200},"emitted_at":1692180839582} {"stream":"ads_insights","data":{"account_currency":"USD","account_id":"212551616838260","account_name":"Airbyte","actions":[{"action_destination":"244953057175777","action_target_id":"244953057175777","action_type":"page_engagement","value":3.0,"1d_click":3.0,"7d_click":3.0,"28d_click":3.0},{"action_destination":"244953057175777","action_target_id":"244953057175777","action_type":"post_engagement","value":3.0,"1d_click":3.0,"7d_click":3.0,"28d_click":3.0},{"action_destination":"244953057175777","action_target_id":"244953057175777","action_type":"link_click","value":3.0,"1d_click":3.0,"7d_click":3.0,"28d_click":3.0}],"ad_id":"23846765228310398","ad_name":"Airbyte Ad","adset_id":"23846765228280398","adset_name":"Vanilla awareness ad set","buying_type":"AUCTION","campaign_id":"23846765228240398","campaign_name":"Airbyte Awareness Campaign 1 (sherif)","clicks":3,"conversion_rate_ranking":"UNKNOWN","cost_per_estimated_ad_recallers":0.007,"cost_per_inline_link_click":0.396667,"cost_per_inline_post_engagement":0.396667,"cost_per_unique_click":0.396667,"cost_per_unique_inline_link_click":0.396667,"cpc":0.396667,"cpm":0.902199,"cpp":0.948207,"created_time":"2021-02-09","ctr":0.227445,"date_start":"2021-02-15","date_stop":"2021-02-15","engagement_rate_ranking":"UNKNOWN","estimated_ad_recall_rate":13.545817,"estimated_ad_recallers":170.0,"frequency":1.050996,"impressions":1319,"inline_link_click_ctr":0.227445,"inline_link_clicks":3,"inline_post_engagement":3,"instant_experience_clicks_to_open":1.0,"instant_experience_clicks_to_start":1.0,"objective":"BRAND_AWARENESS","optimization_goal":"AD_RECALL_LIFT","outbound_clicks":[{"action_destination":"244953057175777","action_target_id":"244953057175777","action_type":"outbound_click","value":3.0}],"quality_ranking":"UNKNOWN","reach":1255,"social_spend":0.0,"spend":1.19,"unique_actions":[{"action_destination":"244953057175777","action_target_id":"244953057175777","action_type":"page_engagement","value":3.0,"1d_click":3.0,"7d_click":3.0,"28d_click":3.0},{"action_destination":"244953057175777","action_target_id":"244953057175777","action_type":"post_engagement","value":3.0,"1d_click":3.0,"7d_click":3.0,"28d_click":3.0},{"action_destination":"244953057175777","action_target_id":"244953057175777","action_type":"link_click","value":3.0,"1d_click":3.0,"7d_click":3.0,"28d_click":3.0}],"unique_clicks":3,"unique_ctr":0.239044,"unique_inline_link_click_ctr":0.239044,"unique_inline_link_clicks":3,"unique_link_clicks_ctr":0.239044,"unique_outbound_clicks":[{"action_destination":"244953057175777","action_target_id":"244953057175777","action_type":"outbound_click","value":3.0}],"updated_time":"2021-08-27","video_play_curve_actions":[{"action_type":"video_view"}],"website_ctr":[{"action_type":"link_click","value":0.227445}],"wish_bid":0.0},"emitted_at":1682686057366} diff --git a/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/spec.json b/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/spec.json index 1719883cf5cab..1657aaeda2d0d 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/spec.json +++ b/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/spec.json @@ -5,14 +5,19 @@ "title": "Source Facebook Marketing", "type": "object", "properties": { - "account_id": { - "title": "Ad Account ID", - "description": "The Facebook Ad account ID to use when pulling data from the Facebook Marketing API. The Ad account ID number is in the account dropdown menu or in your browser's address bar of your Meta Ads Manager. See the docs for more information.", + "account_ids": { + "title": "Ad Account ID(s)", + "description": "The Facebook Ad account ID(s) to pull data from. The Ad account ID number is in the account dropdown menu or in your browser's address bar of your Meta Ads Manager. See the docs for more information.", "order": 0, - "pattern": "^[0-9]+$", - "pattern_descriptor": "1234567890", + "pattern_descriptor": "The Ad Account ID must be a number.", "examples": ["111111111111111"], - "type": "string" + "type": "array", + "minItems": 1, + "items": { + "pattern": "^[0-9]+$", + "type": "string" + }, + "uniqueItems": true }, "access_token": { "title": "Access Token", @@ -388,7 +393,7 @@ "type": "string" } }, - "required": ["account_id", "access_token"] + "required": ["account_ids", "access_token"] }, "supportsIncremental": true, "supported_destination_sync_modes": ["append"], diff --git a/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml b/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml index faf2e1fa31bcb..0bac6b20fc1de 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml +++ b/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: e7778cfc-e97c-4458-9ecb-b4f2bba8946c - dockerImageTag: 1.2.3 + dockerImageTag: 1.3.0 dockerRepository: airbyte/source-facebook-marketing documentationUrl: https://docs.airbyte.com/integrations/sources/facebook-marketing githubIssueLabel: source-facebook-marketing diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/api.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/api.py index e3a6c610e117a..61a171b9659d2 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/api.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/api.py @@ -6,10 +6,10 @@ import logging from dataclasses import dataclass from time import sleep +from typing import List import backoff import pendulum -from cached_property import cached_property from facebook_business import FacebookAdsApi from facebook_business.adobjects.adaccount import AdAccount from facebook_business.api import FacebookResponse @@ -173,8 +173,8 @@ def call( class API: """Simple wrapper around Facebook API""" - def __init__(self, account_id: str, access_token: str, page_size: int = 100): - self._account_id = account_id + def __init__(self, access_token: str, page_size: int = 100): + self._accounts = {} # design flaw in MyFacebookAdsApi requires such strange set of new default api instance self.api = MyFacebookAdsApi.init(access_token=access_token, crash_log=False) # adding the default page size from config to the api base class @@ -183,10 +183,12 @@ def __init__(self, account_id: str, access_token: str, page_size: int = 100): # set the default API client to Facebook lib. FacebookAdsApi.set_default_api(self.api) - @cached_property - def account(self) -> AdAccount: - """Find current account""" - return self._find_account(self._account_id) + def get_account(self, account_id: str) -> AdAccount: + """Get AdAccount object by id""" + if account_id in self._accounts: + return self._accounts[account_id] + self._accounts[account_id] = self._find_account(account_id) + return self._accounts[account_id] @staticmethod def _find_account(account_id: str) -> AdAccount: diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/config_migrations.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/config_migrations.py new file mode 100644 index 0000000000000..c8b6c7e109a20 --- /dev/null +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/config_migrations.py @@ -0,0 +1,82 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import logging +from typing import Any, List, Mapping + +from airbyte_cdk.config_observation import create_connector_config_control_message +from airbyte_cdk.entrypoint import AirbyteEntrypoint +from airbyte_cdk.sources import Source +from airbyte_cdk.sources.message import InMemoryMessageRepository, MessageRepository + +logger = logging.getLogger("airbyte_logger") + + +class MigrateAccountIdToArray: + """ + This class stands for migrating the config at runtime. + This migration is backwards compatible with the previous version, as new property will be created. + When falling back to the previous source version connector will use old property `account_id`. + + Starting from `1.3.0`, the `account_id` property is replaced with `account_ids` property, which is a list of strings. + """ + + message_repository: MessageRepository = InMemoryMessageRepository() + migrate_from_key: str = "account_id" + migrate_to_key: str = "account_ids" + + @classmethod + def should_migrate(cls, config: Mapping[str, Any]) -> bool: + """ + This method determines whether the config should be migrated to have the new structure for the `custom_reports`, + based on the source spec. + Returns: + > True, if the transformation is necessary + > False, otherwise. + > Raises the Exception if the structure could not be migrated. + """ + return False if config.get(cls.migrate_to_key) else True + + @classmethod + def transform(cls, config: Mapping[str, Any]) -> Mapping[str, Any]: + # transform the config + config[cls.migrate_to_key] = [config[cls.migrate_from_key]] + # return transformed config + return config + + @classmethod + def modify_and_save(cls, config_path: str, source: Source, config: Mapping[str, Any]) -> Mapping[str, Any]: + # modify the config + migrated_config = cls.transform(config) + # save the config + source.write_config(migrated_config, config_path) + # return modified config + return migrated_config + + @classmethod + def emit_control_message(cls, migrated_config: Mapping[str, Any]) -> None: + # add the Airbyte Control Message to message repo + cls.message_repository.emit_message(create_connector_config_control_message(migrated_config)) + # emit the Airbyte Control Message from message queue to stdout + for message in cls.message_repository._message_queue: + print(message.json(exclude_unset=True)) + + @classmethod + def migrate(cls, args: List[str], source: Source) -> None: + """ + This method checks the input args, should the config be migrated, + transform if neccessary and emit the CONTROL message. + """ + # get config path + config_path = AirbyteEntrypoint(source).extract_config(args) + # proceed only if `--config` arg is provided + if config_path: + # read the existing config + config = source.read_config(config_path) + # migration check + if cls.should_migrate(config): + cls.emit_control_message( + cls.modify_and_save(config_path, source, config), + ) diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py index c99ffb1439061..2e92663e42fd8 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py @@ -7,9 +7,11 @@ from airbyte_cdk.entrypoint import launch +from .config_migrations import MigrateAccountIdToArray from .source import SourceFacebookMarketing def run(): source = SourceFacebookMarketing() + MigrateAccountIdToArray.migrate(sys.argv[1:], source) launch(source, sys.argv[1:]) diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/schemas/activities.json b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/schemas/activities.json index 4b5c729e0686d..69a31b5f8b553 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/schemas/activities.json +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/schemas/activities.json @@ -1,5 +1,8 @@ { "properties": { + "account_id": { + "type": ["null", "string"] + }, "actor_id": { "type": ["null", "string"] }, diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/schemas/videos.json b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/schemas/videos.json index 9aa51674dd101..3a146978ada69 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/schemas/videos.json +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/schemas/videos.json @@ -1,5 +1,8 @@ { "properties": { + "account_id": { + "type": ["null", "string"] + }, "id": { "type": "string" }, diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/source.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/source.py index 3d900ec6d5192..65e8c057852a4 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/source.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/source.py @@ -76,6 +76,9 @@ def _validate_and_transform(self, config: Mapping[str, Any]): if config.end_date: config.end_date = pendulum.instance(config.end_date) + + config.account_ids = list(config.account_ids) + return config def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> Tuple[bool, Optional[Any]]: @@ -93,11 +96,20 @@ def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> if config.start_date and config.end_date < config.start_date: return False, "End date must be equal or after start date." - api = API(account_id=config.account_id, access_token=config.access_token, page_size=config.page_size) + api = API(access_token=config.access_token, page_size=config.page_size) + + for account_id in config.account_ids: + # Get Ad Account to check creds + logger.info(f"Attempting to retrieve information for account with ID: {account_id}") + ad_account = api.get_account(account_id=account_id) + logger.info(f"Successfully retrieved account information for account: {ad_account}") + + # make sure that we have valid combination of "action_breakdowns" and "breakdowns" parameters + for stream in self.get_custom_insights_streams(api, config): + stream.check_breakdowns(account_id=account_id) - # Get Ad Account to check creds - ad_account = api.account - logger.info(f"Select account {ad_account}") + except facebook_business.exceptions.FacebookRequestError as e: + return False, e._api_error_message except AirbyteTracedException as e: return False, f"{e.message}. Full error: {e.internal_message}" @@ -105,12 +117,6 @@ def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> except Exception as e: return False, f"Unexpected error: {repr(e)}" - # make sure that we have valid combination of "action_breakdowns" and "breakdowns" parameters - for stream in self.get_custom_insights_streams(api, config): - try: - stream.check_breakdowns() - except facebook_business.exceptions.FacebookRequestError as e: - return False, e._api_error_message return True, None def streams(self, config: Mapping[str, Any]) -> List[Type[Stream]]: @@ -124,22 +130,24 @@ def streams(self, config: Mapping[str, Any]) -> List[Type[Stream]]: config.start_date = validate_start_date(config.start_date) config.end_date = validate_end_date(config.start_date, config.end_date) - api = API(account_id=config.account_id, access_token=config.access_token, page_size=config.page_size) + api = API(access_token=config.access_token, page_size=config.page_size) # if start_date not specified then set default start_date for report streams to 2 years ago report_start_date = config.start_date or pendulum.now().add(years=-2) insights_args = dict( api=api, + account_ids=config.account_ids, start_date=report_start_date, end_date=config.end_date, insights_lookback_window=config.insights_lookback_window, insights_job_timeout=config.insights_job_timeout, ) streams = [ - AdAccount(api=api), + AdAccount(api=api, account_ids=config.account_ids), AdSets( api=api, + account_ids=config.account_ids, start_date=config.start_date, end_date=config.end_date, include_deleted=config.include_deleted, @@ -147,6 +155,7 @@ def streams(self, config: Mapping[str, Any]) -> List[Type[Stream]]: ), Ads( api=api, + account_ids=config.account_ids, start_date=config.start_date, end_date=config.end_date, include_deleted=config.include_deleted, @@ -154,6 +163,7 @@ def streams(self, config: Mapping[str, Any]) -> List[Type[Stream]]: ), AdCreatives( api=api, + account_ids=config.account_ids, fetch_thumbnail_images=config.fetch_thumbnail_images, page_size=config.page_size, ), @@ -179,6 +189,7 @@ def streams(self, config: Mapping[str, Any]) -> List[Type[Stream]]: AdsInsightsDemographicsGender(page_size=config.page_size, **insights_args), Campaigns( api=api, + account_ids=config.account_ids, start_date=config.start_date, end_date=config.end_date, include_deleted=config.include_deleted, @@ -186,16 +197,19 @@ def streams(self, config: Mapping[str, Any]) -> List[Type[Stream]]: ), CustomConversions( api=api, + account_ids=config.account_ids, include_deleted=config.include_deleted, page_size=config.page_size, ), CustomAudiences( api=api, + account_ids=config.account_ids, include_deleted=config.include_deleted, page_size=config.page_size, ), Images( api=api, + account_ids=config.account_ids, start_date=config.start_date, end_date=config.end_date, include_deleted=config.include_deleted, @@ -203,6 +217,7 @@ def streams(self, config: Mapping[str, Any]) -> List[Type[Stream]]: ), Videos( api=api, + account_ids=config.account_ids, start_date=config.start_date, end_date=config.end_date, include_deleted=config.include_deleted, @@ -210,6 +225,7 @@ def streams(self, config: Mapping[str, Any]) -> List[Type[Stream]]: ), Activities( api=api, + account_ids=config.account_ids, start_date=config.start_date, end_date=config.end_date, include_deleted=config.include_deleted, @@ -275,6 +291,7 @@ def get_custom_insights_streams(self, api: API, config: ConnectorConfig) -> List ) stream = AdsInsights( api=api, + account_ids=config.account_ids, name=f"Custom{insight.name}", fields=list(insight_fields), breakdowns=list(set(insight.breakdowns)), diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/spec.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/spec.py index be53cf51d84d7..951ce0a2a63c1 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/spec.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/spec.py @@ -5,11 +5,11 @@ import logging from datetime import datetime, timezone from enum import Enum -from typing import List, Optional +from typing import List, Optional, Set from airbyte_cdk.sources.config import BaseConfig from facebook_business.adobjects.adsinsights import AdsInsights -from pydantic import BaseModel, Field, PositiveInt +from pydantic import BaseModel, Field, PositiveInt, constr logger = logging.getLogger("airbyte") @@ -112,18 +112,18 @@ class ConnectorConfig(BaseConfig): class Config: title = "Source Facebook Marketing" - account_id: str = Field( - title="Ad Account ID", + account_ids: Set[constr(regex="^[0-9]+$")] = Field( + title="Ad Account ID(s)", order=0, description=( - "The Facebook Ad account ID to use when pulling data from the Facebook Marketing API. " + "The Facebook Ad account ID(s) to pull data from. " "The Ad account ID number is in the account dropdown menu or in your browser's address " 'bar of your Meta Ads Manager. ' 'See the docs for more information.' ), - pattern="^[0-9]+$", - pattern_descriptor="1234567890", + pattern_descriptor="The Ad Account ID must be a number.", examples=["111111111111111"], + min_items=1, ) access_token: str = Field( diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/async_job_manager.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/async_job_manager.py index 8bfcc6fe74afc..738507e4408bc 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/async_job_manager.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/async_job_manager.py @@ -32,13 +32,14 @@ class InsightAsyncJobManager: # limit is not reliable indicator of async workload capability we still have to use this parameter. MAX_JOBS_IN_QUEUE = 100 - def __init__(self, api: "API", jobs: Iterator[AsyncJob]): + def __init__(self, api: "API", jobs: Iterator[AsyncJob], account_id: str): """Init :param api: :param jobs: """ self._api = api + self._account_id = account_id self._jobs = iter(jobs) self._running_jobs = [] @@ -147,4 +148,4 @@ def _update_api_throttle_limit(self): respond with empty list of data so api use "x-fb-ads-insights-throttle" header to update current insights throttle limit. """ - self._api.account.get_insights() + self._api.get_account(account_id=self._account_id).get_insights() diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/base_insight_streams.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/base_insight_streams.py index eadef8012e5d7..c671a4b9b917b 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/base_insight_streams.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/base_insight_streams.py @@ -11,7 +11,6 @@ from airbyte_cdk.sources.streams.core import package_name_from_class from airbyte_cdk.sources.utils.schema_helpers import ResourceSchemaLoader from airbyte_cdk.utils import AirbyteTracedException -from cached_property import cached_property from facebook_business.exceptions import FacebookBadObjectError, FacebookRequestError from source_facebook_marketing.streams.async_job import AsyncJob, InsightAsyncJob from source_facebook_marketing.streams.async_job_manager import InsightAsyncJobManager @@ -70,7 +69,7 @@ def __init__( super().__init__(**kwargs) self._start_date = self._start_date.date() self._end_date = self._end_date.date() - self._fields = fields + self._custom_fields = fields if action_breakdowns_allow_empty: if action_breakdowns is not None: self.action_breakdowns = action_breakdowns @@ -87,9 +86,9 @@ def __init__( self.level = level # state - self._cursor_value: Optional[pendulum.Date] = None # latest period that was read - self._next_cursor_value = self._get_start_date() - self._completed_slices = set() + self._cursor_values: Optional[Mapping[str, pendulum.Date]] = None # latest period that was read for each account + self._next_cursor_values = self._get_start_date() + self._completed_slices = {account_id: set() for account_id in self._account_ids} @property def name(self) -> str: @@ -128,6 +127,8 @@ def read_records( ) -> Iterable[Mapping[str, Any]]: """Waits for current job to finish (slice) and yield its result""" job = stream_slice["insight_job"] + account_id = stream_slice["account_id"] + try: for obj in job.get_result(): data = obj.export_all_data() @@ -142,25 +143,30 @@ def read_records( except FacebookRequestError as exc: raise traced_exception(exc) - self._completed_slices.add(job.interval.start) - if job.interval.start == self._next_cursor_value: - self._advance_cursor() + self._completed_slices[account_id].add(job.interval.start) + if job.interval.start == self._next_cursor_values[account_id]: + self._advance_cursor(account_id) @property def state(self) -> MutableMapping[str, Any]: """State getter, the result can be stored by the source""" - if self._cursor_value: - return { - self.cursor_field: self._cursor_value.isoformat(), - "slices": [d.isoformat() for d in self._completed_slices], - "time_increment": self.time_increment, - } + new_state = {account_id: {} for account_id in self._account_ids} + + if self._cursor_values: + for account_id in self._account_ids: + if account_id in self._cursor_values and self._cursor_values[account_id]: + new_state[account_id] = {self.cursor_field: self._cursor_values[account_id].isoformat()} + + new_state[account_id]["slices"] = {d.isoformat() for d in self._completed_slices[account_id]} + new_state["time_increment"] = self.time_increment + return new_state if self._completed_slices: - return { - "slices": [d.isoformat() for d in self._completed_slices], - "time_increment": self.time_increment, - } + for account_id in self._account_ids: + new_state[account_id]["slices"] = {d.isoformat() for d in self._completed_slices[account_id]} + + new_state["time_increment"] = self.time_increment + return new_state return {} @@ -170,13 +176,23 @@ def state(self, value: Mapping[str, Any]): # if the time increment configured for this stream is different from the one in the previous state # then the previous state object is invalid and we should start replicating data from scratch # to achieve this, we skip setting the state - if value.get("time_increment", 1) != self.time_increment: + transformed_state = self._transform_state_from_old_format(value, ["time_increment"]) + if transformed_state.get("time_increment", 1) != self.time_increment: logger.info(f"Ignoring bookmark for {self.name} because of different `time_increment` option.") return - self._cursor_value = pendulum.parse(value[self.cursor_field]).date() if value.get(self.cursor_field) else None - self._completed_slices = set(pendulum.parse(v).date() for v in value.get("slices", [])) - self._next_cursor_value = self._get_start_date() + self._cursor_values = { + account_id: pendulum.parse(transformed_state[account_id][self.cursor_field]).date() + if transformed_state.get(account_id, {}).get(self.cursor_field) + else None + for account_id in self._account_ids + } + self._completed_slices = { + account_id: set(pendulum.parse(v).date() for v in transformed_state.get(account_id, {}).get("slices", [])) + for account_id in self._account_ids + } + + self._next_cursor_values = self._get_start_date() def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]): """Update stream state from latest record @@ -186,40 +202,47 @@ def get_updated_state(self, current_stream_state: MutableMapping[str, Any], late """ return self.state - def _date_intervals(self) -> Iterator[pendulum.Date]: + def _date_intervals(self, account_id: str) -> Iterator[pendulum.Date]: """Get date period to sync""" - if self._end_date < self._next_cursor_value: + if self._end_date < self._next_cursor_values[account_id]: return - date_range = self._end_date - self._next_cursor_value + date_range = self._end_date - self._next_cursor_values[account_id] yield from date_range.range("days", self.time_increment) - def _advance_cursor(self): + def _advance_cursor(self, account_id: str): """Iterate over state, find continuing sequence of slices. Get last value, advance cursor there and remove slices from state""" - for ts_start in self._date_intervals(): - if ts_start not in self._completed_slices: - self._next_cursor_value = ts_start + for ts_start in self._date_intervals(account_id): + if ts_start not in self._completed_slices[account_id]: + self._next_cursor_values[account_id] = ts_start break - self._completed_slices.remove(ts_start) - self._cursor_value = ts_start + self._completed_slices[account_id].remove(ts_start) + if self._cursor_values: + self._cursor_values[account_id] = ts_start + else: + self._cursor_values = {account_id: ts_start} - def _generate_async_jobs(self, params: Mapping) -> Iterator[AsyncJob]: + def _generate_async_jobs(self, params: Mapping, account_id: str) -> Iterator[AsyncJob]: """Generator of async jobs :param params: :return: """ - self._next_cursor_value = self._get_start_date() - for ts_start in self._date_intervals(): - if ts_start in self._completed_slices: + self._next_cursor_values = self._get_start_date() + for ts_start in self._date_intervals(account_id): + if ts_start in self._completed_slices.get(account_id, []): continue ts_end = ts_start + pendulum.duration(days=self.time_increment - 1) interval = pendulum.Period(ts_start, ts_end) yield InsightAsyncJob( - api=self._api.api, edge_object=self._api.account, interval=interval, params=params, job_timeout=self.insights_job_timeout + api=self._api.api, + edge_object=self._api.get_account(account_id=account_id), + interval=interval, + params=params, + job_timeout=self.insights_job_timeout, ) - def check_breakdowns(self): + def check_breakdowns(self, account_id: str): """ Making call to check "action_breakdowns" and "breakdowns" combinations https://developers.facebook.com/docs/marketing-api/insights/breakdowns#combiningbreakdowns @@ -229,7 +252,7 @@ def check_breakdowns(self): "breakdowns": self.breakdowns, "fields": ["account_id"], } - self._api.account.get_insights(params=params, is_async=False) + self._api.get_account(account_id=account_id).get_insights(params=params, is_async=False) def _response_data_is_valid(self, data: Iterable[Mapping[str, Any]]) -> bool: """ @@ -255,14 +278,19 @@ def stream_slices( if stream_state: self.state = stream_state - try: - manager = InsightAsyncJobManager(api=self._api, jobs=self._generate_async_jobs(params=self.request_params())) - for job in manager.completed_jobs(): - yield {"insight_job": job} - except FacebookRequestError as exc: - raise traced_exception(exc) + for account_id in self._account_ids: + try: + manager = InsightAsyncJobManager( + api=self._api, + jobs=self._generate_async_jobs(params=self.request_params(), account_id=account_id), + account_id=account_id, + ) + for job in manager.completed_jobs(): + yield {"insight_job": job, "account_id": account_id} + except FacebookRequestError as exc: + raise traced_exception(exc) - def _get_start_date(self) -> pendulum.Date: + def _get_start_date(self) -> Mapping[str, pendulum.Date]: """Get start date to begin sync with. It is not that trivial as it might seem. There are few rules: - don't read data older than start_date @@ -277,33 +305,42 @@ def _get_start_date(self) -> pendulum.Date: today = pendulum.today().date() oldest_date = today - self.INSIGHTS_RETENTION_PERIOD refresh_date = today - self.insights_lookback_period - if self._cursor_value: - start_date = self._cursor_value + pendulum.duration(days=self.time_increment) - if start_date > refresh_date: - logger.info( - f"The cursor value within refresh period ({self.insights_lookback_period}), start sync from {refresh_date} instead." + + start_dates_for_account = {} + for account_id in self._account_ids: + cursor_value = self._cursor_values.get(account_id) if self._cursor_values else None + if cursor_value: + start_date = cursor_value + pendulum.duration(days=self.time_increment) + if start_date > refresh_date: + logger.info( + f"The cursor value within refresh period ({self.insights_lookback_period}), start sync from {refresh_date} instead." + ) + start_date = min(start_date, refresh_date) + + if start_date < self._start_date: + logger.warning(f"Ignore provided state and start sync from start_date ({self._start_date}).") + start_date = max(start_date, self._start_date) + else: + start_date = self._start_date + if start_date < oldest_date: + logger.warning( + f"Loading insights older then {self.INSIGHTS_RETENTION_PERIOD} is not possible. Start sync from {oldest_date}." ) - start_date = min(start_date, refresh_date) + start_dates_for_account[account_id] = max(oldest_date, start_date) - if start_date < self._start_date: - logger.warning(f"Ignore provided state and start sync from start_date ({self._start_date}).") - start_date = max(start_date, self._start_date) - else: - start_date = self._start_date - if start_date < oldest_date: - logger.warning(f"Loading insights older then {self.INSIGHTS_RETENTION_PERIOD} is not possible. Start sync from {oldest_date}.") - return max(oldest_date, start_date) + return start_dates_for_account def request_params(self, **kwargs) -> MutableMapping[str, Any]: - return { + req_params = { "level": self.level, "action_breakdowns": self.action_breakdowns, "action_report_time": self.action_report_time, "breakdowns": self.breakdowns, - "fields": self.fields, + "fields": self.fields(), "time_increment": self.time_increment, "action_attribution_windows": self.action_attribution_windows, } + return req_params def _state_filter(self, stream_state: Mapping[str, Any]) -> Mapping[str, Any]: """Works differently for insights, so remove it""" @@ -315,19 +352,23 @@ def get_json_schema(self) -> Mapping[str, Any]: """ loader = ResourceSchemaLoader(package_name_from_class(self.__class__)) schema = loader.get_schema("ads_insights") - if self._fields: + if self._custom_fields: # 'date_stop' and 'account_id' are also returned by default, even if they are not requested - custom_fields = set(self._fields + [self.cursor_field, "date_stop", "account_id", "ad_id"]) + custom_fields = set(self._custom_fields + [self.cursor_field, "date_stop", "account_id", "ad_id"]) schema["properties"] = {k: v for k, v in schema["properties"].items() if k in custom_fields} if self.breakdowns: breakdowns_properties = loader.get_schema("ads_insights_breakdowns")["properties"] schema["properties"].update({prop: breakdowns_properties[prop] for prop in self.breakdowns}) return schema - @cached_property - def fields(self) -> List[str]: + def fields(self, **kwargs) -> List[str]: """List of fields that we want to query, for now just all properties from stream's schema""" + if self._custom_fields: + return self._custom_fields + if self._fields: return self._fields + schema = ResourceSchemaLoader(package_name_from_class(self.__class__)).get_schema("ads_insights") - return list(schema.get("properties", {}).keys()) + self._fields = list(schema.get("properties", {}).keys()) + return self._fields diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/base_streams.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/base_streams.py index 01b8488ca4ba7..9f396077df8a1 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/base_streams.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/base_streams.py @@ -12,7 +12,6 @@ from airbyte_cdk.sources.streams import Stream from airbyte_cdk.sources.streams.availability_strategy import AvailabilityStrategy from airbyte_cdk.sources.utils.transform import TransformConfig, TypeTransformer -from cached_property import cached_property from facebook_business.adobjects.abstractobject import AbstractObject from facebook_business.exceptions import FacebookRequestError from source_facebook_marketing.streams.common import traced_exception @@ -43,16 +42,20 @@ class FBMarketingStream(Stream, ABC): def availability_strategy(self) -> Optional["AvailabilityStrategy"]: return None - def __init__(self, api: "API", include_deleted: bool = False, page_size: int = 100, **kwargs): + def __init__(self, api: "API", account_ids: List[str], include_deleted: bool = False, page_size: int = 100, **kwargs): super().__init__(**kwargs) self._api = api + self._account_ids = account_ids self.page_size = page_size if page_size is not None else 100 self._include_deleted = include_deleted if self.enable_deleted else False + self._fields = None - @cached_property - def fields(self) -> List[str]: + def fields(self, **kwargs) -> List[str]: """List of fields that we want to query, for now just all properties from stream's schema""" - return list(self.get_json_schema().get("properties", {}).keys()) + if self._fields: + return self._fields + self._saved_fields = list(self.get_json_schema().get("properties", {}).keys()) + return self._saved_fields @classmethod def fix_date_time(cls, record): @@ -78,6 +81,70 @@ def fix_date_time(cls, record): for entry in record: cls.fix_date_time(entry) + @staticmethod + def add_account_id(record, account_id: str): + if "account_id" not in record: + record["account_id"] = account_id + + def get_account_state(self, account_id: str, stream_state: Mapping[str, Any] = None) -> MutableMapping[str, Any]: + """ + Retrieve the state for a specific account. + + If multiple account IDs are present, the state for the specific account ID + is returned if it exists in the stream state. If only one account ID is + present, the entire stream state is returned. + + :param account_id: The account ID for which to retrieve the state. + :param stream_state: The current stream state, optional. + :return: The state information for the specified account as a MutableMapping. + """ + if stream_state and account_id and account_id in stream_state: + account_state = stream_state.get(account_id) + + # copy `include_deleted` from general stream state + if "include_deleted" in stream_state: + account_state["include_deleted"] = stream_state["include_deleted"] + return account_state + elif len(self._account_ids) == 1: + return stream_state + else: + return {} + + def _transform_state_from_old_format(self, state: Mapping[str, Any], move_fields: List[str] = None) -> Mapping[str, Any]: + """ + Transforms the state from an old format to a new format based on account IDs. + + This method transforms the old state to be a dictionary where the keys are account IDs. + If the state is in the old format (not keyed by account IDs), it will transform the state + by nesting it under the account ID. + + :param state: The original state dictionary to transform. + :param move_fields: A list of field names whose values should be moved to the top level of the new state dictionary. + :return: The transformed state dictionary. + """ + + # If the state already contains any of the account IDs, return the state as is. + for account_id in self._account_ids: + if account_id in state: + return state + + # Handle the case where there is only one account ID. + # Transform the state by nesting it under the account ID. + if state and len(self._account_ids) == 1: + account_id = self._account_ids[0] + new_state = {account_id: state} + + # Move specified fields to the top level of the new state. + if move_fields: + for move_field in move_fields: + if move_field in state: + new_state[move_field] = state.pop(move_field) + + return new_state + + # If the state is empty or there are multiple account IDs, return an empty dictionary. + return {} + def read_records( self, sync_mode: SyncMode, @@ -86,15 +153,24 @@ def read_records( stream_state: Mapping[str, Any] = None, ) -> Iterable[Mapping[str, Any]]: """Main read method used by CDK""" + account_id = stream_slice["account_id"] + account_state = stream_slice.get("stream_state", {}) + try: - for record in self.list_objects(params=self.request_params(stream_state=stream_state)): + for record in self.list_objects(params=self.request_params(stream_state=account_state), account_id=account_id): if isinstance(record, AbstractObject): record = record.export_all_data() # convert FB object to dict self.fix_date_time(record) + self.add_account_id(record, stream_slice["account_id"]) yield record except FacebookRequestError as exc: raise traced_exception(exc) + def stream_slices(self, stream_state: Mapping[str, Any] = None, **kwargs) -> Iterable[Optional[Mapping[str, any]]]: + for account_id in self._account_ids: + account_state = self.get_account_state(account_id, stream_state) + yield {"account_id": account_id, "stream_state": account_state} + @abstractmethod def list_objects(self, params: Mapping[str, Any]) -> Iterable: """List FB objects, these objects will be loaded in read_records later with their details. @@ -150,17 +226,21 @@ def __init__(self, start_date: Optional[datetime], end_date: Optional[datetime], def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]): """Update stream state from latest record""" - potentially_new_records_in_the_past = self._include_deleted and not current_stream_state.get("include_deleted", False) + account_id = latest_record["account_id"] + state_for_accounts = self._transform_state_from_old_format(current_stream_state, ["include_deleted"]) + account_state = self.get_account_state(account_id, state_for_accounts) + + potentially_new_records_in_the_past = self._include_deleted and not account_state.get("include_deleted", False) record_value = latest_record[self.cursor_field] - state_value = current_stream_state.get(self.cursor_field) or record_value + state_value = account_state.get(self.cursor_field) or record_value max_cursor = max(pendulum.parse(state_value), pendulum.parse(record_value)) if potentially_new_records_in_the_past: max_cursor = record_value - return { - self.cursor_field: str(max_cursor), - "include_deleted": self._include_deleted, - } + state_for_accounts.setdefault(account_id, {})[self.cursor_field] = str(max_cursor) + + state_for_accounts["include_deleted"] = self._include_deleted + return state_for_accounts def request_params(self, stream_state: Mapping[str, Any], **kwargs) -> MutableMapping[str, Any]: """Include state filter""" @@ -207,28 +287,31 @@ class FBMarketingReversedIncrementalStream(FBMarketingIncrementalStream, ABC): def __init__(self, **kwargs): super().__init__(**kwargs) - self._cursor_value = None - self._max_cursor_value = None + self._cursor_values = {} @property def state(self) -> Mapping[str, Any]: """State getter, get current state and serialize it to emmit Airbyte STATE message""" - if self._cursor_value: - return { - self.cursor_field: self._cursor_value, - "include_deleted": self._include_deleted, - } + if self._cursor_values: + result_state = {account_id: {self.cursor_field: cursor_value} for account_id, cursor_value in self._cursor_values.items()} + result_state["include_deleted"] = self._include_deleted + return result_state return {} @state.setter def state(self, value: Mapping[str, Any]): """State setter, ignore state if current settings mismatch saved state""" - if self._include_deleted and not value.get("include_deleted"): + transformed_state = self._transform_state_from_old_format(value, ["include_deleted"]) + if self._include_deleted and not transformed_state.get("include_deleted"): logger.info(f"Ignoring bookmark for {self.name} because of enabled `include_deleted` option") return - self._cursor_value = pendulum.parse(value[self.cursor_field]) + self._cursor_values = {} + for account_id in self._account_ids: + cursor_value = transformed_state.get(account_id, {}).get(self.cursor_field) + if cursor_value is not None: + self._cursor_values[account_id] = pendulum.parse(cursor_value) def _state_filter(self, stream_state: Mapping[str, Any]) -> Mapping[str, Any]: """Don't have classic cursor filtering""" @@ -250,20 +333,27 @@ def read_records( - update state only when we reach the end - stop reading when we reached the end """ + account_id = stream_slice["account_id"] + account_state = stream_slice.get("stream_state") + try: - records_iter = self.list_objects(params=self.request_params(stream_state=stream_state)) + records_iter = self.list_objects(params=self.request_params(stream_state=account_state), account_id=account_id) + account_cursor = self._cursor_values.get(account_id) + + max_cursor_value = None for record in records_iter: record_cursor_value = pendulum.parse(record[self.cursor_field]) - if self._cursor_value and record_cursor_value < self._cursor_value: + if account_cursor and record_cursor_value < account_cursor: break if not self._include_deleted and self.get_record_deleted_status(record): continue - self._max_cursor_value = max(self._max_cursor_value, record_cursor_value) if self._max_cursor_value else record_cursor_value + max_cursor_value = max(max_cursor_value, record_cursor_value) if max_cursor_value else record_cursor_value record = record.export_all_data() self.fix_date_time(record) + self.add_account_id(record, stream_slice["account_id"]) yield record - self._cursor_value = self._max_cursor_value + self._cursor_values[account_id] = max_cursor_value except FacebookRequestError as exc: raise traced_exception(exc) diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/streams.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/streams.py index 23fd4b565bdf1..c7fd0237963bd 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/streams.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/streams.py @@ -9,7 +9,6 @@ import pendulum import requests from airbyte_cdk.models import SyncMode -from cached_property import cached_property from facebook_business.adobjects.adaccount import AdAccount as FBAdAccount from facebook_business.adobjects.adimage import AdImage from facebook_business.adobjects.user import User @@ -48,10 +47,13 @@ def __init__(self, fetch_thumbnail_images: bool = False, **kwargs): super().__init__(**kwargs) self._fetch_thumbnail_images = fetch_thumbnail_images - @cached_property - def fields(self) -> List[str]: + def fields(self, **kwargs) -> List[str]: """Remove "thumbnail_data_url" field because it is computed field and it's not a field that we can request from Facebook""" - return [f for f in super().fields if f != "thumbnail_data_url"] + if self._fields: + return self._fields + + self._fields = [f for f in super().fields(**kwargs) if f != "thumbnail_data_url"] + return self._fields def read_records( self, @@ -68,8 +70,8 @@ def read_records( record["thumbnail_data_url"] = fetch_thumbnail_data_url(thumbnail_url) yield record - def list_objects(self, params: Mapping[str, Any]) -> Iterable: - return self._api.account.get_ad_creatives(params=params, fields=self.fields) + def list_objects(self, params: Mapping[str, Any], account_id: str) -> Iterable: + return self._api.get_account(account_id=account_id).get_ad_creatives(params=params, fields=self.fields()) class CustomConversions(FBMarketingStream): @@ -78,8 +80,8 @@ class CustomConversions(FBMarketingStream): entity_prefix = "customconversion" enable_deleted = False - def list_objects(self, params: Mapping[str, Any]) -> Iterable: - return self._api.account.get_custom_conversions(params=params, fields=self.fields) + def list_objects(self, params: Mapping[str, Any], account_id: str) -> Iterable: + return self._api.get_account(account_id=account_id).get_custom_conversions(params=params, fields=self.fields()) class CustomAudiences(FBMarketingStream): @@ -91,8 +93,8 @@ class CustomAudiences(FBMarketingStream): # https://github.com/airbytehq/oncall/issues/2765 fields_exceptions = ["rule"] - def list_objects(self, params: Mapping[str, Any]) -> Iterable: - return self._api.account.get_custom_audiences(params=params, fields=self.fields) + def list_objects(self, params: Mapping[str, Any], account_id: str) -> Iterable: + return self._api.get_account(account_id=account_id).get_custom_audiences(params=params, fields=self.fields()) class Ads(FBMarketingIncrementalStream): @@ -100,8 +102,8 @@ class Ads(FBMarketingIncrementalStream): entity_prefix = "ad" - def list_objects(self, params: Mapping[str, Any]) -> Iterable: - return self._api.account.get_ads(params=params, fields=self.fields) + def list_objects(self, params: Mapping[str, Any], account_id: str) -> Iterable: + return self._api.get_account(account_id=account_id).get_ads(params=params, fields=self.fields()) class AdSets(FBMarketingIncrementalStream): @@ -109,8 +111,8 @@ class AdSets(FBMarketingIncrementalStream): entity_prefix = "adset" - def list_objects(self, params: Mapping[str, Any]) -> Iterable: - return self._api.account.get_ad_sets(params=params, fields=self.fields) + def list_objects(self, params: Mapping[str, Any], account_id: str) -> Iterable: + return self._api.get_account(account_id=account_id).get_ad_sets(params=params, fields=self.fields()) class Campaigns(FBMarketingIncrementalStream): @@ -118,8 +120,8 @@ class Campaigns(FBMarketingIncrementalStream): entity_prefix = "campaign" - def list_objects(self, params: Mapping[str, Any]) -> Iterable: - return self._api.account.get_campaigns(params=params, fields=self.fields) + def list_objects(self, params: Mapping[str, Any], account_id: str) -> Iterable: + return self._api.get_account(account_id=account_id).get_campaigns(params=params, fields=self.fields()) class Activities(FBMarketingIncrementalStream): @@ -129,8 +131,16 @@ class Activities(FBMarketingIncrementalStream): cursor_field = "event_time" primary_key = None - def list_objects(self, params: Mapping[str, Any]) -> Iterable: - return self._api.account.get_activities(fields=self.fields, params=params) + def fields(self, **kwargs) -> List[str]: + """Remove account_id from fields as cannot be requested, but it is part of schema as foreign key, will be added during processing""" + if self._fields: + return self._fields + + self._fields = [f for f in super().fields(**kwargs) if f != "account_id"] + return self._fields + + def list_objects(self, params: Mapping[str, Any], account_id: str) -> Iterable: + return self._api.get_account(account_id=account_id).get_activities(fields=self.fields(), params=params) def _state_filter(self, stream_state: Mapping[str, Any]) -> Mapping[str, Any]: """Additional filters associated with state if any set""" @@ -160,9 +170,17 @@ class Videos(FBMarketingReversedIncrementalStream): entity_prefix = "video" - def list_objects(self, params: Mapping[str, Any]) -> Iterable: + def fields(self, **kwargs) -> List[str]: + """Remove account_id from fields as cannot be requested, but it is part of schema as foreign key, will be added during processing""" + if self._fields: + return self._fields + + self._fields = [f for f in super().fields() if f != "account_id"] + return self._fields + + def list_objects(self, params: Mapping[str, Any], account_id: str) -> Iterable: # Remove filtering as it is not working for this stream since 2023-01-13 - return self._api.account.get_ad_videos(params=params, fields=self.fields) + return self._api.get_account(account_id=account_id).get_ad_videos(params=params, fields=self.fields()) class AdAccount(FBMarketingStream): @@ -171,55 +189,66 @@ class AdAccount(FBMarketingStream): use_batch = False enable_deleted = False - def get_task_permissions(self) -> Set[str]: + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._fields_dict = {} + + def get_task_permissions(self, account_id: str) -> Set[str]: """https://developers.facebook.com/docs/marketing-api/reference/ad-account/assigned_users/""" res = set() me = User(fbid="me", api=self._api.api) for business_user in me.get_business_users(): - assigned_users = self._api.account.get_assigned_users(params={"business": business_user["business"].get_id()}) + assigned_users = self._api.get_account(account_id=account_id).get_assigned_users( + params={"business": business_user["business"].get_id()} + ) for assigned_user in assigned_users: if business_user.get_id() == assigned_user.get_id(): res.update(set(assigned_user["tasks"])) return res - @cached_property - def fields(self) -> List[str]: - properties = super().fields + def fields(self, account_id: str, **kwargs) -> List[str]: + if self._fields_dict.get(account_id): + return self._fields_dict.get(account_id) + + properties = super().fields(**kwargs) # https://developers.facebook.com/docs/marketing-apis/guides/javascript-ads-dialog-for-payments/ # To access "funding_source_details", the user making the API call must have a MANAGE task permission for # that specific ad account. - permissions = self.get_task_permissions() + permissions = self.get_task_permissions(account_id=account_id) if "funding_source_details" in properties and "MANAGE" not in permissions: properties.remove("funding_source_details") if "is_prepay_account" in properties and "MANAGE" not in permissions: properties.remove("is_prepay_account") + + self._fields_dict[account_id] = properties return properties - def list_objects(self, params: Mapping[str, Any]) -> Iterable: + def list_objects(self, params: Mapping[str, Any], account_id: str) -> Iterable: """noop in case of AdAccount""" - fields = self.fields + fields = self.fields(account_id=account_id) try: - return [FBAdAccount(self._api.account.get_id()).api_get(fields=fields)] + print(f"{self._api.get_account(account_id=account_id).get_id()=} {account_id=}") + return [FBAdAccount(self._api.get_account(account_id=account_id).get_id()).api_get(fields=fields)] except FacebookRequestError as e: # This is a workaround for cases when account seem to have all the required permissions # but despite of that is not allowed to get `owner` field. See (https://github.com/airbytehq/oncall/issues/3167) if e.api_error_code() == 200 and e.api_error_message() == "(#200) Requires business_management permission to manage the object": fields.remove("owner") - return [FBAdAccount(self._api.account.get_id()).api_get(fields=fields)] + return [FBAdAccount(self._api.get_account(account_id=account_id).get_id()).api_get(fields=fields)] # FB api returns a non-obvious error when accessing the `funding_source_details` field # even though user is granted all the required permissions (`MANAGE`) # https://github.com/airbytehq/oncall/issues/3031 if e.api_error_code() == 100 and e.api_error_message() == "Unsupported request - method type: get": fields.remove("funding_source_details") - return [FBAdAccount(self._api.account.get_id()).api_get(fields=fields)] + return [FBAdAccount(self._api.get_account(account_id=account_id).get_id()).api_get(fields=fields)] raise e class Images(FBMarketingReversedIncrementalStream): """See: https://developers.facebook.com/docs/marketing-api/reference/ad-image""" - def list_objects(self, params: Mapping[str, Any]) -> Iterable: - return self._api.account.get_ad_images(params=params, fields=self.fields) + def list_objects(self, params: Mapping[str, Any], account_id: str) -> Iterable: + return self._api.get_account(account_id=account_id).get_ad_images(params=params, fields=self.fields(account_id=account_id)) def get_record_deleted_status(self, record) -> bool: return record[AdImage.Field.status] == AdImage.Status.deleted diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/conftest.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/conftest.py index ad2454b02ea43..a7574ce206f94 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/conftest.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/conftest.py @@ -23,7 +23,7 @@ def account_id_fixture(): @fixture(scope="session", name="some_config") def some_config_fixture(account_id): - return {"start_date": "2021-01-23T00:00:00Z", "account_id": f"{account_id}", "access_token": "unknown_token"} + return {"start_date": "2021-01-23T00:00:00Z", "account_ids": [f"{account_id}"], "access_token": "unknown_token"} @fixture(autouse=True) @@ -49,8 +49,10 @@ def fb_account_response_fixture(account_id): @fixture(name="api") def api_fixture(some_config, requests_mock, fb_account_response): - api = API(account_id=some_config["account_id"], access_token=some_config["access_token"], page_size=100) + api = API(access_token=some_config["access_token"], page_size=100) requests_mock.register_uri("GET", FacebookSession.GRAPH + f"/{FB_API_VERSION}/me/adaccounts", [fb_account_response]) - requests_mock.register_uri("GET", FacebookSession.GRAPH + f"/{FB_API_VERSION}/act_{some_config['account_id']}/", [fb_account_response]) + requests_mock.register_uri( + "GET", FacebookSession.GRAPH + f"/{FB_API_VERSION}/act_{some_config['account_ids'][0]}/", [fb_account_response] + ) return api diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_api.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_api.py index 3bc8a37c2db8d..29b2ccbfaaffd 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_api.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_api.py @@ -136,6 +136,6 @@ def test__handle_call_rate_limit(self, mocker, fb_api, params, min_rate, usage, def test_find_account(self, api, account_id, requests_mock): requests_mock.register_uri("GET", FacebookSession.GRAPH + f"/{FB_API_VERSION}/act_{account_id}/", [{"json": {"id": "act_test"}}]) - account = api._find_account(account_id) + account = api.get_account(account_id) assert isinstance(account, AdAccount) assert account.get_id() == "act_test" diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_async_job_manager.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_async_job_manager.py index a9234fc31465a..cb0cffffeabbb 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_async_job_manager.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_async_job_manager.py @@ -29,24 +29,24 @@ def update_job_mock_fixture(mocker): class TestInsightAsyncManager: - def test_jobs_empty(self, api): + def test_jobs_empty(self, api, some_config): """Should work event without jobs""" - manager = InsightAsyncJobManager(api=api, jobs=[]) + manager = InsightAsyncJobManager(api=api, jobs=[], account_id=some_config["account_ids"][0]) jobs = list(manager.completed_jobs()) assert not jobs - def test_jobs_completed_immediately(self, api, mocker, time_mock): + def test_jobs_completed_immediately(self, api, mocker, time_mock, some_config): """Manager should emmit jobs without waiting if they completed""" jobs = [ mocker.Mock(spec=InsightAsyncJob, attempt_number=1, failed=False), mocker.Mock(spec=InsightAsyncJob, attempt_number=1, failed=False), ] - manager = InsightAsyncJobManager(api=api, jobs=jobs) + manager = InsightAsyncJobManager(api=api, jobs=jobs, account_id=some_config["account_ids"][0]) completed_jobs = list(manager.completed_jobs()) assert jobs == completed_jobs time_mock.sleep.assert_not_called() - def test_jobs_wait(self, api, mocker, time_mock, update_job_mock): + def test_jobs_wait(self, api, mocker, time_mock, update_job_mock, some_config): """Manager should return completed jobs and wait for others""" def update_job_behaviour(): @@ -61,7 +61,7 @@ def update_job_behaviour(): mocker.Mock(spec=InsightAsyncJob, attempt_number=1, failed=False, completed=False), mocker.Mock(spec=InsightAsyncJob, attempt_number=1, failed=False, completed=False), ] - manager = InsightAsyncJobManager(api=api, jobs=jobs) + manager = InsightAsyncJobManager(api=api, jobs=jobs, account_id=some_config["account_ids"][0]) job = next(manager.completed_jobs(), None) assert job == jobs[1] @@ -74,7 +74,7 @@ def update_job_behaviour(): job = next(manager.completed_jobs(), None) assert job is None - def test_job_restarted(self, api, mocker, time_mock, update_job_mock): + def test_job_restarted(self, api, mocker, time_mock, update_job_mock, some_config): """Manager should restart failed jobs""" def update_job_behaviour(): @@ -89,7 +89,7 @@ def update_job_behaviour(): mocker.Mock(spec=InsightAsyncJob, attempt_number=1, failed=False, completed=True), mocker.Mock(spec=InsightAsyncJob, attempt_number=1, failed=False, completed=False), ] - manager = InsightAsyncJobManager(api=api, jobs=jobs) + manager = InsightAsyncJobManager(api=api, jobs=jobs, account_id=some_config["account_ids"][0]) job = next(manager.completed_jobs(), None) assert job == jobs[0] @@ -101,7 +101,7 @@ def update_job_behaviour(): job = next(manager.completed_jobs(), None) assert job is None - def test_job_split(self, api, mocker, time_mock, update_job_mock): + def test_job_split(self, api, mocker, time_mock, update_job_mock, some_config): """Manager should split failed jobs when they fail second time""" def update_job_behaviour(): @@ -121,7 +121,7 @@ def update_job_behaviour(): sub_jobs[0].get_result.return_value = [1, 2] sub_jobs[1].get_result.return_value = [3, 4] jobs[1].split_job.return_value = sub_jobs - manager = InsightAsyncJobManager(api=api, jobs=jobs) + manager = InsightAsyncJobManager(api=api, jobs=jobs, account_id=some_config["account_ids"][0]) job = next(manager.completed_jobs(), None) assert job == jobs[0] @@ -134,7 +134,7 @@ def update_job_behaviour(): job = next(manager.completed_jobs(), None) assert job is None - def test_job_failed_too_many_times(self, api, mocker, time_mock, update_job_mock): + def test_job_failed_too_many_times(self, api, mocker, time_mock, update_job_mock, some_config): """Manager should fail when job failed too many times""" def update_job_behaviour(): @@ -147,12 +147,12 @@ def update_job_behaviour(): mocker.Mock(spec=InsightAsyncJob, attempt_number=1, failed=False, completed=True), mocker.Mock(spec=InsightAsyncJob, attempt_number=1, failed=False, completed=False), ] - manager = InsightAsyncJobManager(api=api, jobs=jobs) + manager = InsightAsyncJobManager(api=api, jobs=jobs, account_id=some_config["account_ids"][0]) with pytest.raises(JobException, match=f"{jobs[1]}: failed more than {InsightAsyncJobManager.MAX_NUMBER_OF_ATTEMPTS} times."): next(manager.completed_jobs(), None) - def test_nested_job_failed_too_many_times(self, api, mocker, time_mock, update_job_mock): + def test_nested_job_failed_too_many_times(self, api, mocker, time_mock, update_job_mock, some_config): """Manager should fail when a nested job within a ParentAsyncJob failed too many times""" def update_job_behaviour(): @@ -170,7 +170,7 @@ def update_job_behaviour(): mocker.Mock(spec=InsightAsyncJob, attempt_number=1, failed=False, completed=True), mocker.Mock(spec=ParentAsyncJob, _jobs=sub_jobs, attempt_number=1, failed=False, completed=False), ] - manager = InsightAsyncJobManager(api=api, jobs=jobs) + manager = InsightAsyncJobManager(api=api, jobs=jobs, account_id=some_config["account_ids"][0]) with pytest.raises(JobException): next(manager.completed_jobs(), None) diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_base_insight_streams.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_base_insight_streams.py index 6f98004bcfce5..3d6ef2aaa5e69 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_base_insight_streams.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_base_insight_streams.py @@ -48,8 +48,14 @@ def async_job_mock_fixture(mocker): class TestBaseInsightsStream: - def test_init(self, api): - stream = AdsInsights(api=api, start_date=datetime(2010, 1, 1), end_date=datetime(2011, 1, 1), insights_lookback_window=28) + def test_init(self, api, some_config): + stream = AdsInsights( + api=api, + account_ids=some_config["account_ids"], + start_date=datetime(2010, 1, 1), + end_date=datetime(2011, 1, 1), + insights_lookback_window=28, + ) assert not stream.breakdowns assert stream.action_breakdowns == ["action_type", "action_target_id", "action_destination"] @@ -57,9 +63,10 @@ def test_init(self, api): assert stream.primary_key == ["date_start", "account_id", "ad_id"] assert stream.action_report_time == "mixed" - def test_init_override(self, api): + def test_init_override(self, api, some_config): stream = AdsInsights( api=api, + account_ids=some_config["account_ids"], start_date=datetime(2010, 1, 1), end_date=datetime(2011, 1, 1), name="CustomName", @@ -73,7 +80,7 @@ def test_init_override(self, api): assert stream.name == "custom_name" assert stream.primary_key == ["date_start", "account_id", "ad_id", "test1", "test2"] - def test_read_records_all(self, mocker, api): + def test_read_records_all(self, mocker, api, some_config): """1. yield all from mock 2. if read slice 2, 3 state not changed if read slice 2, 3, 1 state changed to 3 @@ -83,6 +90,7 @@ def test_read_records_all(self, mocker, api): job.interval = pendulum.Period(pendulum.date(2010, 1, 1), pendulum.date(2010, 1, 1)) stream = AdsInsights( api=api, + account_ids=some_config["account_ids"], start_date=datetime(2010, 1, 1), end_date=datetime(2011, 1, 1), insights_lookback_window=28, @@ -91,13 +99,13 @@ def test_read_records_all(self, mocker, api): records = list( stream.read_records( sync_mode=SyncMode.incremental, - stream_slice={"insight_job": job}, + stream_slice={"insight_job": job, "account_id": some_config["account_ids"][0]}, ) ) assert len(records) == 3 - def test_read_records_random_order(self, mocker, api): + def test_read_records_random_order(self, mocker, api, some_config): """1. yield all from mock 2. if read slice 2, 3 state not changed if read slice 2, 3, 1 state changed to 3 @@ -105,62 +113,144 @@ def test_read_records_random_order(self, mocker, api): job = mocker.Mock(spec=AsyncJob) job.get_result.return_value = [mocker.Mock(), mocker.Mock(), mocker.Mock()] job.interval = pendulum.Period(pendulum.date(2010, 1, 1), pendulum.date(2010, 1, 1)) - stream = AdsInsights(api=api, start_date=datetime(2010, 1, 1), end_date=datetime(2011, 1, 1), insights_lookback_window=28) + stream = AdsInsights( + api=api, + account_ids=some_config["account_ids"], + start_date=datetime(2010, 1, 1), + end_date=datetime(2011, 1, 1), + insights_lookback_window=28, + ) records = list( stream.read_records( sync_mode=SyncMode.incremental, - stream_slice={"insight_job": job}, + stream_slice={"insight_job": job, "account_id": some_config["account_ids"][0]}, ) ) assert len(records) == 3 @pytest.mark.parametrize( - "state", + "state,result_state", [ - { - AdsInsights.cursor_field: "2010-10-03", - "slices": [ - "2010-01-01", - "2010-01-02", - ], - "time_increment": 1, - }, - { - AdsInsights.cursor_field: "2010-10-03", - }, - { - "slices": [ - "2010-01-01", - "2010-01-02", - ] - }, + # Old format + ( + { + AdsInsights.cursor_field: "2010-10-03", + "slices": [ + "2010-01-01", + "2010-01-02", + ], + "time_increment": 1, + }, + { + "unknown_account": { + AdsInsights.cursor_field: "2010-10-03", + "slices": { + "2010-01-01", + "2010-01-02", + }, + }, + "time_increment": 1, + }, + ), + ( + { + AdsInsights.cursor_field: "2010-10-03", + }, + { + "unknown_account": { + AdsInsights.cursor_field: "2010-10-03", + } + }, + ), + ( + { + "slices": [ + "2010-01-01", + "2010-01-02", + ] + }, + { + "unknown_account": { + "slices": { + "2010-01-01", + "2010-01-02", + } + } + }, + ), + # New format - nested with account_id + ( + { + "unknown_account": { + AdsInsights.cursor_field: "2010-10-03", + "slices": { + "2010-01-01", + "2010-01-02", + }, + }, + "time_increment": 1, + }, + None, + ), + ( + { + "unknown_account": { + AdsInsights.cursor_field: "2010-10-03", + } + }, + None, + ), + ( + { + "unknown_account": { + "slices": { + "2010-01-01", + "2010-01-02", + } + } + }, + None, + ), ], ) - def test_state(self, api, state): + def test_state(self, api, state, result_state, some_config): """State setter/getter should work with all combinations""" - stream = AdsInsights(api=api, start_date=datetime(2010, 1, 1), end_date=datetime(2011, 1, 1), insights_lookback_window=28) + stream = AdsInsights( + api=api, + account_ids=some_config["account_ids"], + start_date=datetime(2010, 1, 1), + end_date=datetime(2011, 1, 1), + insights_lookback_window=28, + ) - assert stream.state == {} + assert stream.state == {"time_increment": 1, "unknown_account": {"slices": set()}} stream.state = state actual_state = stream.state - actual_state["slices"] = sorted(actual_state.get("slices", [])) - state["slices"] = sorted(state.get("slices", [])) - state["time_increment"] = 1 - assert actual_state == state + result_state = state if not result_state else result_state + result_state[some_config["account_ids"][0]]["slices"] = result_state[some_config["account_ids"][0]].get("slices", set()) + result_state["time_increment"] = 1 - def test_stream_slices_no_state(self, api, async_manager_mock, start_date): + assert actual_state == result_state + + def test_stream_slices_no_state(self, api, async_manager_mock, start_date, some_config): """Stream will use start_date when there is not state""" end_date = start_date + duration(weeks=2) - stream = AdsInsights(api=api, start_date=start_date, end_date=end_date, insights_lookback_window=28) + stream = AdsInsights( + api=api, account_ids=some_config["account_ids"], start_date=start_date, end_date=end_date, insights_lookback_window=28 + ) async_manager_mock.completed_jobs.return_value = [1, 2, 3] slices = list(stream.stream_slices(stream_state=None, sync_mode=SyncMode.incremental)) - assert slices == [{"insight_job": 1}, {"insight_job": 2}, {"insight_job": 3}] + assert slices == [ + {"account_id": "unknown_account", "insight_job": 1}, + {"account_id": "unknown_account", "insight_job": 2}, + {"account_id": "unknown_account", "insight_job": 3}, + ] async_manager_mock.assert_called_once() args, kwargs = async_manager_mock.call_args generated_jobs = list(kwargs["jobs"]) @@ -168,16 +258,22 @@ def test_stream_slices_no_state(self, api, async_manager_mock, start_date): assert generated_jobs[0].interval.start == start_date.date() assert generated_jobs[1].interval.start == start_date.date() + duration(days=1) - def test_stream_slices_no_state_close_to_now(self, api, async_manager_mock, recent_start_date): + def test_stream_slices_no_state_close_to_now(self, api, async_manager_mock, recent_start_date, some_config): """Stream will use start_date when there is not state and start_date within 28d from now""" start_date = recent_start_date end_date = pendulum.now() - stream = AdsInsights(api=api, start_date=start_date, end_date=end_date, insights_lookback_window=28) + stream = AdsInsights( + api=api, account_ids=some_config["account_ids"], start_date=start_date, end_date=end_date, insights_lookback_window=28 + ) async_manager_mock.completed_jobs.return_value = [1, 2, 3] slices = list(stream.stream_slices(stream_state=None, sync_mode=SyncMode.incremental)) - assert slices == [{"insight_job": 1}, {"insight_job": 2}, {"insight_job": 3}] + assert slices == [ + {"account_id": "unknown_account", "insight_job": 1}, + {"account_id": "unknown_account", "insight_job": 2}, + {"account_id": "unknown_account", "insight_job": 3}, + ] async_manager_mock.assert_called_once() args, kwargs = async_manager_mock.call_args generated_jobs = list(kwargs["jobs"]) @@ -185,17 +281,23 @@ def test_stream_slices_no_state_close_to_now(self, api, async_manager_mock, rece assert generated_jobs[0].interval.start == start_date.date() assert generated_jobs[1].interval.start == start_date.date() + duration(days=1) - def test_stream_slices_with_state(self, api, async_manager_mock, start_date): + def test_stream_slices_with_state(self, api, async_manager_mock, start_date, some_config): """Stream will use cursor_value from state when there is state""" end_date = start_date + duration(days=10) cursor_value = start_date + duration(days=5) state = {AdsInsights.cursor_field: cursor_value.date().isoformat()} - stream = AdsInsights(api=api, start_date=start_date, end_date=end_date, insights_lookback_window=28) + stream = AdsInsights( + api=api, account_ids=some_config["account_ids"], start_date=start_date, end_date=end_date, insights_lookback_window=28 + ) async_manager_mock.completed_jobs.return_value = [1, 2, 3] slices = list(stream.stream_slices(stream_state=state, sync_mode=SyncMode.incremental)) - assert slices == [{"insight_job": 1}, {"insight_job": 2}, {"insight_job": 3}] + assert slices == [ + {"account_id": "unknown_account", "insight_job": 1}, + {"account_id": "unknown_account", "insight_job": 2}, + {"account_id": "unknown_account", "insight_job": 3}, + ] async_manager_mock.assert_called_once() args, kwargs = async_manager_mock.call_args generated_jobs = list(kwargs["jobs"]) @@ -203,18 +305,24 @@ def test_stream_slices_with_state(self, api, async_manager_mock, start_date): assert generated_jobs[0].interval.start == cursor_value.date() + duration(days=1) assert generated_jobs[1].interval.start == cursor_value.date() + duration(days=2) - def test_stream_slices_with_state_close_to_now(self, api, async_manager_mock, recent_start_date): + def test_stream_slices_with_state_close_to_now(self, api, async_manager_mock, recent_start_date, some_config): """Stream will use start_date when close to now and start_date close to now""" start_date = recent_start_date end_date = pendulum.now() cursor_value = end_date - duration(days=1) state = {AdsInsights.cursor_field: cursor_value.date().isoformat()} - stream = AdsInsights(api=api, start_date=start_date, end_date=end_date, insights_lookback_window=28) + stream = AdsInsights( + api=api, account_ids=some_config["account_ids"], start_date=start_date, end_date=end_date, insights_lookback_window=28 + ) async_manager_mock.completed_jobs.return_value = [1, 2, 3] slices = list(stream.stream_slices(stream_state=state, sync_mode=SyncMode.incremental)) - assert slices == [{"insight_job": 1}, {"insight_job": 2}, {"insight_job": 3}] + assert slices == [ + {"account_id": "unknown_account", "insight_job": 1}, + {"account_id": "unknown_account", "insight_job": 2}, + {"account_id": "unknown_account", "insight_job": 3}, + ] async_manager_mock.assert_called_once() args, kwargs = async_manager_mock.call_args generated_jobs = list(kwargs["jobs"]) @@ -222,20 +330,36 @@ def test_stream_slices_with_state_close_to_now(self, api, async_manager_mock, re assert generated_jobs[0].interval.start == start_date.date() assert generated_jobs[1].interval.start == start_date.date() + duration(days=1) - def test_stream_slices_with_state_and_slices(self, api, async_manager_mock, start_date): + @pytest.mark.parametrize("state_format", ["old_format", "new_format"]) + def test_stream_slices_with_state_and_slices(self, api, async_manager_mock, start_date, some_config, state_format): """Stream will use cursor_value from state, but will skip saved slices""" end_date = start_date + duration(days=10) cursor_value = start_date + duration(days=5) - state = { - AdsInsights.cursor_field: cursor_value.date().isoformat(), - "slices": [(cursor_value + duration(days=1)).date().isoformat(), (cursor_value + duration(days=3)).date().isoformat()], - } - stream = AdsInsights(api=api, start_date=start_date, end_date=end_date, insights_lookback_window=28) + + if state_format == "old_format": + state = { + AdsInsights.cursor_field: cursor_value.date().isoformat(), + "slices": [(cursor_value + duration(days=1)).date().isoformat(), (cursor_value + duration(days=3)).date().isoformat()], + } + else: + state = { + "unknown_account": { + AdsInsights.cursor_field: cursor_value.date().isoformat(), + "slices": [(cursor_value + duration(days=1)).date().isoformat(), (cursor_value + duration(days=3)).date().isoformat()], + } + } + stream = AdsInsights( + api=api, account_ids=some_config["account_ids"], start_date=start_date, end_date=end_date, insights_lookback_window=28 + ) async_manager_mock.completed_jobs.return_value = [1, 2, 3] slices = list(stream.stream_slices(stream_state=state, sync_mode=SyncMode.incremental)) - assert slices == [{"insight_job": 1}, {"insight_job": 2}, {"insight_job": 3}] + assert slices == [ + {"account_id": "unknown_account", "insight_job": 1}, + {"account_id": "unknown_account", "insight_job": 2}, + {"account_id": "unknown_account", "insight_job": 3}, + ] async_manager_mock.assert_called_once() args, kwargs = async_manager_mock.call_args generated_jobs = list(kwargs["jobs"]) @@ -243,18 +367,25 @@ def test_stream_slices_with_state_and_slices(self, api, async_manager_mock, star assert generated_jobs[0].interval.start == cursor_value.date() + duration(days=2) assert generated_jobs[1].interval.start == cursor_value.date() + duration(days=4) - def test_get_json_schema(self, api): - stream = AdsInsights(api=api, start_date=datetime(2010, 1, 1), end_date=datetime(2011, 1, 1), insights_lookback_window=28) + def test_get_json_schema(self, api, some_config): + stream = AdsInsights( + api=api, + account_ids=some_config["account_ids"], + start_date=datetime(2010, 1, 1), + end_date=datetime(2011, 1, 1), + insights_lookback_window=28, + ) schema = stream.get_json_schema() assert "device_platform" not in schema["properties"] assert "country" not in schema["properties"] - assert not (set(stream.fields) - set(schema["properties"].keys())), "all fields present in schema" + assert not (set(stream.fields()) - set(schema["properties"].keys())), "all fields present in schema" - def test_get_json_schema_custom(self, api): + def test_get_json_schema_custom(self, api, some_config): stream = AdsInsights( api=api, + account_ids=some_config["account_ids"], start_date=datetime(2010, 1, 1), end_date=datetime(2011, 1, 1), breakdowns=["device_platform", "country"], @@ -265,38 +396,41 @@ def test_get_json_schema_custom(self, api): assert "device_platform" in schema["properties"] assert "country" in schema["properties"] - assert not (set(stream.fields) - set(schema["properties"].keys())), "all fields present in schema" + assert not (set(stream.fields()) - set(schema["properties"].keys())), "all fields present in schema" - def test_fields(self, api): + def test_fields(self, api, some_config): stream = AdsInsights( api=api, + account_ids=some_config["account_ids"], start_date=datetime(2010, 1, 1), end_date=datetime(2011, 1, 1), insights_lookback_window=28, ) - fields = stream.fields + fields = stream.fields() assert "account_id" in fields assert "account_currency" in fields assert "actions" in fields - def test_fields_custom(self, api): + def test_fields_custom(self, api, some_config): stream = AdsInsights( api=api, + account_ids=some_config["account_ids"], start_date=datetime(2010, 1, 1), end_date=datetime(2011, 1, 1), fields=["account_id", "account_currency"], insights_lookback_window=28, ) - assert stream.fields == ["account_id", "account_currency"] + assert stream.fields() == ["account_id", "account_currency"] schema = stream.get_json_schema() assert schema["properties"].keys() == set(["account_currency", "account_id", stream.cursor_field, "date_stop", "ad_id"]) - def test_level_custom(self, api): + def test_level_custom(self, api, some_config): stream = AdsInsights( api=api, + account_ids=some_config["account_ids"], start_date=datetime(2010, 1, 1), end_date=datetime(2011, 1, 1), fields=["account_id", "account_currency"], @@ -306,9 +440,10 @@ def test_level_custom(self, api): assert stream.level == "adset" - def test_breackdowns_fields_present_in_response_data(self, api): + def test_breackdowns_fields_present_in_response_data(self, api, some_config): stream = AdsInsights( api=api, + account_ids=some_config["account_ids"], start_date=datetime(2010, 1, 1), end_date=datetime(2011, 1, 1), breakdowns=["age", "gender"], diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_base_streams.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_base_streams.py index 1f035c5c878e8..66604660645fd 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_base_streams.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_base_streams.py @@ -9,7 +9,7 @@ from facebook_business import FacebookSession from facebook_business.api import FacebookAdsApi, FacebookAdsApiBatch from source_facebook_marketing.api import MyFacebookAdsApi -from source_facebook_marketing.streams.base_streams import FBMarketingStream +from source_facebook_marketing.streams.base_streams import FBMarketingIncrementalStream, FBMarketingStream @pytest.fixture(name="mock_batch_responses") @@ -96,3 +96,54 @@ def test_date_time_value(self): } }, } == record + + +class ConcreteFBMarketingIncrementalStream(FBMarketingIncrementalStream): + cursor_field = "date" + + def list_objects(self, **kwargs): + return [] + + +@pytest.fixture +def incremental_class_instance(api): + return ConcreteFBMarketingIncrementalStream(api=api, account_ids=["123", "456", "789"], start_date=None, end_date=None) + + +class TestFBMarketingIncrementalStreamSliceAndState: + def test_stream_slices_multiple_accounts_with_state(self, incremental_class_instance): + stream_state = {"123": {"state_key": "state_value"}, "456": {"state_key": "another_state_value"}} + expected_slices = [ + {"account_id": "123", "stream_state": {"state_key": "state_value"}}, + {"account_id": "456", "stream_state": {"state_key": "another_state_value"}}, + {"account_id": "789", "stream_state": {}}, + ] + assert list(incremental_class_instance.stream_slices(stream_state)) == expected_slices + + def test_stream_slices_multiple_accounts_empty_state(self, incremental_class_instance): + expected_slices = [ + {"account_id": "123", "stream_state": {}}, + {"account_id": "456", "stream_state": {}}, + {"account_id": "789", "stream_state": {}}, + ] + assert list(incremental_class_instance.stream_slices()) == expected_slices + + def test_stream_slices_single_account_with_state(self, incremental_class_instance): + incremental_class_instance._account_ids = ["123"] + stream_state = {"state_key": "state_value"} + expected_slices = [{"account_id": "123", "stream_state": stream_state}] + assert list(incremental_class_instance.stream_slices(stream_state)) == expected_slices + + def test_stream_slices_single_account_empty_state(self, incremental_class_instance): + incremental_class_instance._account_ids = ["123"] + expected_slices = [{"account_id": "123", "stream_state": None}] + assert list(incremental_class_instance.stream_slices()) == expected_slices + + def test_get_updated_state(self, incremental_class_instance): + current_stream_state = {"123": {"date": "2021-01-15T00:00:00+00:00"}, "include_deleted": False} + latest_record = {"account_id": "123", "date": "2021-01-20T00:00:00+00:00"} + + expected_state = {"123": {"date": "2021-01-20T00:00:00+00:00", "include_deleted": False}, "include_deleted": False} + + new_state = incremental_class_instance.get_updated_state(current_stream_state, latest_record) + assert new_state == expected_state diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_client.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_client.py index 0f18516db1325..0d862aab6f319 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_client.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_client.py @@ -52,7 +52,7 @@ def fb_call_amount_data_response_fixture(): class TestBackoff: - def test_limit_reached(self, mocker, requests_mock, api, fb_call_rate_response, account_id): + def test_limit_reached(self, mocker, requests_mock, api, fb_call_rate_response, account_id, some_config): """Error once, check that we retry and not fail""" # turn Campaigns into non batch mode to test non batch logic campaign_responses = [ @@ -67,9 +67,9 @@ def test_limit_reached(self, mocker, requests_mock, api, fb_call_rate_response, requests_mock.register_uri("GET", FacebookSession.GRAPH + f"/{FB_API_VERSION}/1/", [{"status_code": 200}]) requests_mock.register_uri("GET", FacebookSession.GRAPH + f"/{FB_API_VERSION}/2/", [{"status_code": 200}]) - stream = Campaigns(api=api, start_date=pendulum.now(), end_date=pendulum.now(), include_deleted=False) + stream = Campaigns(api=api, account_ids=[account_id], start_date=pendulum.now(), end_date=pendulum.now(), include_deleted=False) try: - records = list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={})) + records = list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={}, stream_slice={"account_id": account_id})) assert records except FacebookRequestError: pytest.fail("Call rate error has not being handled") @@ -111,12 +111,12 @@ def test_batch_limit_reached(self, requests_mock, api, fb_call_rate_response, ac requests_mock.register_uri("GET", FacebookSession.GRAPH + f"/{FB_API_VERSION}/act_{account_id}/", responses) requests_mock.register_uri("POST", FacebookSession.GRAPH + f"/{FB_API_VERSION}/", batch_responses) - stream = AdCreatives(api=api, include_deleted=False) - records = list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={})) + stream = AdCreatives(api=api, account_ids=[account_id], include_deleted=False) + records = list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={}, stream_slice={"account_id": account_id})) assert records == [ - {"id": "123", "object_type": "SHARE", "status": "ACTIVE"}, - {"id": "1234", "object_type": "SHARE", "status": "ACTIVE"}, + {"account_id": "unknown_account", "id": "123", "object_type": "SHARE", "status": "ACTIVE"}, + {"account_id": "unknown_account", "id": "1234", "object_type": "SHARE", "status": "ACTIVE"}, ] @pytest.mark.parametrize( @@ -130,7 +130,7 @@ def test_batch_limit_reached(self, requests_mock, api, fb_call_rate_response, ac ) def test_common_error_retry(self, error_response, requests_mock, api, account_id): """Error once, check that we retry and not fail""" - account_data = {"id": 1, "updated_time": "2020-09-25T00:00:00Z", "name": "Some name"} + account_data = {"account_id": "unknown_account", "id": 1, "updated_time": "2020-09-25T00:00:00Z", "name": "Some name"} responses = [ error_response, { @@ -143,8 +143,8 @@ def test_common_error_retry(self, error_response, requests_mock, api, account_id requests_mock.register_uri("GET", FacebookSession.GRAPH + f"/{FB_API_VERSION}/act_{account_id}/", responses) requests_mock.register_uri("GET", FacebookSession.GRAPH + f"/{FB_API_VERSION}/{account_data['id']}/", responses) - stream = AdAccount(api=api) - accounts = list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={})) + stream = AdAccount(api=api, account_ids=[account_id]) + accounts = list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={}, stream_slice={"account_id": account_id})) assert accounts == [account_data] @@ -155,9 +155,11 @@ def test_limit_error_retry(self, fb_call_amount_data_response, requests_mock, ap "GET", FacebookSession.GRAPH + f"/{FB_API_VERSION}/act_{account_id}/campaigns", [fb_call_amount_data_response] ) - stream = Campaigns(api=api, start_date=pendulum.now(), end_date=pendulum.now(), include_deleted=False, page_size=100) + stream = Campaigns( + api=api, account_ids=[account_id], start_date=pendulum.now(), end_date=pendulum.now(), include_deleted=False, page_size=100 + ) try: - list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={})) + list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={}, stream_slice={"account_id": account_id})) except AirbyteTracedException: assert [x.qs.get("limit")[0] for x in res.request_history] == ["100", "50", "25", "12", "6"] @@ -192,9 +194,11 @@ def test_limit_error_retry_revert_page_size(self, requests_mock, api, account_id [error, success, error, success], ) - stream = Activities(api=api, start_date=pendulum.now(), end_date=pendulum.now(), include_deleted=False, page_size=100) + stream = Activities( + api=api, account_ids=[account_id], start_date=pendulum.now(), end_date=pendulum.now(), include_deleted=False, page_size=100 + ) try: - list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={})) + list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={}, stream_slice={"account_id": account_id})) except FacebookRequestError: assert [x.qs.get("limit")[0] for x in res.request_history] == ["100", "50", "100", "50"] @@ -218,8 +222,8 @@ def test_start_date_not_provided(self, requests_mock, api, account_id): [success], ) - stream = Activities(api=api, start_date=None, end_date=None, include_deleted=False, page_size=100) - list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={})) + stream = Activities(api=api, account_ids=[account_id], start_date=None, end_date=None, include_deleted=False, page_size=100) + list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={}, stream_slice={"account_id": account_id})) def test_limit_error_retry_next_page(self, fb_call_amount_data_response, requests_mock, api, account_id): """Unlike the previous test, this one tests the API call fail on the second or more page of a request.""" @@ -240,8 +244,10 @@ def test_limit_error_retry_next_page(self, fb_call_amount_data_response, request ], ) - stream = Videos(api=api, start_date=pendulum.now(), end_date=pendulum.now(), include_deleted=False, page_size=100) + stream = Videos( + api=api, account_ids=[account_id], start_date=pendulum.now(), end_date=pendulum.now(), include_deleted=False, page_size=100 + ) try: - list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={})) + list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={}, stream_slice={"account_id": account_id})) except AirbyteTracedException: assert [x.qs.get("limit")[0] for x in res.request_history] == ["100", "100", "50", "25", "12", "6"] diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_config_migrations.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_config_migrations.py new file mode 100644 index 0000000000000..092b855396c13 --- /dev/null +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_config_migrations.py @@ -0,0 +1,87 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import json +from typing import Any, Mapping + +from airbyte_cdk.models import OrchestratorType, Type +from airbyte_cdk.sources import Source +from source_facebook_marketing.config_migrations import MigrateAccountIdToArray +from source_facebook_marketing.source import SourceFacebookMarketing + +# BASE ARGS +CMD = "check" +TEST_CONFIG_PATH = "unit_tests/test_migrations/test_old_config.json" +NEW_TEST_CONFIG_PATH = "unit_tests/test_migrations/test_new_config.json" +UPGRADED_TEST_CONFIG_PATH = "unit_tests/test_migrations/test_upgraded_config.json" +SOURCE_INPUT_ARGS = [CMD, "--config", TEST_CONFIG_PATH] +SOURCE: Source = SourceFacebookMarketing() + + +# HELPERS +def load_config(config_path: str = TEST_CONFIG_PATH) -> Mapping[str, Any]: + with open(config_path, "r") as config: + return json.load(config) + + +def revert_migration(config_path: str = TEST_CONFIG_PATH) -> None: + with open(config_path, "r") as test_config: + config = json.load(test_config) + config.pop("account_ids") + with open(config_path, "w") as updated_config: + config = json.dumps(config) + updated_config.write(config) + + +def test_migrate_config(): + migration_instance = MigrateAccountIdToArray() + original_config = load_config() + # migrate the test_config + migration_instance.migrate(SOURCE_INPUT_ARGS, SOURCE) + # load the updated config + test_migrated_config = load_config() + # check migrated property + assert "account_ids" in test_migrated_config + assert isinstance(test_migrated_config["account_ids"], list) + # check the old property is in place + assert "account_id" in test_migrated_config + assert isinstance(test_migrated_config["account_id"], str) + # check the migration should be skipped, once already done + assert not migration_instance.should_migrate(test_migrated_config) + # load the old custom reports VS migrated + assert [original_config["account_id"]] == test_migrated_config["account_ids"] + # test CONTROL MESSAGE was emitted + control_msg = migration_instance.message_repository._message_queue[0] + assert control_msg.type == Type.CONTROL + assert control_msg.control.type == OrchestratorType.CONNECTOR_CONFIG + # old custom_reports are stil type(str) + assert isinstance(control_msg.control.connectorConfig.config["account_id"], str) + # new custom_reports are type(list) + assert isinstance(control_msg.control.connectorConfig.config["account_ids"], list) + # check the migrated values + assert control_msg.control.connectorConfig.config["account_ids"] == ["01234567890"] + # revert the test_config to the starting point + revert_migration() + + +def test_config_is_reverted(): + # check the test_config state, it has to be the same as before tests + test_config = load_config() + # check the config no longer has the migarted property + assert "account_ids" not in test_config + # check the old property is still there + assert "account_id" in test_config + assert isinstance(test_config["account_id"], str) + + +def test_should_not_migrate_new_config(): + new_config = load_config(NEW_TEST_CONFIG_PATH) + migration_instance = MigrateAccountIdToArray() + assert not migration_instance.should_migrate(new_config) + +def test_should_not_migrate_upgraded_config(): + new_config = load_config(UPGRADED_TEST_CONFIG_PATH) + migration_instance = MigrateAccountIdToArray() + assert not migration_instance.should_migrate(new_config) diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_errors.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_errors.py index 372ca7c5cdd2e..105306b25f555 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_errors.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_errors.py @@ -15,7 +15,7 @@ FB_API_VERSION = FacebookAdsApi.API_VERSION account_id = "unknown_account" -some_config = {"start_date": "2021-01-23T00:00:00Z", "account_id": account_id, "access_token": "unknown_token"} +some_config = {"start_date": "2021-01-23T00:00:00Z", "account_ids": [account_id], "access_token": "unknown_token"} base_url = f"{FacebookSession.GRAPH}/{FB_API_VERSION}/" act_url = f"{base_url}act_{account_id}/" @@ -26,8 +26,8 @@ } } ad_creative_data = [ - {"id": "111111", "name": "ad creative 1", "updated_time": "2023-03-21T22:33:56-0700"}, - {"id": "222222", "name": "ad creative 2", "updated_time": "2023-03-22T22:33:56-0700"}, + {"account_id": account_id, "id": "111111", "name": "ad creative 1", "updated_time": "2023-03-21T22:33:56-0700"}, + {"account_id": account_id, "id": "222222", "name": "ad creative 2", "updated_time": "2023-03-22T22:33:56-0700"}, ] ad_creative_response = { "json": { @@ -288,9 +288,11 @@ def test_retryable_error(self, some_config, requests_mock, name, retryable_error requests_mock.register_uri("GET", f"{act_url}", [retryable_error_response, ad_account_response]) requests_mock.register_uri("GET", f"{act_url}adcreatives", [retryable_error_response, ad_creative_response]) - api = API(account_id=some_config["account_id"], access_token=some_config["access_token"], page_size=100) - stream = AdCreatives(api=api, include_deleted=False) - ad_creative_records = list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={})) + api = API(access_token=some_config["access_token"], page_size=100) + stream = AdCreatives(api=api, account_ids=some_config["account_ids"], include_deleted=False) + ad_creative_records = list( + stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={}, stream_slice={"account_id": account_id}) + ) assert ad_creative_records == ad_creative_data @@ -301,12 +303,12 @@ def test_retryable_error(self, some_config, requests_mock, name, retryable_error def test_config_error_during_account_info_read(self, requests_mock, name, friendly_msg, config_error_response): """Error raised during account info read""" - api = API(account_id=some_config["account_id"], access_token=some_config["access_token"], page_size=100) - stream = AdCreatives(api=api, include_deleted=False) + api = API(access_token=some_config["access_token"], page_size=100) + stream = AdCreatives(api=api, account_ids=some_config["account_ids"], include_deleted=False) requests_mock.register_uri("GET", f"{act_url}", [config_error_response, ad_account_response]) try: - list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={})) + list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={}, stream_slice={"account_id": account_id})) assert False except Exception as error: assert isinstance(error, AirbyteTracedException) @@ -318,13 +320,13 @@ def test_config_error_during_account_info_read(self, requests_mock, name, friend def test_config_error_during_actual_nodes_read(self, requests_mock, name, friendly_msg, config_error_response): """Error raised during actual nodes read""" - api = API(account_id=some_config["account_id"], access_token=some_config["access_token"], page_size=100) - stream = AdCreatives(api=api, include_deleted=False) + api = API(access_token=some_config["access_token"], page_size=100) + stream = AdCreatives(api=api, account_ids=some_config["account_ids"], include_deleted=False) requests_mock.register_uri("GET", f"{act_url}", [ad_account_response]) requests_mock.register_uri("GET", f"{act_url}adcreatives", [config_error_response, ad_creative_response]) try: - list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={})) + list(stream.read_records(sync_mode=SyncMode.full_refresh, stream_state={}, stream_slice={"account_id": account_id})) assert False except Exception as error: assert isinstance(error, AirbyteTracedException) @@ -335,9 +337,10 @@ def test_config_error_during_actual_nodes_read(self, requests_mock, name, friend def test_config_error_insights_account_info_read(self, requests_mock, name, friendly_msg, config_error_response): """Error raised during actual nodes read""" - api = API(account_id=some_config["account_id"], access_token=some_config["access_token"], page_size=100) + api = API(access_token=some_config["access_token"], page_size=100) stream = AdsInsights( api=api, + account_ids=some_config["account_ids"], start_date=datetime(2010, 1, 1), end_date=datetime(2011, 1, 1), fields=["account_id", "account_currency"], @@ -357,9 +360,10 @@ def test_config_error_insights_account_info_read(self, requests_mock, name, frie def test_config_error_insights_during_actual_nodes_read(self, requests_mock, name, friendly_msg, config_error_response): """Error raised during actual nodes read""" - api = API(account_id=some_config["account_id"], access_token=some_config["access_token"], page_size=100) + api = API(access_token=some_config["access_token"], page_size=100) stream = AdsInsights( api=api, + account_ids=some_config["account_ids"], start_date=datetime(2010, 1, 1), end_date=datetime(2011, 1, 1), fields=["account_id", "account_currency"], @@ -411,8 +415,11 @@ def test_adaccount_list_objects_retry(self, requests_mock, failure_response): ] As a workaround for this case we can retry the API call excluding `owner` from `?fields=` GET query param. """ - api = API(account_id=some_config["account_id"], access_token=some_config["access_token"], page_size=100) - stream = AdAccount(api=api) + api = API(access_token=some_config["access_token"], page_size=100) + stream = AdAccount( + api=api, + account_ids=some_config["account_ids"], + ) business_user = {"account_id": account_id, "business": {"id": "1", "name": "TEST"}} requests_mock.register_uri("GET", f"{base_url}me/business_users", status_code=200, json=business_user) @@ -423,5 +430,5 @@ def test_adaccount_list_objects_retry(self, requests_mock, failure_response): success_response = {"status_code": 200, "json": {"account_id": account_id}} requests_mock.register_uri("GET", f"{act_url}", [failure_response, success_response]) - record_gen = stream.read_records(sync_mode=SyncMode.full_refresh, stream_slice=None, stream_state={}) + record_gen = stream.read_records(sync_mode=SyncMode.full_refresh, stream_slice={"account_id": account_id}, stream_state={}) assert list(record_gen) == [{"account_id": "unknown_account", "id": "act_unknown_account"}] diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/test_new_config.json b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/test_new_config.json new file mode 100644 index 0000000000000..489ff3fd68fb2 --- /dev/null +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/test_new_config.json @@ -0,0 +1,14 @@ +{ + "start_date": "2021-02-08T00:00:00Z", + "end_date": "2021-02-15T00:00:00Z", + "custom_insights": [ + { + "name": "custom_insight_stream", + "fields": ["account_name", "clicks", "cpc", "account_id", "ad_id"], + "breakdowns": ["gender"], + "action_breakdowns": [] + } + ], + "account_ids": ["01234567890"], + "access_token": "access_token" +} diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/test_old_config.json b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/test_old_config.json new file mode 100644 index 0000000000000..a04560eb77103 --- /dev/null +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/test_old_config.json @@ -0,0 +1,14 @@ +{ + "start_date": "2021-02-08T00:00:00Z", + "end_date": "2021-02-15T00:00:00Z", + "custom_insights": [ + { + "name": "custom_insight_stream", + "fields": ["account_name", "clicks", "cpc", "account_id", "ad_id"], + "breakdowns": ["gender"], + "action_breakdowns": [] + } + ], + "account_id": "01234567890", + "access_token": "access_token" +} diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/test_upgraded_config.json b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/test_upgraded_config.json new file mode 100644 index 0000000000000..648b4e2c390b1 --- /dev/null +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/test_upgraded_config.json @@ -0,0 +1,15 @@ +{ + "start_date": "2021-02-08T00:00:00Z", + "end_date": "2021-02-15T00:00:00Z", + "custom_insights": [ + { + "name": "custom_insight_stream", + "fields": ["account_name", "clicks", "cpc", "account_id", "ad_id"], + "breakdowns": ["gender"], + "action_breakdowns": [] + } + ], + "account_id": "01234567890", + "account_ids": ["01234567890"], + "access_token": "access_token" +} diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_source.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_source.py index 98bb41ad72f94..7b96c5ced3b9c 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_source.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_source.py @@ -4,6 +4,7 @@ from copy import deepcopy +from unittest.mock import call import pytest from airbyte_cdk.models import ( @@ -26,7 +27,7 @@ @pytest.fixture(name="config") def config_fixture(requests_mock): config = { - "account_id": "123", + "account_ids": ["123"], "access_token": "TOKEN", "start_date": "2019-10-10T00:00:00Z", "end_date": "2020-10-10T00:00:00Z", @@ -50,7 +51,7 @@ def inner(**kwargs): @pytest.fixture(name="api") def api_fixture(mocker): api_mock = mocker.patch("source_facebook_marketing.source.API") - api_mock.return_value = mocker.Mock(account=123) + api_mock.return_value = mocker.Mock(account=mocker.Mock(return_value=123)) return api_mock @@ -82,8 +83,13 @@ def test_check_connection_find_account_was_called(self, api_find_account, config """Check if _find_account was called to validate credentials""" ok, error_msg = fb_marketing.check_connection(logger_mock, config=config) - api_find_account.assert_called_once_with(config["account_id"]) - logger_mock.info.assert_called_once_with("Select account 1234") + api_find_account.assert_called_once_with(config["account_ids"][0]) + logger_mock.info.assert_has_calls( + [ + call("Attempting to retrieve information for account with ID: 123"), + call("Successfully retrieved account information for account: 1234"), + ] + ) assert ok assert not error_msg diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_streams.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_streams.py index 12f493ce37e5c..a2b03c52e67ca 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_streams.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_streams.py @@ -19,7 +19,7 @@ from source_facebook_marketing.streams.streams import fetch_thumbnail_data_url -def test_filter_all_statuses(api, mocker): +def test_filter_all_statuses(api, mocker, some_config): mocker.patch.multiple(FBMarketingStream, __abstractmethods__=set()) expected = { "filtering": [ @@ -45,7 +45,7 @@ def test_filter_all_statuses(api, mocker): } ] } - assert FBMarketingStream(api=api)._filter_all_statuses() == expected + assert FBMarketingStream(api=api, account_ids=some_config["account_ids"])._filter_all_statuses() == expected @pytest.mark.parametrize( @@ -76,15 +76,27 @@ def test_parse_call_rate_header(): [AdsInsightsRegion, ["region"], ["action_type", "action_target_id", "action_destination"]], ], ) -def test_ads_insights_breakdowns(class_name, breakdowns, action_breakdowns): - kwargs = {"api": None, "start_date": pendulum.now(), "end_date": pendulum.now(), "insights_lookback_window": 1} +def test_ads_insights_breakdowns(class_name, breakdowns, action_breakdowns, some_config): + kwargs = { + "api": None, + "account_ids": some_config["account_ids"], + "start_date": pendulum.now(), + "end_date": pendulum.now(), + "insights_lookback_window": 1, + } stream = class_name(**kwargs) assert stream.breakdowns == breakdowns assert stream.action_breakdowns == action_breakdowns -def test_custom_ads_insights_breakdowns(): - kwargs = {"api": None, "start_date": pendulum.now(), "end_date": pendulum.now(), "insights_lookback_window": 1} +def test_custom_ads_insights_breakdowns(some_config): + kwargs = { + "api": None, + "account_ids": some_config["account_ids"], + "start_date": pendulum.now(), + "end_date": pendulum.now(), + "insights_lookback_window": 1, + } stream = AdsInsights(breakdowns=["mmm"], action_breakdowns=["action_destination"], **kwargs) assert stream.breakdowns == ["mmm"] assert stream.action_breakdowns == ["action_destination"] @@ -98,9 +110,10 @@ def test_custom_ads_insights_breakdowns(): assert stream.action_breakdowns == [] -def test_custom_ads_insights_action_report_times(): +def test_custom_ads_insights_action_report_times(some_config): kwargs = { "api": None, + "account_ids": some_config["account_ids"], "start_date": pendulum.now(), "end_date": pendulum.now(), "insights_lookback_window": 1, diff --git a/docs/integrations/sources/facebook-marketing.md b/docs/integrations/sources/facebook-marketing.md index fb39df1d2d82c..75eff14ff67cd 100644 --- a/docs/integrations/sources/facebook-marketing.md +++ b/docs/integrations/sources/facebook-marketing.md @@ -70,7 +70,7 @@ You can use the [Access Token Tool](https://developers.facebook.com/tools/access #### Facebook Marketing Source Settings -1. For **Account ID**, enter the [Facebook Ad Account ID Number](https://www.facebook.com/business/help/1492627900875762) to use when pulling data from the Facebook Marketing API. To find this ID, open your Meta Ads Manager. The Ad Account ID number is in the **Account** dropdown menu or in your browser's address bar. Refer to the [Facebook docs](https://www.facebook.com/business/help/1492627900875762) for more information. +1. For **Account ID(s)**, enter one or multiple comma-separated [Facebook Ad Account ID Numbers](https://www.facebook.com/business/help/1492627900875762) to use when pulling data from the Facebook Marketing API. To find this ID, open your Meta Ads Manager. The Ad Account ID number is in the **Account** dropdown menu or in your browser's address bar. Refer to the [Facebook docs](https://www.facebook.com/business/help/1492627900875762) for more information. 2. (Optional) For **Start Date**, use the provided datepicker, or enter the date programmatically in the `YYYY-MM-DDTHH:mm:ssZ` format. If not set then all data will be replicated for usual streams and only last 2 years for insight streams. :::warning @@ -201,133 +201,134 @@ The Facebook Marketing connector uses the `lookback_window` parameter to repeate ## Changelog -| Version | Date | Pull Request | Subject | -|:--------|:-----------|:---------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 1.2.3 | 2024-01-04 | [33934](https://github.com/airbytehq/airbyte/pull/33828) | Make ready for airbyte-lib | -| 1.2.2 | 2024-01-02 | [33828](https://github.com/airbytehq/airbyte/pull/33828) | Add insights job timeout to be an option, so a user can specify their own value | -| 1.2.1 | 2023-11-22 | [32731](https://github.com/airbytehq/airbyte/pull/32731) | Removed validation that blocked personal ad accounts during `check` | -| 1.2.0 | 2023-10-31 | [31999](https://github.com/airbytehq/airbyte/pull/31999) | Extend the `AdCreatives` stream schema | -| 1.1.17 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | -| 1.1.16 | 2023-10-11 | [31284](https://github.com/airbytehq/airbyte/pull/31284) | Fix error occurring when trying to access the `funding_source_details` field of the `AdAccount` stream | -| 1.1.15 | 2023-10-06 | [31132](https://github.com/airbytehq/airbyte/pull/31132) | Fix permission error for `AdAccount` stream | -| 1.1.14 | 2023-09-26 | [30758](https://github.com/airbytehq/airbyte/pull/30758) | Exception should not be raises if a stream is not found | -| 1.1.13 | 2023-09-22 | [30706](https://github.com/airbytehq/airbyte/pull/30706) | Performance testing - include socat binary in docker image | -| 1.1.12 | 2023-09-22 | [30655](https://github.com/airbytehq/airbyte/pull/30655) | Updated doc; improved schema for custom insight streams; updated SAT or custom insight streams; removed obsolete optional max_batch_size option from spec | -| 1.1.11 | 2023-09-21 | [30650](https://github.com/airbytehq/airbyte/pull/30650) | Fix None issue since start_date is optional | -| 1.1.10 | 2023-09-15 | [30485](https://github.com/airbytehq/airbyte/pull/30485) | added 'status' and 'configured_status' fields for campaigns stream schema | -| 1.1.9 | 2023-08-31 | [29994](https://github.com/airbytehq/airbyte/pull/29994) | Removed batch processing, updated description in specs, added user-friendly error message, removed start_date from required attributes | -| 1.1.8 | 2023-09-04 | [29666](https://github.com/airbytehq/airbyte/pull/29666) | Adding custom field `boosted_object_id` to a streams schema in `campaigns` catalog `CustomAudiences` | -| 1.1.7 | 2023-08-21 | [29674](https://github.com/airbytehq/airbyte/pull/29674) | Exclude `rule` from stream `CustomAudiences` | -| 1.1.6 | 2023-08-18 | [29642](https://github.com/airbytehq/airbyte/pull/29642) | Stop batch requests if only 1 left in a batch | -| 1.1.5 | 2023-08-18 | [29610](https://github.com/airbytehq/airbyte/pull/29610) | Automatically reduce batch size | -| 1.1.4 | 2023-08-08 | [29412](https://github.com/airbytehq/airbyte/pull/29412) | Add new custom_audience stream | -| 1.1.3 | 2023-08-08 | [29208](https://github.com/airbytehq/airbyte/pull/29208) | Add account type validation during check | -| 1.1.2 | 2023-08-03 | [29042](https://github.com/airbytehq/airbyte/pull/29042) | Fix broken `advancedAuth` references for `spec` | -| 1.1.1 | 2023-07-26 | [27996](https://github.com/airbytehq/airbyte/pull/27996) | Remove reference to authSpecification | -| 1.1.0 | 2023-07-11 | [26345](https://github.com/airbytehq/airbyte/pull/26345) | Add new `action_report_time` attribute to `AdInsights` class | -| 1.0.1 | 2023-07-07 | [27979](https://github.com/airbytehq/airbyte/pull/27979) | Added the ability to restore the reduced request record limit after the successful retry, and handle the `unknown error` (code 99) with the retry strategy | -| 1.0.0 | 2023-07-05 | [27563](https://github.com/airbytehq/airbyte/pull/27563) | Migrate to FB SDK version 17 | -| 0.5.0 | 2023-06-26 | [27728](https://github.com/airbytehq/airbyte/pull/27728) | License Update: Elv2 | -| 0.4.3 | 2023-05-12 | [27483](https://github.com/airbytehq/airbyte/pull/27483) | Reduce replication start date by one more day | -| 0.4.2 | 2023-06-09 | [27201](https://github.com/airbytehq/airbyte/pull/27201) | Add `complete_oauth_server_output_specification` to spec | -| 0.4.1 | 2023-06-02 | [26941](https://github.com/airbytehq/airbyte/pull/26941) | Remove `authSpecification` from spec.json, use `advanced_auth` instead | -| 0.4.0 | 2023-05-29 | [26720](https://github.com/airbytehq/airbyte/pull/26720) | Add Prebuilt Ad Insights reports | -| 0.3.7 | 2023-05-12 | [26000](https://github.com/airbytehq/airbyte/pull/26000) | Handle config errors | -| 0.3.6 | 2023-04-27 | [22999](https://github.com/airbytehq/airbyte/pull/22999) | Specified date formatting in specification | -| 0.3.5 | 2023-04-26 | [24994](https://github.com/airbytehq/airbyte/pull/24994) | Emit stream status messages | -| 0.3.4 | 2023-04-18 | [22990](https://github.com/airbytehq/airbyte/pull/22990) | Increase pause interval | -| 0.3.3 | 2023-04-14 | [25204](https://github.com/airbytehq/airbyte/pull/25204) | Fix data retention period validation | -| 0.3.2 | 2023-04-08 | [25003](https://github.com/airbytehq/airbyte/pull/25003) | Don't fetch `thumbnail_data_url` if it's None | -| 0.3.1 | 2023-03-27 | [24600](https://github.com/airbytehq/airbyte/pull/24600) | Reduce request record limit when retrying second page or further | -| 0.3.0 | 2023-03-16 | [19141](https://github.com/airbytehq/airbyte/pull/19141) | Added Level parameter to custom Ads Insights | -| 0.2.86 | 2023-03-01 | [23625](https://github.com/airbytehq/airbyte/pull/23625) | Add user friendly fields description in spec and docs. Extend error message for invalid Account ID case. | -| 0.2.85 | 2023-02-14 | [23003](https://github.com/airbytehq/airbyte/pull/23003) | Bump facebook_business to 16.0.0 | -| 0.2.84 | 2023-01-27 | [22003](https://github.com/airbytehq/airbyte/pull/22003) | Set `AvailabilityStrategy` for streams explicitly to `None` | -| 0.2.83 | 2023-01-13 | [21149](https://github.com/airbytehq/airbyte/pull/21149) | Videos stream remove filtering | -| 0.2.82 | 2023-01-09 | [21149](https://github.com/airbytehq/airbyte/pull/21149) | Fix AdAccount schema | -| 0.2.81 | 2023-01-05 | [21057](https://github.com/airbytehq/airbyte/pull/21057) | Remove unsupported fields from request | -| 0.2.80 | 2022-12-21 | [20736](https://github.com/airbytehq/airbyte/pull/20736) | Fix update next cursor | -| 0.2.79 | 2022-12-07 | [20402](https://github.com/airbytehq/airbyte/pull/20402) | Exclude Not supported fields from request | -| 0.2.78 | 2022-12-07 | [20165](https://github.com/airbytehq/airbyte/pull/20165) | Fix fields permission error | -| 0.2.77 | 2022-12-06 | [20131](https://github.com/airbytehq/airbyte/pull/20131) | Update next cursor value at read start | -| 0.2.76 | 2022-12-03 | [20043](https://github.com/airbytehq/airbyte/pull/20043) | Allows `action_breakdowns` to be an empty list - bugfix for #20016 | -| 0.2.75 | 2022-12-03 | [20016](https://github.com/airbytehq/airbyte/pull/20016) | Allows `action_breakdowns` to be an empty list | -| 0.2.74 | 2022-11-25 | [19803](https://github.com/airbytehq/airbyte/pull/19803) | New default for `action_breakdowns`, improve "check" command speed | -| 0.2.73 | 2022-11-21 | [19645](https://github.com/airbytehq/airbyte/pull/19645) | Check "breakdowns" combinations | -| 0.2.72 | 2022-11-04 | [18971](https://github.com/airbytehq/airbyte/pull/18971) | Handle FacebookBadObjectError for empty results on async jobs | -| 0.2.71 | 2022-10-31 | [18734](https://github.com/airbytehq/airbyte/pull/18734) | Reduce request record limit on retry | -| 0.2.70 | 2022-10-26 | [18045](https://github.com/airbytehq/airbyte/pull/18045) | Upgrade FB SDK to v15.0 | -| 0.2.69 | 2022-10-17 | [18045](https://github.com/airbytehq/airbyte/pull/18045) | Remove "pixel" field from the Custom Conversions stream schema | -| 0.2.68 | 2022-10-12 | [17869](https://github.com/airbytehq/airbyte/pull/17869) | Remove "format" from optional datetime `end_date` field | -| 0.2.67 | 2022-10-04 | [17551](https://github.com/airbytehq/airbyte/pull/17551) | Add `cursor_field` for custom_insights stream schema | -| 0.2.65 | 2022-09-29 | [17371](https://github.com/airbytehq/airbyte/pull/17371) | Fix stream CustomConversions `enable_deleted=False` | -| 0.2.64 | 2022-09-22 | [17304](https://github.com/airbytehq/airbyte/pull/17304) | Migrate to per-stream state. | -| 0.2.64 | 2022-09-22 | [17027](https://github.com/airbytehq/airbyte/pull/17027) | Limit time range with 37 months when creating an insight job from lower edge object. Retry bulk request when getting error code `960` | -| 0.2.63 | 2022-09-06 | [15724](https://github.com/airbytehq/airbyte/pull/15724) | Add the Custom Conversion stream | -| 0.2.62 | 2022-09-01 | [16222](https://github.com/airbytehq/airbyte/pull/16222) | Remove `end_date` from config if empty value (re-implement #16096) | -| 0.2.61 | 2022-08-29 | [16096](https://github.com/airbytehq/airbyte/pull/16096) | Remove `end_date` from config if empty value | -| 0.2.60 | 2022-08-19 | [15788](https://github.com/airbytehq/airbyte/pull/15788) | Retry FacebookBadObjectError | -| 0.2.59 | 2022-08-04 | [15327](https://github.com/airbytehq/airbyte/pull/15327) | Shift date validation from config validation to stream method | -| 0.2.58 | 2022-07-25 | [15012](https://github.com/airbytehq/airbyte/pull/15012) | Add `DATA_RETENTION_PERIOD`validation and fix `failed_delivery_checks` field schema type issue | -| 0.2.57 | 2022-07-25 | [14831](https://github.com/airbytehq/airbyte/pull/14831) | Update Facebook SDK to version 14.0.0 | -| 0.2.56 | 2022-07-19 | [14831](https://github.com/airbytehq/airbyte/pull/14831) | Add future `start_date` and `end_date` validation | -| 0.2.55 | 2022-07-18 | [14786](https://github.com/airbytehq/airbyte/pull/14786) | Check if the authorized user has the "MANAGE" task permission when getting the `funding_source_details` field in the ad_account stream | -| 0.2.54 | 2022-06-29 | [14267](https://github.com/airbytehq/airbyte/pull/14267) | Make MAX_BATCH_SIZE available in config | -| 0.2.53 | 2022-06-16 | [13623](https://github.com/airbytehq/airbyte/pull/13623) | Add fields `bid_amount` `bid_strategy` `bid_constraints` to `ads_set` stream | -| 0.2.52 | 2022-06-14 | [13749](https://github.com/airbytehq/airbyte/pull/13749) | Fix the `not syncing any data` issue | -| 0.2.51 | 2022-05-30 | [13317](https://github.com/airbytehq/airbyte/pull/13317) | Change tax_id to string (Canadian has letter in tax_id) | -| 0.2.50 | 2022-04-27 | [12402](https://github.com/airbytehq/airbyte/pull/12402) | Add lookback window to insights streams | -| 0.2.49 | 2022-05-20 | [13047](https://github.com/airbytehq/airbyte/pull/13047) | Fix duplicating records during insights lookback period | -| 0.2.48 | 2022-05-19 | [13008](https://github.com/airbytehq/airbyte/pull/13008) | Update CDK to v0.1.58 avoid crashing on incorrect stream schemas | -| 0.2.47 | 2022-05-06 | [12685](https://github.com/airbytehq/airbyte/pull/12685) | Update CDK to v0.1.56 to emit an `AirbyeTraceMessage` on uncaught exceptions | -| 0.2.46 | 2022-04-22 | [12171](https://github.com/airbytehq/airbyte/pull/12171) | Allow configuration of page_size for requests | -| 0.2.45 | 2022-05-03 | [12390](https://github.com/airbytehq/airbyte/pull/12390) | Better retry logic for split-up async jobs | -| 0.2.44 | 2022-04-14 | [11751](https://github.com/airbytehq/airbyte/pull/11751) | Update API to a directly initialise an AdAccount with the given ID | -| 0.2.43 | 2022-04-13 | [11801](https://github.com/airbytehq/airbyte/pull/11801) | Fix `user_tos_accepted` schema to be an object | -| 0.2.42 | 2022-04-06 | [11761](https://github.com/airbytehq/airbyte/pull/11761) | Upgrade Facebook Python SDK to version 13 | -| 0.2.41 | 2022-03-28 | [11446](https://github.com/airbytehq/airbyte/pull/11446) | Increase number of attempts for individual jobs | -| 0.2.40 | 2022-02-28 | [10698](https://github.com/airbytehq/airbyte/pull/10698) | Improve sleeps time in rate limit handler | -| 0.2.39 | 2022-03-09 | [10917](https://github.com/airbytehq/airbyte/pull/10917) | Retry connections when FB API returns error code 2 (temporary oauth error) | -| 0.2.38 | 2022-03-08 | [10531](https://github.com/airbytehq/airbyte/pull/10531) | Add `time_increment` parameter to custom insights | -| 0.2.37 | 2022-02-28 | [10655](https://github.com/airbytehq/airbyte/pull/10655) | Add Activities stream | -| 0.2.36 | 2022-02-24 | [10588](https://github.com/airbytehq/airbyte/pull/10588) | Fix `execute_in_batch` for large amount of requests | -| 0.2.35 | 2022-02-18 | [10348](https://github.com/airbytehq/airbyte/pull/10348) | Add error code 104 to backoff triggers | -| 0.2.34 | 2022-02-17 | [10180](https://github.com/airbytehq/airbyte/pull/9805) | Performance and reliability fixes | -| 0.2.33 | 2021-12-28 | [10180](https://github.com/airbytehq/airbyte/pull/10180) | Add AdAccount and Images streams | -| 0.2.32 | 2022-01-07 | [10138](https://github.com/airbytehq/airbyte/pull/10138) | Add `primary_key` for all insights streams. | -| 0.2.31 | 2021-12-29 | [9138](https://github.com/airbytehq/airbyte/pull/9138) | Fix videos stream format field incorrect type | -| 0.2.30 | 2021-12-20 | [8962](https://github.com/airbytehq/airbyte/pull/8962) | Add `asset_feed_spec` field to `ad creatives` stream | -| 0.2.29 | 2021-12-17 | [8649](https://github.com/airbytehq/airbyte/pull/8649) | Retrieve ad_creatives image as data encoded | -| 0.2.28 | 2021-12-13 | [8742](https://github.com/airbytehq/airbyte/pull/8742) | Fix for schema generation related to "breakdown" fields | -| 0.2.27 | 2021-11-29 | [8257](https://github.com/airbytehq/airbyte/pull/8257) | Add fields to Campaign stream | -| 0.2.26 | 2021-11-19 | [7855](https://github.com/airbytehq/airbyte/pull/7855) | Add Video stream | -| 0.2.25 | 2021-11-12 | [7904](https://github.com/airbytehq/airbyte/pull/7904) | Implement retry logic for async jobs | -| 0.2.24 | 2021-11-09 | [7744](https://github.com/airbytehq/airbyte/pull/7744) | Fix fail when async job takes too long | -| 0.2.23 | 2021-11-08 | [7734](https://github.com/airbytehq/airbyte/pull/7734) | Resolve $ref field for discover schema | -| 0.2.22 | 2021-11-05 | [7605](https://github.com/airbytehq/airbyte/pull/7605) | Add job retry logics to AdsInsights stream | -| 0.2.21 | 2021-10-05 | [4864](https://github.com/airbytehq/airbyte/pull/4864) | Update insights streams with custom entries for fields, breakdowns and action_breakdowns | -| 0.2.20 | 2021-10-04 | [6719](https://github.com/airbytehq/airbyte/pull/6719) | Update version of facebook_business package to 12.0 | -| 0.2.19 | 2021-09-30 | [6438](https://github.com/airbytehq/airbyte/pull/6438) | Annotate Oauth2 flow initialization parameters in connector specification | -| 0.2.18 | 2021-09-28 | [6499](https://github.com/airbytehq/airbyte/pull/6499) | Fix field values converting fail | -| 0.2.17 | 2021-09-14 | [4978](https://github.com/airbytehq/airbyte/pull/4978) | Convert values' types according to schema types | -| 0.2.16 | 2021-09-14 | [6060](https://github.com/airbytehq/airbyte/pull/6060) | Fix schema for `ads_insights` stream | -| 0.2.15 | 2021-09-14 | [5958](https://github.com/airbytehq/airbyte/pull/5958) | Fix url parsing and add report that exposes conversions | -| 0.2.14 | 2021-07-19 | [4820](https://github.com/airbytehq/airbyte/pull/4820) | Improve the rate limit management | -| 0.2.12 | 2021-06-20 | [3743](https://github.com/airbytehq/airbyte/pull/3743) | Refactor connector to use CDK: - Improve error handling. - Improve async job performance \(insights\). - Add new configuration parameter `insights_days_per_job`. - Rename stream `adsets` to `ad_sets`. - Refactor schema logic for insights, allowing to configure any possible insight stream. | -| 0.2.10 | 2021-06-16 | [3973](https://github.com/airbytehq/airbyte/pull/3973) | Update version of facebook_business to 11.0 | -| 0.2.9 | 2021-06-10 | [3996](https://github.com/airbytehq/airbyte/pull/3996) | Add `AIRBYTE_ENTRYPOINT` for Kubernetes support | -| 0.2.8 | 2021-06-09 | [3973](https://github.com/airbytehq/airbyte/pull/3973) | Add 80000 as a rate-limiting error code | -| 0.2.7 | 2021-06-03 | [3646](https://github.com/airbytehq/airbyte/pull/3646) | Add missing fields to AdInsights streams | -| 0.2.6 | 2021-05-25 | [3525](https://github.com/airbytehq/airbyte/pull/3525) | Fix handling call rate limit | -| 0.2.5 | 2021-05-20 | [3396](https://github.com/airbytehq/airbyte/pull/3396) | Allow configuring insights lookback window | -| 0.2.4 | 2021-05-13 | [3395](https://github.com/airbytehq/airbyte/pull/3395) | Fix an issue that caused losing Insights data from the past 28 days while incremental sync | -| 0.2.3 | 2021-04-28 | [3116](https://github.com/airbytehq/airbyte/pull/3116) | Wait longer \(5 min\) for async jobs to start | -| 0.2.2 | 2021-04-03 | [2726](https://github.com/airbytehq/airbyte/pull/2726) | Fix base connector versioning | -| 0.2.1 | 2021-03-12 | [2391](https://github.com/airbytehq/airbyte/pull/2391) | Support FB Marketing API v10 | -| 0.2.0 | 2021-03-09 | [2238](https://github.com/airbytehq/airbyte/pull/2238) | Protocol allows future/unknown properties | -| 0.1.4 | 2021-02-24 | [1902](https://github.com/airbytehq/airbyte/pull/1902) | Add `include_deleted` option in params | -| 0.1.3 | 2021-02-15 | [1990](https://github.com/airbytehq/airbyte/pull/1990) | Support Insights stream via async queries | -| 0.1.2 | 2021-01-22 | [1699](https://github.com/airbytehq/airbyte/pull/1699) | Add incremental support | -| 0.1.1 | 2021-01-15 | [1552](https://github.com/airbytehq/airbyte/pull/1552) | Release Native Facebook Marketing Connector | +| Version | Date | Pull Request | Subject | +|:--------|:-----------|:---------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 1.3.0 | 2024-01-09 | [33538](https://github.com/airbytehq/airbyte/pull/33538) | Updated the `Ad Account ID(s)` property to support multiple IDs | +| 1.2.3 | 2024-01-04 | [33934](https://github.com/airbytehq/airbyte/pull/33828) | Make ready for airbyte-lib | +| 1.2.2 | 2024-01-02 | [33828](https://github.com/airbytehq/airbyte/pull/33828) | Add insights job timeout to be an option, so a user can specify their own value | +| 1.2.1 | 2023-11-22 | [32731](https://github.com/airbytehq/airbyte/pull/32731) | Removed validation that blocked personal ad accounts during `check` | +| 1.2.0 | 2023-10-31 | [31999](https://github.com/airbytehq/airbyte/pull/31999) | Extend the `AdCreatives` stream schema | +| 1.1.17 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | +| 1.1.16 | 2023-10-11 | [31284](https://github.com/airbytehq/airbyte/pull/31284) | Fix error occurring when trying to access the `funding_source_details` field of the `AdAccount` stream | +| 1.1.15 | 2023-10-06 | [31132](https://github.com/airbytehq/airbyte/pull/31132) | Fix permission error for `AdAccount` stream | +| 1.1.14 | 2023-09-26 | [30758](https://github.com/airbytehq/airbyte/pull/30758) | Exception should not be raises if a stream is not found | +| 1.1.13 | 2023-09-22 | [30706](https://github.com/airbytehq/airbyte/pull/30706) | Performance testing - include socat binary in docker image | +| 1.1.12 | 2023-09-22 | [30655](https://github.com/airbytehq/airbyte/pull/30655) | Updated doc; improved schema for custom insight streams; updated SAT or custom insight streams; removed obsolete optional max_batch_size option from spec | +| 1.1.11 | 2023-09-21 | [30650](https://github.com/airbytehq/airbyte/pull/30650) | Fix None issue since start_date is optional | +| 1.1.10 | 2023-09-15 | [30485](https://github.com/airbytehq/airbyte/pull/30485) | added 'status' and 'configured_status' fields for campaigns stream schema | +| 1.1.9 | 2023-08-31 | [29994](https://github.com/airbytehq/airbyte/pull/29994) | Removed batch processing, updated description in specs, added user-friendly error message, removed start_date from required attributes | +| 1.1.8 | 2023-09-04 | [29666](https://github.com/airbytehq/airbyte/pull/29666) | Adding custom field `boosted_object_id` to a streams schema in `campaigns` catalog `CustomAudiences` | +| 1.1.7 | 2023-08-21 | [29674](https://github.com/airbytehq/airbyte/pull/29674) | Exclude `rule` from stream `CustomAudiences` | +| 1.1.6 | 2023-08-18 | [29642](https://github.com/airbytehq/airbyte/pull/29642) | Stop batch requests if only 1 left in a batch | +| 1.1.5 | 2023-08-18 | [29610](https://github.com/airbytehq/airbyte/pull/29610) | Automatically reduce batch size | +| 1.1.4 | 2023-08-08 | [29412](https://github.com/airbytehq/airbyte/pull/29412) | Add new custom_audience stream | +| 1.1.3 | 2023-08-08 | [29208](https://github.com/airbytehq/airbyte/pull/29208) | Add account type validation during check | +| 1.1.2 | 2023-08-03 | [29042](https://github.com/airbytehq/airbyte/pull/29042) | Fix broken `advancedAuth` references for `spec` | +| 1.1.1 | 2023-07-26 | [27996](https://github.com/airbytehq/airbyte/pull/27996) | Remove reference to authSpecification | +| 1.1.0 | 2023-07-11 | [26345](https://github.com/airbytehq/airbyte/pull/26345) | Add new `action_report_time` attribute to `AdInsights` class | +| 1.0.1 | 2023-07-07 | [27979](https://github.com/airbytehq/airbyte/pull/27979) | Added the ability to restore the reduced request record limit after the successful retry, and handle the `unknown error` (code 99) with the retry strategy | +| 1.0.0 | 2023-07-05 | [27563](https://github.com/airbytehq/airbyte/pull/27563) | Migrate to FB SDK version 17 | +| 0.5.0 | 2023-06-26 | [27728](https://github.com/airbytehq/airbyte/pull/27728) | License Update: Elv2 | +| 0.4.3 | 2023-05-12 | [27483](https://github.com/airbytehq/airbyte/pull/27483) | Reduce replication start date by one more day | +| 0.4.2 | 2023-06-09 | [27201](https://github.com/airbytehq/airbyte/pull/27201) | Add `complete_oauth_server_output_specification` to spec | +| 0.4.1 | 2023-06-02 | [26941](https://github.com/airbytehq/airbyte/pull/26941) | Remove `authSpecification` from spec.json, use `advanced_auth` instead | +| 0.4.0 | 2023-05-29 | [26720](https://github.com/airbytehq/airbyte/pull/26720) | Add Prebuilt Ad Insights reports | +| 0.3.7 | 2023-05-12 | [26000](https://github.com/airbytehq/airbyte/pull/26000) | Handle config errors | +| 0.3.6 | 2023-04-27 | [22999](https://github.com/airbytehq/airbyte/pull/22999) | Specified date formatting in specification | +| 0.3.5 | 2023-04-26 | [24994](https://github.com/airbytehq/airbyte/pull/24994) | Emit stream status messages | +| 0.3.4 | 2023-04-18 | [22990](https://github.com/airbytehq/airbyte/pull/22990) | Increase pause interval | +| 0.3.3 | 2023-04-14 | [25204](https://github.com/airbytehq/airbyte/pull/25204) | Fix data retention period validation | +| 0.3.2 | 2023-04-08 | [25003](https://github.com/airbytehq/airbyte/pull/25003) | Don't fetch `thumbnail_data_url` if it's None | +| 0.3.1 | 2023-03-27 | [24600](https://github.com/airbytehq/airbyte/pull/24600) | Reduce request record limit when retrying second page or further | +| 0.3.0 | 2023-03-16 | [19141](https://github.com/airbytehq/airbyte/pull/19141) | Added Level parameter to custom Ads Insights | +| 0.2.86 | 2023-03-01 | [23625](https://github.com/airbytehq/airbyte/pull/23625) | Add user friendly fields description in spec and docs. Extend error message for invalid Account ID case. | +| 0.2.85 | 2023-02-14 | [23003](https://github.com/airbytehq/airbyte/pull/23003) | Bump facebook_business to 16.0.0 | +| 0.2.84 | 2023-01-27 | [22003](https://github.com/airbytehq/airbyte/pull/22003) | Set `AvailabilityStrategy` for streams explicitly to `None` | +| 0.2.83 | 2023-01-13 | [21149](https://github.com/airbytehq/airbyte/pull/21149) | Videos stream remove filtering | +| 0.2.82 | 2023-01-09 | [21149](https://github.com/airbytehq/airbyte/pull/21149) | Fix AdAccount schema | +| 0.2.81 | 2023-01-05 | [21057](https://github.com/airbytehq/airbyte/pull/21057) | Remove unsupported fields from request | +| 0.2.80 | 2022-12-21 | [20736](https://github.com/airbytehq/airbyte/pull/20736) | Fix update next cursor | +| 0.2.79 | 2022-12-07 | [20402](https://github.com/airbytehq/airbyte/pull/20402) | Exclude Not supported fields from request | +| 0.2.78 | 2022-12-07 | [20165](https://github.com/airbytehq/airbyte/pull/20165) | Fix fields permission error | +| 0.2.77 | 2022-12-06 | [20131](https://github.com/airbytehq/airbyte/pull/20131) | Update next cursor value at read start | +| 0.2.76 | 2022-12-03 | [20043](https://github.com/airbytehq/airbyte/pull/20043) | Allows `action_breakdowns` to be an empty list - bugfix for #20016 | +| 0.2.75 | 2022-12-03 | [20016](https://github.com/airbytehq/airbyte/pull/20016) | Allows `action_breakdowns` to be an empty list | +| 0.2.74 | 2022-11-25 | [19803](https://github.com/airbytehq/airbyte/pull/19803) | New default for `action_breakdowns`, improve "check" command speed | +| 0.2.73 | 2022-11-21 | [19645](https://github.com/airbytehq/airbyte/pull/19645) | Check "breakdowns" combinations | +| 0.2.72 | 2022-11-04 | [18971](https://github.com/airbytehq/airbyte/pull/18971) | Handle FacebookBadObjectError for empty results on async jobs | +| 0.2.71 | 2022-10-31 | [18734](https://github.com/airbytehq/airbyte/pull/18734) | Reduce request record limit on retry | +| 0.2.70 | 2022-10-26 | [18045](https://github.com/airbytehq/airbyte/pull/18045) | Upgrade FB SDK to v15.0 | +| 0.2.69 | 2022-10-17 | [18045](https://github.com/airbytehq/airbyte/pull/18045) | Remove "pixel" field from the Custom Conversions stream schema | +| 0.2.68 | 2022-10-12 | [17869](https://github.com/airbytehq/airbyte/pull/17869) | Remove "format" from optional datetime `end_date` field | +| 0.2.67 | 2022-10-04 | [17551](https://github.com/airbytehq/airbyte/pull/17551) | Add `cursor_field` for custom_insights stream schema | +| 0.2.65 | 2022-09-29 | [17371](https://github.com/airbytehq/airbyte/pull/17371) | Fix stream CustomConversions `enable_deleted=False` | +| 0.2.64 | 2022-09-22 | [17304](https://github.com/airbytehq/airbyte/pull/17304) | Migrate to per-stream state. | +| 0.2.64 | 2022-09-22 | [17027](https://github.com/airbytehq/airbyte/pull/17027) | Limit time range with 37 months when creating an insight job from lower edge object. Retry bulk request when getting error code `960` | +| 0.2.63 | 2022-09-06 | [15724](https://github.com/airbytehq/airbyte/pull/15724) | Add the Custom Conversion stream | +| 0.2.62 | 2022-09-01 | [16222](https://github.com/airbytehq/airbyte/pull/16222) | Remove `end_date` from config if empty value (re-implement #16096) | +| 0.2.61 | 2022-08-29 | [16096](https://github.com/airbytehq/airbyte/pull/16096) | Remove `end_date` from config if empty value | +| 0.2.60 | 2022-08-19 | [15788](https://github.com/airbytehq/airbyte/pull/15788) | Retry FacebookBadObjectError | +| 0.2.59 | 2022-08-04 | [15327](https://github.com/airbytehq/airbyte/pull/15327) | Shift date validation from config validation to stream method | +| 0.2.58 | 2022-07-25 | [15012](https://github.com/airbytehq/airbyte/pull/15012) | Add `DATA_RETENTION_PERIOD`validation and fix `failed_delivery_checks` field schema type issue | +| 0.2.57 | 2022-07-25 | [14831](https://github.com/airbytehq/airbyte/pull/14831) | Update Facebook SDK to version 14.0.0 | +| 0.2.56 | 2022-07-19 | [14831](https://github.com/airbytehq/airbyte/pull/14831) | Add future `start_date` and `end_date` validation | +| 0.2.55 | 2022-07-18 | [14786](https://github.com/airbytehq/airbyte/pull/14786) | Check if the authorized user has the "MANAGE" task permission when getting the `funding_source_details` field in the ad_account stream | +| 0.2.54 | 2022-06-29 | [14267](https://github.com/airbytehq/airbyte/pull/14267) | Make MAX_BATCH_SIZE available in config | +| 0.2.53 | 2022-06-16 | [13623](https://github.com/airbytehq/airbyte/pull/13623) | Add fields `bid_amount` `bid_strategy` `bid_constraints` to `ads_set` stream | +| 0.2.52 | 2022-06-14 | [13749](https://github.com/airbytehq/airbyte/pull/13749) | Fix the `not syncing any data` issue | +| 0.2.51 | 2022-05-30 | [13317](https://github.com/airbytehq/airbyte/pull/13317) | Change tax_id to string (Canadian has letter in tax_id) | +| 0.2.50 | 2022-04-27 | [12402](https://github.com/airbytehq/airbyte/pull/12402) | Add lookback window to insights streams | +| 0.2.49 | 2022-05-20 | [13047](https://github.com/airbytehq/airbyte/pull/13047) | Fix duplicating records during insights lookback period | +| 0.2.48 | 2022-05-19 | [13008](https://github.com/airbytehq/airbyte/pull/13008) | Update CDK to v0.1.58 avoid crashing on incorrect stream schemas | +| 0.2.47 | 2022-05-06 | [12685](https://github.com/airbytehq/airbyte/pull/12685) | Update CDK to v0.1.56 to emit an `AirbyeTraceMessage` on uncaught exceptions | +| 0.2.46 | 2022-04-22 | [12171](https://github.com/airbytehq/airbyte/pull/12171) | Allow configuration of page_size for requests | +| 0.2.45 | 2022-05-03 | [12390](https://github.com/airbytehq/airbyte/pull/12390) | Better retry logic for split-up async jobs | +| 0.2.44 | 2022-04-14 | [11751](https://github.com/airbytehq/airbyte/pull/11751) | Update API to a directly initialise an AdAccount with the given ID | +| 0.2.43 | 2022-04-13 | [11801](https://github.com/airbytehq/airbyte/pull/11801) | Fix `user_tos_accepted` schema to be an object | +| 0.2.42 | 2022-04-06 | [11761](https://github.com/airbytehq/airbyte/pull/11761) | Upgrade Facebook Python SDK to version 13 | +| 0.2.41 | 2022-03-28 | [11446](https://github.com/airbytehq/airbyte/pull/11446) | Increase number of attempts for individual jobs | +| 0.2.40 | 2022-02-28 | [10698](https://github.com/airbytehq/airbyte/pull/10698) | Improve sleeps time in rate limit handler | +| 0.2.39 | 2022-03-09 | [10917](https://github.com/airbytehq/airbyte/pull/10917) | Retry connections when FB API returns error code 2 (temporary oauth error) | +| 0.2.38 | 2022-03-08 | [10531](https://github.com/airbytehq/airbyte/pull/10531) | Add `time_increment` parameter to custom insights | +| 0.2.37 | 2022-02-28 | [10655](https://github.com/airbytehq/airbyte/pull/10655) | Add Activities stream | +| 0.2.36 | 2022-02-24 | [10588](https://github.com/airbytehq/airbyte/pull/10588) | Fix `execute_in_batch` for large amount of requests | +| 0.2.35 | 2022-02-18 | [10348](https://github.com/airbytehq/airbyte/pull/10348) | Add error code 104 to backoff triggers | +| 0.2.34 | 2022-02-17 | [10180](https://github.com/airbytehq/airbyte/pull/9805) | Performance and reliability fixes | +| 0.2.33 | 2021-12-28 | [10180](https://github.com/airbytehq/airbyte/pull/10180) | Add AdAccount and Images streams | +| 0.2.32 | 2022-01-07 | [10138](https://github.com/airbytehq/airbyte/pull/10138) | Add `primary_key` for all insights streams. | +| 0.2.31 | 2021-12-29 | [9138](https://github.com/airbytehq/airbyte/pull/9138) | Fix videos stream format field incorrect type | +| 0.2.30 | 2021-12-20 | [8962](https://github.com/airbytehq/airbyte/pull/8962) | Add `asset_feed_spec` field to `ad creatives` stream | +| 0.2.29 | 2021-12-17 | [8649](https://github.com/airbytehq/airbyte/pull/8649) | Retrieve ad_creatives image as data encoded | +| 0.2.28 | 2021-12-13 | [8742](https://github.com/airbytehq/airbyte/pull/8742) | Fix for schema generation related to "breakdown" fields | +| 0.2.27 | 2021-11-29 | [8257](https://github.com/airbytehq/airbyte/pull/8257) | Add fields to Campaign stream | +| 0.2.26 | 2021-11-19 | [7855](https://github.com/airbytehq/airbyte/pull/7855) | Add Video stream | +| 0.2.25 | 2021-11-12 | [7904](https://github.com/airbytehq/airbyte/pull/7904) | Implement retry logic for async jobs | +| 0.2.24 | 2021-11-09 | [7744](https://github.com/airbytehq/airbyte/pull/7744) | Fix fail when async job takes too long | +| 0.2.23 | 2021-11-08 | [7734](https://github.com/airbytehq/airbyte/pull/7734) | Resolve $ref field for discover schema | +| 0.2.22 | 2021-11-05 | [7605](https://github.com/airbytehq/airbyte/pull/7605) | Add job retry logics to AdsInsights stream | +| 0.2.21 | 2021-10-05 | [4864](https://github.com/airbytehq/airbyte/pull/4864) | Update insights streams with custom entries for fields, breakdowns and action_breakdowns | +| 0.2.20 | 2021-10-04 | [6719](https://github.com/airbytehq/airbyte/pull/6719) | Update version of facebook_business package to 12.0 | +| 0.2.19 | 2021-09-30 | [6438](https://github.com/airbytehq/airbyte/pull/6438) | Annotate Oauth2 flow initialization parameters in connector specification | +| 0.2.18 | 2021-09-28 | [6499](https://github.com/airbytehq/airbyte/pull/6499) | Fix field values converting fail | +| 0.2.17 | 2021-09-14 | [4978](https://github.com/airbytehq/airbyte/pull/4978) | Convert values' types according to schema types | +| 0.2.16 | 2021-09-14 | [6060](https://github.com/airbytehq/airbyte/pull/6060) | Fix schema for `ads_insights` stream | +| 0.2.15 | 2021-09-14 | [5958](https://github.com/airbytehq/airbyte/pull/5958) | Fix url parsing and add report that exposes conversions | +| 0.2.14 | 2021-07-19 | [4820](https://github.com/airbytehq/airbyte/pull/4820) | Improve the rate limit management | +| 0.2.12 | 2021-06-20 | [3743](https://github.com/airbytehq/airbyte/pull/3743) | Refactor connector to use CDK: - Improve error handling. - Improve async job performance \(insights\). - Add new configuration parameter `insights_days_per_job`. - Rename stream `adsets` to `ad_sets`. - Refactor schema logic for insights, allowing to configure any possible insight stream. | +| 0.2.10 | 2021-06-16 | [3973](https://github.com/airbytehq/airbyte/pull/3973) | Update version of facebook_business to 11.0 | +| 0.2.9 | 2021-06-10 | [3996](https://github.com/airbytehq/airbyte/pull/3996) | Add `AIRBYTE_ENTRYPOINT` for Kubernetes support | +| 0.2.8 | 2021-06-09 | [3973](https://github.com/airbytehq/airbyte/pull/3973) | Add 80000 as a rate-limiting error code | +| 0.2.7 | 2021-06-03 | [3646](https://github.com/airbytehq/airbyte/pull/3646) | Add missing fields to AdInsights streams | +| 0.2.6 | 2021-05-25 | [3525](https://github.com/airbytehq/airbyte/pull/3525) | Fix handling call rate limit | +| 0.2.5 | 2021-05-20 | [3396](https://github.com/airbytehq/airbyte/pull/3396) | Allow configuring insights lookback window | +| 0.2.4 | 2021-05-13 | [3395](https://github.com/airbytehq/airbyte/pull/3395) | Fix an issue that caused losing Insights data from the past 28 days while incremental sync | +| 0.2.3 | 2021-04-28 | [3116](https://github.com/airbytehq/airbyte/pull/3116) | Wait longer \(5 min\) for async jobs to start | +| 0.2.2 | 2021-04-03 | [2726](https://github.com/airbytehq/airbyte/pull/2726) | Fix base connector versioning | +| 0.2.1 | 2021-03-12 | [2391](https://github.com/airbytehq/airbyte/pull/2391) | Support FB Marketing API v10 | +| 0.2.0 | 2021-03-09 | [2238](https://github.com/airbytehq/airbyte/pull/2238) | Protocol allows future/unknown properties | +| 0.1.4 | 2021-02-24 | [1902](https://github.com/airbytehq/airbyte/pull/1902) | Add `include_deleted` option in params | +| 0.1.3 | 2021-02-15 | [1990](https://github.com/airbytehq/airbyte/pull/1990) | Support Insights stream via async queries | +| 0.1.2 | 2021-01-22 | [1699](https://github.com/airbytehq/airbyte/pull/1699) | Add incremental support | +| 0.1.1 | 2021-01-15 | [1552](https://github.com/airbytehq/airbyte/pull/1552) | Release Native Facebook Marketing Connector | From 804a34b2122bc9941a42c979c1c97ea766359235 Mon Sep 17 00:00:00 2001 From: Akash Kulkarni <113392464+akashkulk@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:06:38 -0800 Subject: [PATCH 051/574] [Source-mssql] : Remove options for data_to_sync & snapshot_isolation (#33700) --- .../connectors/source-mssql/metadata.yaml | 2 +- .../source/mssql/MssqlCdcHelper.java | 93 +------------------ .../source/mssql/MssqlSource.java | 33 +------ .../source-mssql/src/main/resources/spec.json | 16 ---- .../mssql/CdcMssqlSourceAcceptanceTest.java | 1 - .../mssql/CdcMssqlSourceDatatypeTest.java | 1 - .../resources/expected_spec.json | 16 ---- .../source/mssql/CdcMssqlSourceTest.java | 29 +----- .../source/mssql/CdcMssqlSslSourceTest.java | 9 +- .../source/mssql/CdcStateCompressionTest.java | 13 +-- .../source/mssql/MssqlCdcHelperTest.java | 91 +----------------- .../source/mssql/MsSQLTestDatabase.java | 56 +++++------ docs/integrations/sources/mssql.md | 5 +- 13 files changed, 45 insertions(+), 320 deletions(-) diff --git a/airbyte-integrations/connectors/source-mssql/metadata.yaml b/airbyte-integrations/connectors/source-mssql/metadata.yaml index 050bb08d179ca..9c04f3be84938 100644 --- a/airbyte-integrations/connectors/source-mssql/metadata.yaml +++ b/airbyte-integrations/connectors/source-mssql/metadata.yaml @@ -9,7 +9,7 @@ data: connectorSubtype: database connectorType: source definitionId: b5ea17b1-f170-46dc-bc31-cc744ca984c1 - dockerImageTag: 3.5.1 + dockerImageTag: 3.6.0 dockerRepository: airbyte/source-mssql documentationUrl: https://docs.airbyte.com/integrations/sources/mssql githubIssueLabel: source-mssql diff --git a/airbyte-integrations/connectors/source-mssql/src/main/java/io/airbyte/integrations/source/mssql/MssqlCdcHelper.java b/airbyte-integrations/connectors/source-mssql/src/main/java/io/airbyte/integrations/source/mssql/MssqlCdcHelper.java index 6665241df02c6..840215a697475 100644 --- a/airbyte-integrations/connectors/source-mssql/src/main/java/io/airbyte/integrations/source/mssql/MssqlCdcHelper.java +++ b/airbyte-integrations/connectors/source-mssql/src/main/java/io/airbyte/integrations/source/mssql/MssqlCdcHelper.java @@ -27,8 +27,6 @@ public class MssqlCdcHelper { private static final String REPLICATION_FIELD = "replication"; private static final String REPLICATION_TYPE_FIELD = "replication_type"; private static final String METHOD_FIELD = "method"; - private static final String CDC_SNAPSHOT_ISOLATION_FIELD = "snapshot_isolation"; - private static final String CDC_DATA_TO_SYNC_FIELD = "data_to_sync"; private static final Duration HEARTBEAT_INTERVAL = Duration.ofSeconds(10L); @@ -40,69 +38,6 @@ public enum ReplicationMethod { CDC } - /** - * The default "SNAPSHOT" mode can prevent other (non-Airbyte) transactions from updating table rows - * while we snapshot. References: - * https://docs.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql?view=sql-server-ver15 - * https://debezium.io/documentation/reference/2.2/connectors/sqlserver.html#sqlserver-property-snapshot-isolation-mode - */ - public enum SnapshotIsolation { - - SNAPSHOT("Snapshot", "snapshot"), - READ_COMMITTED("Read Committed", "read_committed"); - - private final String snapshotIsolationLevel; - private final String debeziumIsolationMode; - - SnapshotIsolation(final String snapshotIsolationLevel, final String debeziumIsolationMode) { - this.snapshotIsolationLevel = snapshotIsolationLevel; - this.debeziumIsolationMode = debeziumIsolationMode; - } - - public String getDebeziumIsolationMode() { - return debeziumIsolationMode; - } - - public static SnapshotIsolation from(final String jsonValue) { - for (final SnapshotIsolation value : values()) { - if (value.snapshotIsolationLevel.equalsIgnoreCase(jsonValue)) { - return value; - } - } - throw new IllegalArgumentException("Unexpected snapshot isolation level: " + jsonValue); - } - - } - - // https://debezium.io/documentation/reference/2.2/connectors/sqlserver.html#sqlserver-property-snapshot-mode - public enum DataToSync { - - EXISTING_AND_NEW("Existing and New", "initial"), - NEW_CHANGES_ONLY("New Changes Only", "schema_only"); - - private final String dataToSyncConfig; - private final String debeziumSnapshotMode; - - DataToSync(final String value, final String debeziumSnapshotMode) { - this.dataToSyncConfig = value; - this.debeziumSnapshotMode = debeziumSnapshotMode; - } - - public String getDebeziumSnapshotMode() { - return debeziumSnapshotMode; - } - - public static DataToSync from(final String value) { - for (final DataToSync s : values()) { - if (s.dataToSyncConfig.equalsIgnoreCase(value)) { - return s; - } - } - throw new IllegalArgumentException("Unexpected data to sync setting: " + value); - } - - } - @VisibleForTesting static boolean isCdc(final JsonNode config) { // new replication method config since version 0.4.0 @@ -122,28 +57,6 @@ static boolean isCdc(final JsonNode config) { return false; } - @VisibleForTesting - static SnapshotIsolation getSnapshotIsolationConfig(final JsonNode config) { - // new replication method config since version 0.4.0 - if (config.hasNonNull(LEGACY_REPLICATION_FIELD) && config.get(LEGACY_REPLICATION_FIELD).isObject()) { - final JsonNode replicationConfig = config.get(LEGACY_REPLICATION_FIELD); - final JsonNode snapshotIsolation = replicationConfig.get(CDC_SNAPSHOT_ISOLATION_FIELD); - return SnapshotIsolation.from(snapshotIsolation.asText()); - } - return SnapshotIsolation.SNAPSHOT; - } - - @VisibleForTesting - static DataToSync getDataToSyncConfig(final JsonNode config) { - // new replication method config since version 0.4.0 - if (config.hasNonNull(LEGACY_REPLICATION_FIELD) && config.get(LEGACY_REPLICATION_FIELD).isObject()) { - final JsonNode replicationConfig = config.get(LEGACY_REPLICATION_FIELD); - final JsonNode dataToSync = replicationConfig.get(CDC_DATA_TO_SYNC_FIELD); - return DataToSync.from(dataToSync.asText()); - } - return DataToSync.EXISTING_AND_NEW; - } - static Properties getDebeziumProperties(final JdbcDatabase database, final ConfiguredAirbyteCatalog catalog, final boolean isSnapshot) { final JsonNode config = database.getSourceConfig(); final JsonNode dbConfig = database.getDatabaseConfig(); @@ -166,10 +79,12 @@ static Properties getDebeziumProperties(final JdbcDatabase database, final Confi if (isSnapshot) { props.setProperty("snapshot.mode", "initial_only"); } else { - props.setProperty("snapshot.mode", getDataToSyncConfig(config).getDebeziumSnapshotMode()); + // If not in snapshot mode, initial will make sure that a snapshot is taken if the transaction log + // is rotated out. This will also end up read streaming changes from the transaction_log. + props.setProperty("snapshot.mode", "initial"); } - props.setProperty("snapshot.isolation.mode", getSnapshotIsolationConfig(config).getDebeziumIsolationMode()); + props.setProperty("snapshot.isolation.mode", "read_committed"); props.setProperty("schema.include.list", getSchema(catalog)); props.setProperty("database.names", config.get(JdbcUtils.DATABASE_KEY).asText()); diff --git a/airbyte-integrations/connectors/source-mssql/src/main/java/io/airbyte/integrations/source/mssql/MssqlSource.java b/airbyte-integrations/connectors/source-mssql/src/main/java/io/airbyte/integrations/source/mssql/MssqlSource.java index 6b081913984ca..229a5c9940809 100644 --- a/airbyte-integrations/connectors/source-mssql/src/main/java/io/airbyte/integrations/source/mssql/MssqlSource.java +++ b/airbyte-integrations/connectors/source-mssql/src/main/java/io/airbyte/integrations/source/mssql/MssqlSource.java @@ -41,7 +41,6 @@ import io.airbyte.commons.json.Jsons; import io.airbyte.commons.util.AutoCloseableIterator; import io.airbyte.commons.util.AutoCloseableIterators; -import io.airbyte.integrations.source.mssql.MssqlCdcHelper.SnapshotIsolation; import io.airbyte.protocol.models.CommonField; import io.airbyte.protocol.models.v0.AirbyteCatalog; import io.airbyte.protocol.models.v0.AirbyteConnectionStatus; @@ -104,7 +103,7 @@ SELECT CAST(IIF(EXISTS(SELECT TOP 1 1 FROM "%s"."%s" WHERE "%s" IS NULL), 1, 0) public static final String JDBC_DELIMITER = ";"; private List schemas; - public static Source sshWrappedSource(MssqlSource source) { + public static Source sshWrappedSource(final MssqlSource source) { return new SshWrappedSource(source, JdbcUtils.HOST_LIST_KEY, JdbcUtils.PORT_LIST_KEY); } @@ -372,7 +371,6 @@ public List> getCheckOperations(final J checkOperations.add(database -> assertCdcEnabledInDb(config, database)); checkOperations.add(database -> assertCdcSchemaQueryable(config, database)); checkOperations.add(database -> assertSqlServerAgentRunning(database)); - checkOperations.add(database -> assertSnapshotIsolationAllowed(config, database)); } return checkOperations; @@ -456,35 +454,6 @@ protected void assertSqlServerAgentRunning(final JdbcDatabase database) throws S } } - protected void assertSnapshotIsolationAllowed(final JsonNode config, final JdbcDatabase database) - throws SQLException { - if (MssqlCdcHelper.getSnapshotIsolationConfig(config) != SnapshotIsolation.SNAPSHOT) { - return; - } - - final List queryResponse = database.queryJsons(connection -> { - final String sql = "SELECT name, snapshot_isolation_state FROM sys.databases WHERE name = ?"; - final PreparedStatement ps = connection.prepareStatement(sql); - ps.setString(1, config.get(JdbcUtils.DATABASE_KEY).asText()); - LOGGER.info(String.format( - "Checking that snapshot isolation is enabled on database '%s' using the query: '%s'", - config.get(JdbcUtils.DATABASE_KEY).asText(), sql)); - return ps; - }, sourceOperations::rowToJson); - - if (queryResponse.size() < 1) { - throw new RuntimeException(String.format( - "Couldn't find '%s' in sys.databases table. Please check the spelling and that the user has relevant permissions (see docs).", - config.get(JdbcUtils.DATABASE_KEY).asText())); - } - if (queryResponse.get(0).get("snapshot_isolation_state").asInt() != 1) { - throw new RuntimeException(String.format( - "Detected that snapshot isolation is not enabled for database '%s'. MSSQL CDC relies on snapshot isolation. " - + "Please check the documentation on how to enable snapshot isolation on MS SQL Server.", - config.get(JdbcUtils.DATABASE_KEY).asText())); - } - } - @Override public List> getIncrementalIterators(final JdbcDatabase database, final ConfiguredAirbyteCatalog catalog, diff --git a/airbyte-integrations/connectors/source-mssql/src/main/resources/spec.json b/airbyte-integrations/connectors/source-mssql/src/main/resources/spec.json index f4ed5b698924e..005311b9e5a8d 100644 --- a/airbyte-integrations/connectors/source-mssql/src/main/resources/spec.json +++ b/airbyte-integrations/connectors/source-mssql/src/main/resources/spec.json @@ -132,22 +132,6 @@ "const": "CDC", "order": 0 }, - "data_to_sync": { - "title": "Data to Sync", - "type": "string", - "default": "Existing and New", - "enum": ["Existing and New", "New Changes Only"], - "description": "What data should be synced under the CDC. \"Existing and New\" will read existing data as a snapshot, and sync new changes through CDC. \"New Changes Only\" will skip the initial snapshot, and only sync new changes through CDC.", - "order": 1 - }, - "snapshot_isolation": { - "title": "Initial Snapshot Isolation Level", - "type": "string", - "default": "Snapshot", - "enum": ["Snapshot", "Read Committed"], - "description": "Existing data in the database are synced through an initial snapshot. This parameter controls the isolation level that will be used during the initial snapshotting. If you choose the \"Snapshot\" level, you must enable the snapshot isolation mode on the database.", - "order": 2 - }, "initial_waiting_seconds": { "type": "integer", "title": "Initial Waiting Time in Seconds (Advanced)", diff --git a/airbyte-integrations/connectors/source-mssql/src/test-integration/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mssql/src/test-integration/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceAcceptanceTest.java index a9e5771d398af..a87c3916ac6d3 100644 --- a/airbyte-integrations/connectors/source-mssql/src/test-integration/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceAcceptanceTest.java +++ b/airbyte-integrations/connectors/source-mssql/src/test-integration/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceAcceptanceTest.java @@ -106,7 +106,6 @@ protected void setupEnvironment(final TestDestinationEnv environment) { \t@role_name = N'%s', \t@supports_net_changes = 0"""; testdb - .withSnapshotIsolation() .withCdc() .withWaitUntilAgentRunning() // create tables diff --git a/airbyte-integrations/connectors/source-mssql/src/test-integration/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceDatatypeTest.java b/airbyte-integrations/connectors/source-mssql/src/test-integration/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceDatatypeTest.java index 44057dc38d087..adfa26005af34 100644 --- a/airbyte-integrations/connectors/source-mssql/src/test-integration/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceDatatypeTest.java +++ b/airbyte-integrations/connectors/source-mssql/src/test-integration/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceDatatypeTest.java @@ -23,7 +23,6 @@ protected JsonNode getConfig() { @Override protected Database setupDatabase() { testdb = MsSQLTestDatabase.in(BaseImage.MSSQL_2022, ContainerModifier.AGENT) - .withSnapshotIsolation() .withCdc(); return testdb.getDatabase(); } diff --git a/airbyte-integrations/connectors/source-mssql/src/test-integration/resources/expected_spec.json b/airbyte-integrations/connectors/source-mssql/src/test-integration/resources/expected_spec.json index b9d2eb4c42e4d..c2f000494ee42 100644 --- a/airbyte-integrations/connectors/source-mssql/src/test-integration/resources/expected_spec.json +++ b/airbyte-integrations/connectors/source-mssql/src/test-integration/resources/expected_spec.json @@ -132,22 +132,6 @@ "const": "CDC", "order": 0 }, - "data_to_sync": { - "title": "Data to Sync", - "type": "string", - "default": "Existing and New", - "enum": ["Existing and New", "New Changes Only"], - "description": "What data should be synced under the CDC. \"Existing and New\" will read existing data as a snapshot, and sync new changes through CDC. \"New Changes Only\" will skip the initial snapshot, and only sync new changes through CDC.", - "order": 1 - }, - "snapshot_isolation": { - "title": "Initial Snapshot Isolation Level", - "type": "string", - "default": "Snapshot", - "enum": ["Snapshot", "Read Committed"], - "description": "Existing data in the database are synced through an initial snapshot. This parameter controls the isolation level that will be used during the initial snapshotting. If you choose the \"Snapshot\" level, you must enable the snapshot isolation mode on the database.", - "order": 2 - }, "initial_waiting_seconds": { "type": "integer", "title": "Initial Waiting Time in Seconds (Advanced)", diff --git a/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceTest.java b/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceTest.java index 0c5ecc3438e50..17a9f42baa75d 100644 --- a/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceTest.java +++ b/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcMssqlSourceTest.java @@ -97,7 +97,6 @@ protected MsSQLTestDatabase createTestDatabase() { .withConnectionProperty("encrypt", "false") .withConnectionProperty("databaseName", testdb.getDatabaseName()) .initialized() - .withSnapshotIsolation() .withWaitUntilAgentRunning() .withCdc(); } @@ -169,7 +168,7 @@ protected DataSource createTestDataSource() { protected void tearDown() { try { DataSourceFactory.close(testDataSource); - } catch (Exception e) { + } catch (final Exception e) { throw new RuntimeException(e); } super.tearDown(); @@ -235,30 +234,6 @@ void testAssertSqlServerAgentRunning() { assertDoesNotThrow(() -> source().assertSqlServerAgentRunning(testDatabase())); } - @Test - void testAssertSnapshotIsolationAllowed() { - // snapshot isolation enabled by setup so assert check passes - assertDoesNotThrow(() -> source().assertSnapshotIsolationAllowed(config(), testDatabase())); - // now disable snapshot isolation and assert that check fails - testdb.withoutSnapshotIsolation(); - assertThrows(RuntimeException.class, () -> source().assertSnapshotIsolationAllowed(config(), testDatabase())); - } - - @Test - void testAssertSnapshotIsolationDisabled() { - final JsonNode replicationConfig = Jsons.jsonNode(ImmutableMap.builder() - .put("method", "CDC") - .put("data_to_sync", "New Changes Only") - // set snapshot_isolation level to "Read Committed" to disable snapshot - .put("snapshot_isolation", "Read Committed") - .build()); - final var config = config(); - Jsons.replaceNestedValue(config, List.of("replication_method"), replicationConfig); - assertDoesNotThrow(() -> source().assertSnapshotIsolationAllowed(config, testDatabase())); - testdb.withoutSnapshotIsolation(); - assertDoesNotThrow(() -> source().assertSnapshotIsolationAllowed(config, testDatabase())); - } - // Ensure the CDC check operations are included when CDC is enabled // todo: make this better by checking the returned checkOperations from source.getCheckOperations @Test @@ -280,8 +255,6 @@ void testCdcCheckOperations() throws Exception { status = source().check(config()); assertEquals(status.getStatus(), AirbyteConnectionStatus.Status.FAILED); testdb.withAgentStarted().withWaitUntilAgentRunning(); - // assertSnapshotIsolationAllowed - testdb.withoutSnapshotIsolation(); status = source().check(config()); assertEquals(status.getStatus(), AirbyteConnectionStatus.Status.FAILED); } diff --git a/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcMssqlSslSourceTest.java b/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcMssqlSslSourceTest.java index 4a1c05507b89f..fca405bd39480 100644 --- a/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcMssqlSslSourceTest.java +++ b/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcMssqlSslSourceTest.java @@ -23,8 +23,8 @@ public class CdcMssqlSslSourceTest extends CdcMssqlSourceTest { } protected MSSQLServerContainer createContainer() { - MsSQLContainerFactory containerFactory = new MsSQLContainerFactory(); - MSSQLServerContainer container = + final MsSQLContainerFactory containerFactory = new MsSQLContainerFactory(); + final MSSQLServerContainer container = containerFactory.createNewContainer(DockerImageName.parse("mcr.microsoft.com/mssql/server:2022-latest")); containerFactory.withSslCertificates(container); return container; @@ -38,7 +38,6 @@ final protected MsSQLTestDatabase createTestDatabase() { .withConnectionProperty("databaseName", testdb.getDatabaseName()) .withConnectionProperty("trustServerCertificate", "true") .initialized() - .withSnapshotIsolation() .withCdc() .withWaitUntilAgentRunning(); } @@ -60,10 +59,10 @@ protected JsonNode config() { try { containerIp = InetAddress.getByName(testdb.getContainer().getHost()) .getHostAddress(); - } catch (UnknownHostException e) { + } catch (final UnknownHostException e) { throw new RuntimeException(e); } - String certificate = testdb.getCertificate(CertificateKey.SERVER); + final String certificate = testdb.getCertificate(CertificateKey.SERVER); return testdb.configBuilder() .withEncrytedVerifyServerCertificate(certificate, testdb.getContainer().getHost()) .with(JdbcUtils.HOST_KEY, containerIp) diff --git a/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcStateCompressionTest.java b/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcStateCompressionTest.java index 95ab8bc1f15a9..f1856f79e0647 100644 --- a/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcStateCompressionTest.java +++ b/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/CdcStateCompressionTest.java @@ -66,7 +66,6 @@ public void setup() { .withConnectionProperty("encrypt", "false") .withConnectionProperty("databaseName", testdb.getDatabaseName()) .initialized() - .withSnapshotIsolation() .withCdc() .withWaitUntilAgentRunning(); @@ -129,7 +128,7 @@ public void setup() { } private AirbyteCatalog getCatalog() { - var streams = new ArrayList(); + final var streams = new ArrayList(); for (int i = 0; i < TEST_TABLES; i++) { streams.add(CatalogHelpers.createAirbyteStream( "test_table_%d".formatted(i), @@ -164,9 +163,7 @@ private JsonNode config() { .with("is_test", true) .with("replication_method", Map.of( "method", "CDC", - "data_to_sync", "Existing and New", - "initial_waiting_seconds", 60, - "snapshot_isolation", "Snapshot")) + "initial_waiting_seconds", 60)) .build(); } @@ -194,7 +191,7 @@ public void testCompressedSchemaHistory() throws Exception { assertTrue(lastSharedStateFromFirstBatch.get(IS_COMPRESSED).asBoolean()); final var recordsFromFirstBatch = extractRecordMessages(dataFromFirstBatch); assertEquals(TEST_TABLES, recordsFromFirstBatch.size()); - for (var record : recordsFromFirstBatch) { + for (final var record : recordsFromFirstBatch) { assertEquals("1", record.getData().get("id").toString()); } @@ -219,7 +216,7 @@ public void testCompressedSchemaHistory() throws Exception { assertTrue(lastSharedStateFromSecondBatch.get(IS_COMPRESSED).asBoolean()); final var recordsFromSecondBatch = extractRecordMessages(dataFromSecondBatch); assertEquals(TEST_TABLES, recordsFromSecondBatch.size()); - for (var record : recordsFromSecondBatch) { + for (final var record : recordsFromSecondBatch) { assertEquals("2", record.getData().get("id").toString()); } } @@ -241,7 +238,7 @@ private Map> extractRecordMessagesStreamWise(f .collect(Collectors.groupingBy(AirbyteRecordMessage::getStream)); final Map> recordsPerStreamWithNoDuplicates = new HashMap<>(); - for (var entry : recordsPerStream.entrySet()) { + for (final var entry : recordsPerStream.entrySet()) { final var set = new HashSet<>(entry.getValue()); recordsPerStreamWithNoDuplicates.put(entry.getKey(), set); assertEquals(entry.getValue().size(), set.size(), "duplicate records in sync for " + entry.getKey()); diff --git a/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/MssqlCdcHelperTest.java b/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/MssqlCdcHelperTest.java index a2f29d5064a72..d1ec53fe19157 100644 --- a/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/MssqlCdcHelperTest.java +++ b/airbyte-integrations/connectors/source-mssql/src/test/java/io/airbyte/integrations/source/mssql/MssqlCdcHelperTest.java @@ -4,14 +4,11 @@ package io.airbyte.integrations.source.mssql; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.databind.JsonNode; import io.airbyte.commons.json.Jsons; -import io.airbyte.integrations.source.mssql.MssqlCdcHelper.DataToSync; -import io.airbyte.integrations.source.mssql.MssqlCdcHelper.SnapshotIsolation; import java.util.Map; import org.junit.jupiter.api.Test; @@ -33,9 +30,7 @@ public void testIsCdc() { final JsonNode newCdc = Jsons.jsonNode(Map.of("replication_method", Jsons.jsonNode(Map.of( - "method", "CDC", - "data_to_sync", "Existing and New", - "snapshot_isolation", "Snapshot")))); + "method", "CDC")))); assertTrue(MssqlCdcHelper.isCdc(newCdc)); // migration from legacy to new config @@ -46,90 +41,10 @@ public void testIsCdc() { final JsonNode mixCdc = Jsons.jsonNode(Map.of( "replication", Jsons.jsonNode(Map.of( - "replication_type", "Standard", - "data_to_sync", "Existing and New", - "snapshot_isolation", "Snapshot")), + "replication_type", "Standard")), "replication_method", Jsons.jsonNode(Map.of( - "method", "CDC", - "data_to_sync", "Existing and New", - "snapshot_isolation", "Snapshot")))); + "method", "CDC")))); assertTrue(MssqlCdcHelper.isCdc(mixCdc)); } - @Test - public void testGetSnapshotIsolation() { - // legacy replication method config before version 0.4.0 - assertEquals(SnapshotIsolation.SNAPSHOT, MssqlCdcHelper.getSnapshotIsolationConfig(LEGACY_CDC_CONFIG)); - - // new replication method config since version 0.4.0 - final JsonNode newCdcNonSnapshot = Jsons.jsonNode(Map.of("replication_method", - Jsons.jsonNode(Map.of( - "method", "CDC", - "data_to_sync", "Existing and New", - "snapshot_isolation", "Read Committed")))); - assertEquals(SnapshotIsolation.READ_COMMITTED, MssqlCdcHelper.getSnapshotIsolationConfig(newCdcNonSnapshot)); - - final JsonNode newCdcSnapshot = Jsons.jsonNode(Map.of("replication_method", - Jsons.jsonNode(Map.of( - "method", "CDC", - "data_to_sync", "Existing and New", - "snapshot_isolation", "Snapshot")))); - assertEquals(SnapshotIsolation.SNAPSHOT, MssqlCdcHelper.getSnapshotIsolationConfig(newCdcSnapshot)); - - // migration from legacy to new config - final JsonNode mixCdcNonSnapshot = Jsons.jsonNode(Map.of( - "replication", "Standard", - "replication_method", Jsons.jsonNode(Map.of( - "method", "CDC", - "data_to_sync", "Existing and New", - "snapshot_isolation", "Read Committed")))); - assertEquals(SnapshotIsolation.READ_COMMITTED, MssqlCdcHelper.getSnapshotIsolationConfig(mixCdcNonSnapshot)); - - final JsonNode mixCdcSnapshot = Jsons.jsonNode(Map.of( - "replication", "Standard", - "replication_method", Jsons.jsonNode(Map.of( - "method", "CDC", - "data_to_sync", "Existing and New", - "snapshot_isolation", "Snapshot")))); - assertEquals(SnapshotIsolation.SNAPSHOT, MssqlCdcHelper.getSnapshotIsolationConfig(mixCdcSnapshot)); - } - - @Test - public void testGetDataToSyncConfig() { - // legacy replication method config before version 0.4.0 - assertEquals(DataToSync.EXISTING_AND_NEW, MssqlCdcHelper.getDataToSyncConfig(LEGACY_CDC_CONFIG)); - - // new replication method config since version 0.4.0 - final JsonNode newCdcExistingAndNew = Jsons.jsonNode(Map.of("replication_method", - Jsons.jsonNode(Map.of( - "method", "CDC", - "data_to_sync", "Existing and New", - "snapshot_isolation", "Read Committed")))); - assertEquals(DataToSync.EXISTING_AND_NEW, MssqlCdcHelper.getDataToSyncConfig(newCdcExistingAndNew)); - - final JsonNode newCdcNewOnly = Jsons.jsonNode(Map.of("replication_method", - Jsons.jsonNode(Map.of( - "method", "CDC", - "data_to_sync", "New Changes Only", - "snapshot_isolation", "Snapshot")))); - assertEquals(DataToSync.NEW_CHANGES_ONLY, MssqlCdcHelper.getDataToSyncConfig(newCdcNewOnly)); - - final JsonNode mixCdcExistingAndNew = Jsons.jsonNode(Map.of( - "replication", "Standard", - "replication_method", Jsons.jsonNode(Map.of( - "method", "CDC", - "data_to_sync", "Existing and New", - "snapshot_isolation", "Read Committed")))); - assertEquals(DataToSync.EXISTING_AND_NEW, MssqlCdcHelper.getDataToSyncConfig(mixCdcExistingAndNew)); - - final JsonNode mixCdcNewOnly = Jsons.jsonNode(Map.of( - "replication", "Standard", - "replication_method", - Jsons.jsonNode(Map.of( - "method", "CDC", - "data_to_sync", "New Changes Only", - "snapshot_isolation", "Snapshot")))); - assertEquals(DataToSync.NEW_CHANGES_ONLY, MssqlCdcHelper.getDataToSyncConfig(mixCdcNewOnly)); - } - } diff --git a/airbyte-integrations/connectors/source-mssql/src/testFixtures/java/io/airbyte/integrations/source/mssql/MsSQLTestDatabase.java b/airbyte-integrations/connectors/source-mssql/src/testFixtures/java/io/airbyte/integrations/source/mssql/MsSQLTestDatabase.java index aae8f6333788a..8ee98f176cd24 100644 --- a/airbyte-integrations/connectors/source-mssql/src/testFixtures/java/io/airbyte/integrations/source/mssql/MsSQLTestDatabase.java +++ b/airbyte-integrations/connectors/source-mssql/src/testFixtures/java/io/airbyte/integrations/source/mssql/MsSQLTestDatabase.java @@ -35,7 +35,7 @@ public static enum BaseImage { private final String reference; - private BaseImage(String reference) { + private BaseImage(final String reference) { this.reference = reference; } @@ -49,14 +49,14 @@ public static enum ContainerModifier { private final String methodName; - private ContainerModifier(String methodName) { + private ContainerModifier(final String methodName) { this.methodName = methodName; } } - static public MsSQLTestDatabase in(BaseImage imageName, ContainerModifier... methods) { - String[] methodNames = Stream.of(methods).map(im -> im.methodName).toList().toArray(new String[0]); + static public MsSQLTestDatabase in(final BaseImage imageName, final ContainerModifier... methods) { + final String[] methodNames = Stream.of(methods).map(im -> im.methodName).toList().toArray(new String[0]); final var container = new MsSQLContainerFactory().shared(imageName.reference, methodNames); final var testdb = new MsSQLTestDatabase(container); return testdb @@ -65,18 +65,10 @@ static public MsSQLTestDatabase in(BaseImage imageName, ContainerModifier... met .initialized(); } - public MsSQLTestDatabase(MSSQLServerContainer container) { + public MsSQLTestDatabase(final MSSQLServerContainer container) { super(container); } - public MsSQLTestDatabase withSnapshotIsolation() { - return with("ALTER DATABASE %s SET ALLOW_SNAPSHOT_ISOLATION ON;", getDatabaseName()); - } - - public MsSQLTestDatabase withoutSnapshotIsolation() { - return with("ALTER DATABASE %s SET ALLOW_SNAPSHOT_ISOLATION OFF;", getDatabaseName()); - } - public MsSQLTestDatabase withCdc() { return with("EXEC sys.sp_cdc_enable_db;"); } @@ -119,12 +111,12 @@ private void waitForAgentState(final boolean running) { return; } LOGGER.debug("Retrying, SQLServerAgent state {} does not match expected '{}'.", r, expectedValue); - } catch (SQLException e) { + } catch (final SQLException e) { LOGGER.debug("Retrying agent state query after catching exception {}.", e.getMessage()); } try { Thread.sleep(1_000); // Wait one second between retries. - } catch (InterruptedException e) { + } catch (final InterruptedException e) { throw new RuntimeException(e); } } @@ -141,12 +133,12 @@ public MsSQLTestDatabase withWaitUntilMaxLsnAvailable() { return self(); } LOGGER.debug("Retrying, max LSN still not available for database {}.", getDatabaseName()); - } catch (SQLException e) { + } catch (final SQLException e) { LOGGER.warn("Retrying max LSN query after catching exception {}", e.getMessage()); } try { Thread.sleep(1_000); // Wait one second between retries. - } catch (InterruptedException e) { + } catch (final InterruptedException e) { throw new RuntimeException(e); } } @@ -192,7 +184,7 @@ public void dropDatabaseAndUser() { String.format("DROP DATABASE %s", getDatabaseName())))); } - public Stream mssqlCmd(Stream sql) { + public Stream mssqlCmd(final Stream sql) { return Stream.of("/opt/mssql-tools/bin/sqlcmd", "-U", getContainer().getUsername(), "-P", getContainer().getPassword(), @@ -221,7 +213,7 @@ public static enum CertificateKey { public final boolean isValid; - CertificateKey(boolean isValid) { + CertificateKey(final boolean isValid) { this.isValid = isValid; } @@ -229,18 +221,18 @@ public static enum CertificateKey { private Map cachedCerts; - public synchronized String getCertificate(CertificateKey certificateKey) { + public synchronized String getCertificate(final CertificateKey certificateKey) { if (cachedCerts == null) { - Map cachedCerts = new HashMap<>(); + final Map cachedCerts = new HashMap<>(); try { - for (CertificateKey key : CertificateKey.values()) { - String command = "cat /tmp/certs/" + key.name().toLowerCase() + ".crt"; - String certificate = getContainer().execInContainer("bash", "-c", command).getStdout().trim(); + for (final CertificateKey key : CertificateKey.values()) { + final String command = "cat /tmp/certs/" + key.name().toLowerCase() + ".crt"; + final String certificate = getContainer().execInContainer("bash", "-c", command).getStdout().trim(); cachedCerts.put(key, certificate); } - } catch (IOException e) { + } catch (final IOException e) { throw new UncheckedIOException(e); - } catch (InterruptedException e) { + } catch (final InterruptedException e) { throw new RuntimeException(e); } this.cachedCerts = cachedCerts; @@ -255,7 +247,7 @@ public MsSQLConfigBuilder configBuilder() { static public class MsSQLConfigBuilder extends ConfigBuilder { - protected MsSQLConfigBuilder(MsSQLTestDatabase testDatabase) { + protected MsSQLConfigBuilder(final MsSQLTestDatabase testDatabase) { super(testDatabase); with(JdbcUtils.JDBC_URL_PARAMS_KEY, "loginTimeout=2"); @@ -266,12 +258,10 @@ public MsSQLConfigBuilder withCdcReplication() { return with("is_test", true) .with("replication_method", Map.of( "method", "CDC", - "data_to_sync", "Existing and New", - "initial_waiting_seconds", DEFAULT_CDC_REPLICATION_INITIAL_WAIT.getSeconds(), - "snapshot_isolation", "Snapshot")); + "initial_waiting_seconds", DEFAULT_CDC_REPLICATION_INITIAL_WAIT.getSeconds())); } - public MsSQLConfigBuilder withSchemas(String... schemas) { + public MsSQLConfigBuilder withSchemas(final String... schemas) { return with(JdbcUtils.SCHEMAS_KEY, List.of(schemas)); } @@ -281,7 +271,7 @@ public MsSQLConfigBuilder withoutSsl() { } @Deprecated - public MsSQLConfigBuilder withSsl(Map sslMode) { + public MsSQLConfigBuilder withSsl(final Map sslMode) { return with("ssl_method", sslMode); } @@ -289,7 +279,7 @@ public MsSQLConfigBuilder withEncrytedTrustServerCertificate() { return withSsl(Map.of("ssl_method", "encrypted_trust_server_certificate")); } - public MsSQLConfigBuilder withEncrytedVerifyServerCertificate(String certificate, String hostnameInCertificate) { + public MsSQLConfigBuilder withEncrytedVerifyServerCertificate(final String certificate, final String hostnameInCertificate) { if (hostnameInCertificate != null) { return withSsl(Map.of("ssl_method", "encrypted_verify_certificate", "certificate", certificate, diff --git a/docs/integrations/sources/mssql.md b/docs/integrations/sources/mssql.md index 21ca86e71094e..6370453b034d6 100644 --- a/docs/integrations/sources/mssql.md +++ b/docs/integrations/sources/mssql.md @@ -342,8 +342,9 @@ WHERE actor_definition_id ='b5ea17b1-f170-46dc-bc31-cc744ca984c1' AND (configura | Version | Date | Pull Request | Subject | |:--------|:-----------|:------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------| -| 3.5.1 | 2024-01-05 | [33510](https://github.com/airbytehq/airbyte/pull/33510) | Test-only changes. | -| 3.5.0 | 2023-12-19 | [33071](https://github.com/airbytehq/airbyte/pull/33071) | Fix SSL configuration parameters | +| 3.6.0 | 2024-01-10 | [33700](https://github.com/airbytehq/airbyte/pull/33700) | Remove CDC config options for data_to_sync and snapshot isolation. | +| 3.5.1 | 2024-01-05 | [33510](https://github.com/airbytehq/airbyte/pull/33510) | Test-only changes. | +| 3.5.0 | 2023-12-19 | [33071](https://github.com/airbytehq/airbyte/pull/33071) | Fix SSL configuration parameters | | 3.4.1 | 2024-01-02 | [33755](https://github.com/airbytehq/airbyte/pull/33755) | Encode binary to base64 format | | 3.4.0 | 2023-12-19 | [33481](https://github.com/airbytehq/airbyte/pull/33481) | Remove LEGACY state flag | | 3.3.2 | 2023-12-14 | [33505](https://github.com/airbytehq/airbyte/pull/33225) | Using the released CDK. | From a0ece1231e45556cb9e89d2b89f66aef8db8289b Mon Sep 17 00:00:00 2001 From: Gireesh Sreepathi Date: Wed, 10 Jan 2024 15:42:32 -0800 Subject: [PATCH 052/574] destination-postgres: Add tunnel heartbeats and keepalive (#33875) Signed-off-by: Gireesh Sreepathi --- airbyte-cdk/java/airbyte-cdk/core/build.gradle | 2 +- .../airbyte-cdk/core/src/main/resources/version.properties | 2 +- .../destination/DestinationAcceptanceTest.java | 7 +++++++ .../destination-postgres-strict-encrypt/build.gradle | 4 ++-- .../destination-postgres-strict-encrypt/metadata.yaml | 2 +- .../connectors/destination-postgres/build.gradle | 4 ++-- .../connectors/destination-postgres/metadata.yaml | 2 +- .../postgres/SshPostgresDestinationAcceptanceTest.java | 4 ++++ docs/integrations/destinations/postgres.md | 3 ++- 9 files changed, 21 insertions(+), 9 deletions(-) diff --git a/airbyte-cdk/java/airbyte-cdk/core/build.gradle b/airbyte-cdk/java/airbyte-cdk/core/build.gradle index 0cfb8f6c101bc..cfef9562cbbe1 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/build.gradle +++ b/airbyte-cdk/java/airbyte-cdk/core/build.gradle @@ -72,7 +72,7 @@ dependencies { implementation libs.debezium.mongodb api libs.bundles.datadog - implementation 'org.apache.sshd:sshd-mina:2.8.0' + implementation 'org.apache.sshd:sshd-mina:2.11.0' implementation libs.testcontainers implementation libs.testcontainers.mysql diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties b/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties index 25d4002e6b5da..753eb4c8def57 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties @@ -1 +1 @@ -version=0.11.5 +version=0.12.0 diff --git a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/testFixtures/java/io/airbyte/cdk/integrations/standardtest/destination/DestinationAcceptanceTest.java b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/testFixtures/java/io/airbyte/cdk/integrations/standardtest/destination/DestinationAcceptanceTest.java index c0938ba6d4bcc..6aa6f1e6e0550 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/testFixtures/java/io/airbyte/cdk/integrations/standardtest/destination/DestinationAcceptanceTest.java +++ b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/testFixtures/java/io/airbyte/cdk/integrations/standardtest/destination/DestinationAcceptanceTest.java @@ -1268,6 +1268,13 @@ protected void runSyncAndVerifyStateOutput(final JsonNode config, .stream() .filter(m -> m.getType() == Type.STATE) .findFirst() + .map(msg -> { + // Modify state message to remove destination stats. + final AirbyteStateMessage clone = msg.getState(); + clone.setDestinationStats(null); + msg.setState(clone); + return msg; + }) .orElseGet(() -> { fail("Destination failed to output state"); return null; diff --git a/airbyte-integrations/connectors/destination-postgres-strict-encrypt/build.gradle b/airbyte-integrations/connectors/destination-postgres-strict-encrypt/build.gradle index 008910bcaa9e0..5614144c5a842 100644 --- a/airbyte-integrations/connectors/destination-postgres-strict-encrypt/build.gradle +++ b/airbyte-integrations/connectors/destination-postgres-strict-encrypt/build.gradle @@ -4,12 +4,12 @@ plugins { } airbyteJavaConnector { - cdkVersionRequired = '0.8.0' + cdkVersionRequired = '0.11.1' features = [ 'db-sources', // required for tests 'db-destinations' ] - useLocalCdk = false + useLocalCdk = true } //remove once upgrading the CDK version to 0.4.x or later diff --git a/airbyte-integrations/connectors/destination-postgres-strict-encrypt/metadata.yaml b/airbyte-integrations/connectors/destination-postgres-strict-encrypt/metadata.yaml index f53641ee30040..389254e3dafd6 100644 --- a/airbyte-integrations/connectors/destination-postgres-strict-encrypt/metadata.yaml +++ b/airbyte-integrations/connectors/destination-postgres-strict-encrypt/metadata.yaml @@ -9,7 +9,7 @@ data: connectorSubtype: database connectorType: destination definitionId: 25c5221d-dce2-4163-ade9-739ef790f503 - dockerImageTag: 0.5.1 + dockerImageTag: 0.5.2 dockerRepository: airbyte/destination-postgres-strict-encrypt githubIssueLabel: destination-postgres icon: postgresql.svg diff --git a/airbyte-integrations/connectors/destination-postgres/build.gradle b/airbyte-integrations/connectors/destination-postgres/build.gradle index 97722a7f955a3..33dca5c89a4fd 100644 --- a/airbyte-integrations/connectors/destination-postgres/build.gradle +++ b/airbyte-integrations/connectors/destination-postgres/build.gradle @@ -4,12 +4,12 @@ plugins { } airbyteJavaConnector { - cdkVersionRequired = '0.8.0' + cdkVersionRequired = '0.11.1' features = [ 'db-sources', // required for tests 'db-destinations', ] - useLocalCdk = false + useLocalCdk = true } //remove once upgrading the CDK version to 0.4.x or later diff --git a/airbyte-integrations/connectors/destination-postgres/metadata.yaml b/airbyte-integrations/connectors/destination-postgres/metadata.yaml index d3959cf70a7c0..2af3b0b80fb2b 100644 --- a/airbyte-integrations/connectors/destination-postgres/metadata.yaml +++ b/airbyte-integrations/connectors/destination-postgres/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: database connectorType: destination definitionId: 25c5221d-dce2-4163-ade9-739ef790f503 - dockerImageTag: 0.5.1 + dockerImageTag: 0.5.2 dockerRepository: airbyte/destination-postgres documentationUrl: https://docs.airbyte.com/integrations/destinations/postgres githubIssueLabel: destination-postgres diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/SshPostgresDestinationAcceptanceTest.java b/airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/SshPostgresDestinationAcceptanceTest.java index 2daaddc3b34e1..4412909f15394 100644 --- a/airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/SshPostgresDestinationAcceptanceTest.java +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/SshPostgresDestinationAcceptanceTest.java @@ -4,7 +4,10 @@ package io.airbyte.integrations.destination.postgres; +import static io.airbyte.cdk.integrations.base.ssh.SshTunnel.CONNECTION_OPTIONS_KEY; + import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.airbyte.cdk.db.Database; import io.airbyte.cdk.db.factory.DSLContextFactory; import io.airbyte.cdk.db.factory.DatabaseDriver; @@ -63,6 +66,7 @@ protected List retrieveRecordsFromTable(final String tableName, final .with("schema", "public") .withoutSsl() .build(); + ((ObjectNode) config).putObject(CONNECTION_OPTIONS_KEY); return SshTunnel.sshWrap( config, JdbcUtils.HOST_LIST_KEY, diff --git a/docs/integrations/destinations/postgres.md b/docs/integrations/destinations/postgres.md index d16622e49484f..bb3d6c1f3b754 100644 --- a/docs/integrations/destinations/postgres.md +++ b/docs/integrations/destinations/postgres.md @@ -170,6 +170,7 @@ Now that you have set up the Postgres destination connector, check out the follo | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:----------------------------------------------------------------------------------------------------| +| 0.5.2 | 2024-01-08 | [33875](https://github.com/airbytehq/airbyte/pull/33875) | Update CDK to get Tunnel heartbeats feature | | 0.5.1 | 2024-01-04 | [33873](https://github.com/airbytehq/airbyte/pull/33873) | Install normalization to enable DV2 beta | | 0.5.0 | 2023-12-18 | [33507](https://github.com/airbytehq/airbyte/pull/33507) | Upgrade to latest CDK; Fix DATs and tests | | 0.4.0 | 2023-06-27 | [\#27781](https://github.com/airbytehq/airbyte/pull/27781) | License Update: Elv2 | @@ -188,4 +189,4 @@ Now that you have set up the Postgres destination connector, check out the follo | 0.3.13 | 2021-12-01 | [\#8371](https://github.com/airbytehq/airbyte/pull/8371) | Fixed incorrect handling "\n" in ssh key | | 0.3.12 | 2021-11-08 | [\#7719](https://github.com/airbytehq/airbyte/pull/7719) | Improve handling of wide rows by buffering records based on their byte size rather than their count | | 0.3.11 | 2021-09-07 | [\#5743](https://github.com/airbytehq/airbyte/pull/5743) | Add SSH Tunnel support | -| 0.3.10 | 2021-08-11 | [\#5336](https://github.com/airbytehq/airbyte/pull/5336) | Destination Postgres: fix \u0000\(NULL\) value processing | +| 0.3.10 | 2021-08-11 | [\#5336](https://github.com/airbytehq/airbyte/pull/5336) | Destination Postgres: fix \u0000\(NULL\) value processing | \ No newline at end of file From dcbec4a65bd429eaf9e04add7ff24c01686a9391 Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Wed, 10 Jan 2024 16:36:27 -0800 Subject: [PATCH 053/574] =?UTF-8?q?=F0=9F=93=9D=20Fix=20BigQuery=20Destina?= =?UTF-8?q?tion:=20fail=20when=20using=20Google=20Default=20Application=20?= =?UTF-8?q?Credentials=20(#34073)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../destination-bigquery/metadata.yaml | 2 +- .../bigquery/BigQueryDestination.java | 34 ++-- .../destination/bigquery/BigQueryUtils.java | 12 -- .../bigquery/BigQueryUtilsTest.java | 26 --- docs/integrations/destinations/bigquery.md | 171 ++++++++++++------ 5 files changed, 142 insertions(+), 103 deletions(-) diff --git a/airbyte-integrations/connectors/destination-bigquery/metadata.yaml b/airbyte-integrations/connectors/destination-bigquery/metadata.yaml index 27e56ad79b3d5..5d5abb06054c9 100644 --- a/airbyte-integrations/connectors/destination-bigquery/metadata.yaml +++ b/airbyte-integrations/connectors/destination-bigquery/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: database connectorType: destination definitionId: 22f6c74f-5699-40ff-833c-4a879ea40133 - dockerImageTag: 2.3.28 + dockerImageTag: 2.3.29 dockerRepository: airbyte/destination-bigquery documentationUrl: https://docs.airbyte.com/integrations/destinations/bigquery githubIssueLabel: destination-bigquery diff --git a/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryDestination.java b/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryDestination.java index 18fd44b880444..edec86e609917 100644 --- a/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryDestination.java +++ b/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryDestination.java @@ -190,16 +190,19 @@ public static BigQuery getBigQuery(final JsonNode config) { } public static GoogleCredentials getServiceAccountCredentials(final JsonNode config) throws IOException { - if (!BigQueryUtils.isUsingJsonCredentials(config)) { + final JsonNode serviceAccountKey = config.get(BigQueryConsts.CONFIG_CREDS); + // Follows this order of resolution: + // https://cloud.google.com/java/docs/reference/google-auth-library/latest/com.google.auth.oauth2.GoogleCredentials#com_google_auth_oauth2_GoogleCredentials_getApplicationDefault + if (serviceAccountKey == null) { LOGGER.info("No service account key json is provided. It is required if you are using Airbyte cloud."); LOGGER.info("Using the default service account credential from environment."); return GoogleCredentials.getApplicationDefault(); } // The JSON credential can either be a raw JSON object, or a serialized JSON object. - final String credentialsString = config.get(BigQueryConsts.CONFIG_CREDS).isObject() - ? Jsons.serialize(config.get(BigQueryConsts.CONFIG_CREDS)) - : config.get(BigQueryConsts.CONFIG_CREDS).asText(); + final String credentialsString = serviceAccountKey.isObject() + ? Jsons.serialize(serviceAccountKey) + : serviceAccountKey.asText(); return GoogleCredentials.fromStream( new ByteArrayInputStream(credentialsString.getBytes(Charsets.UTF_8))); } @@ -236,15 +239,20 @@ public SerializedAirbyteMessageConsumer getSerializedMessageConsumer(final JsonN AirbyteExceptionHandler.addAllStringsInConfigForDeinterpolation(config); final JsonNode serviceAccountKey = config.get(BigQueryConsts.CONFIG_CREDS); - if (serviceAccountKey.isTextual()) { - // There are cases where we fail to deserialize the service account key. In these cases, we - // shouldn't do anything. - // Google's creds library is more lenient with JSON-parsing than Jackson, and I'd rather just let it - // go. - Jsons.tryDeserialize(serviceAccountKey.asText()) - .ifPresent(AirbyteExceptionHandler::addAllStringsInConfigForDeinterpolation); - } else { - AirbyteExceptionHandler.addAllStringsInConfigForDeinterpolation(serviceAccountKey); + if (serviceAccountKey != null) { + // If the service account key is a non-null string, we will try to + // deserialize it. Otherwise, we will let the Google library find it in + // the environment during the client initialization. + if (serviceAccountKey.isTextual()) { + // There are cases where we fail to deserialize the service account key. In these cases, we + // shouldn't do anything. + // Google's creds library is more lenient with JSON-parsing than Jackson, and I'd rather just let it + // go. + Jsons.tryDeserialize(serviceAccountKey.asText()) + .ifPresent(AirbyteExceptionHandler::addAllStringsInConfigForDeinterpolation); + } else { + AirbyteExceptionHandler.addAllStringsInConfigForDeinterpolation(serviceAccountKey); + } } if (uploadingMethod == UploadingMethod.STANDARD) { diff --git a/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryUtils.java b/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryUtils.java index 255b685190b28..3377acceb1dab 100644 --- a/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryUtils.java +++ b/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/BigQueryUtils.java @@ -396,18 +396,6 @@ public static JobInfo.WriteDisposition getWriteDisposition(final DestinationSync } } - public static boolean isUsingJsonCredentials(final JsonNode config) { - if (!config.has(BigQueryConsts.CONFIG_CREDS)) { - return false; - } - final JsonNode json = config.get(BigQueryConsts.CONFIG_CREDS); - if (json.isTextual()) { - return !json.asText().isEmpty(); - } else { - return !Jsons.serialize(json).isEmpty(); - } - } - // https://googleapis.dev/python/bigquery/latest/generated/google.cloud.bigquery.client.Client.html public static Integer getBigQueryClientChunkSize(final JsonNode config) { Integer chunkSizeFromConfig = null; diff --git a/airbyte-integrations/connectors/destination-bigquery/src/test/java/io/airbyte/integrations/destination/bigquery/BigQueryUtilsTest.java b/airbyte-integrations/connectors/destination-bigquery/src/test/java/io/airbyte/integrations/destination/bigquery/BigQueryUtilsTest.java index 8043726bda18d..0f03515ee0876 100644 --- a/airbyte-integrations/connectors/destination-bigquery/src/test/java/io/airbyte/integrations/destination/bigquery/BigQueryUtilsTest.java +++ b/airbyte-integrations/connectors/destination-bigquery/src/test/java/io/airbyte/integrations/destination/bigquery/BigQueryUtilsTest.java @@ -5,18 +5,13 @@ package io.airbyte.integrations.destination.bigquery; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableMap; import io.airbyte.commons.json.Jsons; -import java.util.Collections; -import java.util.Map; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -58,27 +53,6 @@ public void testGetDatasetIdFail(final String projectId, final String datasetId, assertEquals(expected, exception.getMessage()); } - @Test - public void testIsUsingJsonCredentials() { - // empty - final JsonNode emptyConfig = Jsons.jsonNode(Collections.emptyMap()); - assertFalse(BigQueryUtils.isUsingJsonCredentials(emptyConfig)); - - // empty text - final JsonNode emptyTextConfig = Jsons.jsonNode(Map.of(BigQueryConsts.CONFIG_CREDS, "")); - assertFalse(BigQueryUtils.isUsingJsonCredentials(emptyTextConfig)); - - // non-empty text - final JsonNode nonEmptyTextConfig = Jsons.jsonNode( - Map.of(BigQueryConsts.CONFIG_CREDS, "{ \"service_account\": \"test@airbyte.io\" }")); - assertTrue(BigQueryUtils.isUsingJsonCredentials(nonEmptyTextConfig)); - - // object - final JsonNode objectConfig = Jsons.jsonNode(Map.of( - BigQueryConsts.CONFIG_CREDS, Jsons.jsonNode(Map.of("service_account", "test@airbyte.io")))); - assertTrue(BigQueryUtils.isUsingJsonCredentials(objectConfig)); - } - private static Stream validBigQueryIdProvider() { return Stream.of( Arguments.arguments("my-project", "my_dataset", "my_dataset"), diff --git a/docs/integrations/destinations/bigquery.md b/docs/integrations/destinations/bigquery.md index 55d902896ff1a..646ffcc7128f7 100644 --- a/docs/integrations/destinations/bigquery.md +++ b/docs/integrations/destinations/bigquery.md @@ -1,68 +1,115 @@ # BigQuery -Setting up the BigQuery destination connector involves setting up the data loading method (BigQuery Standard method and Google Cloud Storage bucket) and configuring the BigQuery destination connector using the Airbyte UI. +Setting up the BigQuery destination connector involves setting up the data loading method (BigQuery +Standard method and Google Cloud Storage bucket) and configuring the BigQuery destination connector +using the Airbyte UI. This page guides you through setting up the BigQuery destination connector. ## Prerequisites -- For Airbyte Open Source users using the [Postgres](https://docs.airbyte.com/integrations/sources/postgres) source connector, [upgrade](https://docs.airbyte.com/operator-guides/upgrading-airbyte/) your Airbyte platform to version `v0.40.0-alpha` or newer and upgrade your BigQuery connector to version `1.1.14` or newer +- For Airbyte Open Source users using the + [Postgres](https://docs.airbyte.com/integrations/sources/postgres) source connector, + [upgrade](https://docs.airbyte.com/operator-guides/upgrading-airbyte/) your Airbyte platform to + version `v0.40.0-alpha` or newer and upgrade your BigQuery connector to version `1.1.14` or newer - [A Google Cloud project with BigQuery enabled](https://cloud.google.com/bigquery/docs/quickstarts/query-public-dataset-console) -- [A BigQuery dataset](https://cloud.google.com/bigquery/docs/quickstarts/quickstart-web-ui#create_a_dataset) to sync data to. +- [A BigQuery dataset](https://cloud.google.com/bigquery/docs/quickstarts/quickstart-web-ui#create_a_dataset) + to sync data to. - **Note:** Queries written in BigQuery can only reference datasets in the same physical location. If you plan on combining the data that Airbyte syncs with data from other datasets in your queries, create the datasets in the same location on Google Cloud. For more information, read [Introduction to Datasets](https://cloud.google.com/bigquery/docs/datasets-intro) + **Note:** Queries written in BigQuery can only reference datasets in the same physical location. + If you plan on combining the data that Airbyte syncs with data from other datasets in your + queries, create the datasets in the same location on Google Cloud. For more information, read + [Introduction to Datasets](https://cloud.google.com/bigquery/docs/datasets-intro) -- (Required for Airbyte Cloud; Optional for Airbyte Open Source) A Google Cloud [Service Account](https://cloud.google.com/iam/docs/service-accounts) with the [`BigQuery User`](https://cloud.google.com/bigquery/docs/access-control#bigquery) and [`BigQuery Data Editor`](https://cloud.google.com/bigquery/docs/access-control#bigquery) roles and the [Service Account Key in JSON format](https://cloud.google.com/iam/docs/creating-managing-service-account-keys). +- (Required for Airbyte Cloud; Optional for Airbyte Open Source) A Google Cloud + [Service Account](https://cloud.google.com/iam/docs/service-accounts) with the + [`BigQuery User`](https://cloud.google.com/bigquery/docs/access-control#bigquery) and + [`BigQuery Data Editor`](https://cloud.google.com/bigquery/docs/access-control#bigquery) roles and + the + [Service Account Key in JSON format](https://cloud.google.com/iam/docs/creating-managing-service-account-keys). ## Setup guide ### Step 1: Set up a data loading method -Although you can load data using BigQuery's [`INSERTS`](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax), we highly recommend using a [Google Cloud Storage bucket](https://cloud.google.com/storage/docs/introduction) not only for performance and cost but reliability since larger datasets are prone to more failures when using standard inserts. +Although you can load data using BigQuery's +[`INSERTS`](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax), we highly +recommend using a [Google Cloud Storage bucket](https://cloud.google.com/storage/docs/introduction) +not only for performance and cost but reliability since larger datasets are prone to more failures +when using standard inserts. #### (Recommended) Using a Google Cloud Storage bucket To use a Google Cloud Storage bucket: -1. [Create a Cloud Storage bucket](https://cloud.google.com/storage/docs/creating-buckets) with the Protection Tools set to `none` or `Object versioning`. Make sure the bucket does not have a [retention policy](https://cloud.google.com/storage/docs/samples/storage-set-retention-policy). +1. [Create a Cloud Storage bucket](https://cloud.google.com/storage/docs/creating-buckets) with the + Protection Tools set to `none` or `Object versioning`. Make sure the bucket does not have a + [retention policy](https://cloud.google.com/storage/docs/samples/storage-set-retention-policy). 2. [Create an HMAC key and access ID](https://cloud.google.com/storage/docs/authentication/managing-hmackeys#create). -3. Grant the [`Storage Object Admin` role](https://cloud.google.com/storage/docs/access-control/iam-roles#standard-roles) to the Google Cloud [Service Account](https://cloud.google.com/iam/docs/service-accounts). This must be the same service account as the one you configure for BigQuery access in the [BigQuery connector setup step](#step-2-set-up-the-bigquery-connector). -4. Make sure your Cloud Storage bucket is accessible from the machine running Airbyte. The easiest way to verify if Airbyte is able to connect to your bucket is via the check connection tool in the UI. - -Your bucket must be encrypted using a Google-managed encryption key (this is the default setting when creating a new bucket). We currently do not support buckets using customer-managed encryption keys (CMEK). You can view this setting under the "Configuration" tab of your GCS bucket, in the `Encryption type` row. +3. Grant the + [`Storage Object Admin` role](https://cloud.google.com/storage/docs/access-control/iam-roles#standard-roles) + to the Google Cloud [Service Account](https://cloud.google.com/iam/docs/service-accounts). This + must be the same service account as the one you configure for BigQuery access in the + [BigQuery connector setup step](#step-2-set-up-the-bigquery-connector). +4. Make sure your Cloud Storage bucket is accessible from the machine running Airbyte. The easiest + way to verify if Airbyte is able to connect to your bucket is via the check connection tool in + the UI. + +Your bucket must be encrypted using a Google-managed encryption key (this is the default setting +when creating a new bucket). We currently do not support buckets using customer-managed encryption +keys (CMEK). You can view this setting under the "Configuration" tab of your GCS bucket, in the +`Encryption type` row. #### Using `INSERT` -You can use BigQuery's [`INSERT`](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax) statement to upload data directly from your source to BigQuery. While this is faster to set up initially, we strongly recommend not using this option for anything other than a quick demo. Due to the Google BigQuery SDK client limitations, using `INSERT` is 10x slower than using a Google Cloud Storage bucket, and you may see some failures for big datasets and slow sources (For example, if reading from a source takes more than 10-12 hours). For more details, refer to https://github.com/airbytehq/airbyte/issues/3549 +You can use BigQuery's +[`INSERT`](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax) statement to +upload data directly from your source to BigQuery. While this is faster to set up initially, we +strongly recommend not using this option for anything other than a quick demo. Due to the Google +BigQuery SDK client limitations, using `INSERT` is 10x slower than using a Google Cloud Storage +bucket, and you may see some failures for big datasets and slow sources (For example, if reading +from a source takes more than 10-12 hours). For more details, refer to +https://github.com/airbytehq/airbyte/issues/3549 ### Step 2: Set up the BigQuery connector -1. Log into your [Airbyte Cloud](https://cloud.airbyte.com/workspaces) or Airbyte Open Source account. +1. Log into your [Airbyte Cloud](https://cloud.airbyte.com/workspaces) or Airbyte Open Source + account. 2. Click **Destinations** and then click **+ New destination**. 3. On the Set up the destination page, select **BigQuery** from the **Destination type** dropdown. 4. Enter the name for the BigQuery connector. -5. For **Project ID**, enter your [Google Cloud project ID](https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects). -6. For **Dataset Location**, select the location of your BigQuery dataset. - :::warning - You cannot change the location later. - ::: -7. For **Default Dataset ID**, enter the BigQuery [Dataset ID](https://cloud.google.com/bigquery/docs/datasets#create-dataset). -8. For **Loading Method**, select [Standard Inserts](#using-insert) or [GCS Staging](#recommended-using-a-google-cloud-storage-bucket). - :::tip - We recommend using the GCS Staging option. - ::: -9. For **Service Account Key JSON (Required for cloud, optional for open-source)**, enter the Google Cloud [Service Account Key in JSON format](https://cloud.google.com/iam/docs/creating-managing-service-account-keys). -10. For **Transformation Query Run Type (Optional)**, select **interactive** to have [BigQuery run interactive query jobs](https://cloud.google.com/bigquery/docs/running-queries#queries) or **batch** to have [BigQuery run batch queries](https://cloud.google.com/bigquery/docs/running-queries#batch). - - :::note - Interactive queries are executed as soon as possible and count towards daily concurrent quotas and limits, while batch queries are executed as soon as idle resources are available in the BigQuery shared resource pool. If BigQuery hasn't started the query within 24 hours, BigQuery changes the job priority to interactive. Batch queries don't count towards your concurrent rate limit, making it easier to start many queries at once. - ::: - -11. For **Google BigQuery Client Chunk Size (Optional)**, use the default value of 15 MiB. Later, if you see networking or memory management problems with the sync (specifically on the destination), try decreasing the chunk size. In that case, the sync will be slower but more likely to succeed. +5. For **Project ID**, enter your + [Google Cloud project ID](https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects). +6. For **Dataset Location**, select the location of your BigQuery dataset. :::warning You cannot + change the location later. ::: +7. For **Default Dataset ID**, enter the BigQuery + [Dataset ID](https://cloud.google.com/bigquery/docs/datasets#create-dataset). +8. For **Loading Method**, select [Standard Inserts](#using-insert) or + [GCS Staging](#recommended-using-a-google-cloud-storage-bucket). :::tip We recommend using the + GCS Staging option. ::: +9. For **Service Account Key JSON (Required for cloud, optional for open-source)**, enter the Google + Cloud + [Service Account Key in JSON format](https://cloud.google.com/iam/docs/creating-managing-service-account-keys). +10. For **Transformation Query Run Type (Optional)**, select **interactive** to have + [BigQuery run interactive query jobs](https://cloud.google.com/bigquery/docs/running-queries#queries) + or **batch** to have + [BigQuery run batch queries](https://cloud.google.com/bigquery/docs/running-queries#batch). + + :::note Interactive queries are executed as soon as possible and count towards daily concurrent + quotas and limits, while batch queries are executed as soon as idle resources are available in + the BigQuery shared resource pool. If BigQuery hasn't started the query within 24 hours, + BigQuery changes the job priority to interactive. Batch queries don't count towards your + concurrent rate limit, making it easier to start many queries at once. ::: + +11. For **Google BigQuery Client Chunk Size (Optional)**, use the default value of 15 MiB. Later, if + you see networking or memory management problems with the sync (specifically on the + destination), try decreasing the chunk size. In that case, the sync will be slower but more + likely to succeed. ## Supported sync modes -The BigQuery destination connector supports the following [sync modes](https://docs.airbyte.com/cloud/core-concepts#connection-sync-modes): +The BigQuery destination connector supports the following +[sync modes](https://docs.airbyte.com/cloud/core-concepts#connection-sync-modes): - Full Refresh Sync - Incremental - Append Sync @@ -70,39 +117,55 @@ The BigQuery destination connector supports the following [sync modes](https://d ## Output schema -Airbyte outputs each stream into its own raw table in `airbyte_internal` dataset by default (can be overriden by user) and a final table with Typed columns. Contents in raw table are _NOT_ deduplicated. +Airbyte outputs each stream into its own raw table in `airbyte_internal` dataset by default (can be +overriden by user) and a final table with Typed columns. Contents in raw table are _NOT_ +deduplicated. ### Raw Table schema | Airbyte field | Description | Column type | -|------------------------|--------------------------------------------------------------------|-------------| +| ---------------------- | ------------------------------------------------------------------ | ----------- | | \_airbyte_raw_id | A UUID assigned to each processed event | STRING | | \_airbyte_extracted_at | A timestamp for when the event was pulled from the data source | TIMESTAMP | | \_airbyte_loaded_at | Timestamp to indicate when the record was loaded into Typed tables | TIMESTAMP | | \_airbyte_data | A JSON blob with the event data. | STRING | -**Note:** Although the contents of the `_airbyte_data` are fairly stable, schema of the raw table could be subject to change in future versions. +**Note:** Although the contents of the `_airbyte_data` are fairly stable, schema of the raw table +could be subject to change in future versions. ### Final Table schema -- `airbyte_raw_id`: A UUID assigned by Airbyte to each event that is processed. The column type in BigQuery is `String`. -- `airbyte_extracted_at`: A timestamp representing when the event was pulled from the data source. The column type in BigQuery is `Timestamp`. -- `_airbyte_meta`: A JSON blob representing typing errors. You can query these results to audit misformatted or unexpected data. The column type in BigQuery is `JSON`. - ... and a column of the proper data type for each of the top-level properties from your source's schema. Arrays and Objects will remain as JSON columns in BigQuery. Learn more about Typing and Deduping [here](/understanding-airbyte/typing-deduping) - -The output tables in BigQuery are partitioned by the Time-unit column `airbyte_extracted_at` at a daily granularity and clustered by `airbyte_extracted_at` and the table Primary Keys. Partitions boundaries are based on UTC time. -This is useful to limit the number of partitions scanned when querying these partitioned tables, by using a predicate filter (a `WHERE` clause). Filters on the partitioning column are used to prune the partitions and reduce the query cost. (The parameter **Require partition filter** is not enabled by Airbyte, but you may toggle it by updating the produced tables.) +- `airbyte_raw_id`: A UUID assigned by Airbyte to each event that is processed. The column type in + BigQuery is `String`. +- `airbyte_extracted_at`: A timestamp representing when the event was pulled from the data source. + The column type in BigQuery is `Timestamp`. +- `_airbyte_meta`: A JSON blob representing typing errors. You can query these results to audit + misformatted or unexpected data. The column type in BigQuery is `JSON`. ... and a column of the + proper data type for each of the top-level properties from your source's schema. Arrays and + Objects will remain as JSON columns in BigQuery. Learn more about Typing and Deduping + [here](/understanding-airbyte/typing-deduping) + +The output tables in BigQuery are partitioned by the Time-unit column `airbyte_extracted_at` at a +daily granularity and clustered by `airbyte_extracted_at` and the table Primary Keys. Partitions +boundaries are based on UTC time. This is useful to limit the number of partitions scanned when +querying these partitioned tables, by using a predicate filter (a `WHERE` clause). Filters on the +partitioning column are used to prune the partitions and reduce the query cost. (The parameter +**Require partition filter** is not enabled by Airbyte, but you may toggle it by updating the +produced tables.) ## BigQuery Naming Conventions -Follow [BigQuery Datasets Naming conventions](https://cloud.google.com/bigquery/docs/datasets#dataset-naming). +Follow +[BigQuery Datasets Naming conventions](https://cloud.google.com/bigquery/docs/datasets#dataset-naming). -Airbyte converts any invalid characters into `_` characters when writing data. However, since datasets that begin with `_` are hidden on the BigQuery Explorer panel, Airbyte prepends the namespace with `n` for converted namespaces. +Airbyte converts any invalid characters into `_` characters when writing data. However, since +datasets that begin with `_` are hidden on the BigQuery Explorer panel, Airbyte prepends the +namespace with `n` for converted namespaces. ## Data type map | Airbyte type | BigQuery type | -|:------------------------------------|:--------------| +| :---------------------------------- | :------------ | | STRING | STRING | | STRING (BASE64) | STRING | | STRING (BIG_NUMBER) | STRING | @@ -122,16 +185,21 @@ Airbyte converts any invalid characters into `_` characters when writing data. H The service account does not have the proper permissions. -- Make sure the BigQuery service account has `BigQuery User` and `BigQuery Data Editor` roles or equivalent permissions as those two roles. -- If the GCS staging mode is selected, ensure the BigQuery service account has the right permissions to the GCS bucket and path or the `Cloud Storage Admin` role, which includes a superset of the required permissions. +- Make sure the BigQuery service account has `BigQuery User` and `BigQuery Data Editor` roles or + equivalent permissions as those two roles. +- If the GCS staging mode is selected, ensure the BigQuery service account has the right permissions + to the GCS bucket and path or the `Cloud Storage Admin` role, which includes a superset of the + required permissions. The HMAC key is wrong. -- Make sure the HMAC key is created for the BigQuery service account, and the service account has permission to access the GCS bucket and path. +- Make sure the HMAC key is created for the BigQuery service account, and the service account has + permission to access the GCS bucket and path. ## Tutorials -Now that you have set up the BigQuery destination connector, check out the following BigQuery tutorials: +Now that you have set up the BigQuery destination connector, check out the following BigQuery +tutorials: - [Export Google Analytics data to BigQuery](https://airbyte.com/tutorials/export-google-analytics-to-bigquery) - [Load data from Facebook Ads to BigQuery](https://airbyte.com/tutorials/facebook-ads-to-bigquery) @@ -141,7 +209,8 @@ Now that you have set up the BigQuery destination connector, check out the follo ## Changelog | Version | Date | Pull Request | Subject | -|:--------|:-----------|:-----------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| :------ | :--------- | :--------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2.3.29 | 2024-01-09 | [34003](https://github.com/airbytehq/airbyte/pull/34003) | Fix loading credentials from GCP Env | | 2.3.28 | 2024-01-08 | [34021](https://github.com/airbytehq/airbyte/pull/34021) | Add idempotency ids in dummy insert for check call | | 2.3.27 | 2024-01-05 | [\#33948](https://github.com/airbytehq/airbyte/pull/33948) | Skip retrieving initial table state when setup fails | | 2.3.26 | 2024-01-04 | [\#33730](https://github.com/airbytehq/airbyte/pull/33730) | Internal code structure changes | @@ -302,4 +371,4 @@ Now that you have set up the BigQuery destination connector, check out the follo | 0.3.10 | 2021-07-28 | [\#3549](https://github.com/airbytehq/airbyte/issues/3549) | Add extended logs and made JobId filled with region and projectId | | 0.3.9 | 2021-07-28 | [\#5026](https://github.com/airbytehq/airbyte/pull/5026) | Add sanitized json fields in raw tables to handle quotes in column names | | 0.3.6 | 2021-06-18 | [\#3947](https://github.com/airbytehq/airbyte/issues/3947) | Service account credentials are now optional. | -| 0.3.4 | 2021-06-07 | [\#3277](https://github.com/airbytehq/airbyte/issues/3277) | Add dataset location option | \ No newline at end of file +| 0.3.4 | 2021-06-07 | [\#3277](https://github.com/airbytehq/airbyte/issues/3277) | Add dataset location option | From d29cb2d41e62ec7cf49f3a2d2a04d8fbc5978d80 Mon Sep 17 00:00:00 2001 From: Gireesh Sreepathi Date: Wed, 10 Jan 2024 16:48:14 -0800 Subject: [PATCH 054/574] Publish CDK and fix postgres to use cdk (#34135) Signed-off-by: Gireesh Sreepathi --- airbyte-cdk/java/airbyte-cdk/README.md | 1 + .../destination-postgres-strict-encrypt/build.gradle | 4 ++-- .../destination-postgres-strict-encrypt/metadata.yaml | 2 +- .../connectors/destination-postgres/build.gradle | 4 ++-- .../connectors/destination-postgres/metadata.yaml | 2 +- docs/integrations/destinations/postgres.md | 1 + 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/airbyte-cdk/java/airbyte-cdk/README.md b/airbyte-cdk/java/airbyte-cdk/README.md index 363532eadfbcc..5a45cbb2c3021 100644 --- a/airbyte-cdk/java/airbyte-cdk/README.md +++ b/airbyte-cdk/java/airbyte-cdk/README.md @@ -166,6 +166,7 @@ MavenLocal debugging steps: | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0.12.0 | 2024-01-10 | [\#33875](https://github.com/airbytehq/airbyte/pull/33875) | Upgrade sshd-mina to 2.11.1 | | 0.11.5 | 2024-01-10 | [\#34119](https://github.com/airbytehq/airbyte/pull/34119) | Remove wal2json support for postgres+debezium. | | 0.11.4 | 2024-01-09 | [\#33305](https://github.com/airbytehq/airbyte/pull/33305) | Source stats in incremental syncs | | 0.11.3 | 2023-01-09 | [\#33658](https://github.com/airbytehq/airbyte/pull/33658) | Always fail when debezium fails, even if it happened during the setup phase. | diff --git a/airbyte-integrations/connectors/destination-postgres-strict-encrypt/build.gradle b/airbyte-integrations/connectors/destination-postgres-strict-encrypt/build.gradle index 5614144c5a842..11cfb6f26b788 100644 --- a/airbyte-integrations/connectors/destination-postgres-strict-encrypt/build.gradle +++ b/airbyte-integrations/connectors/destination-postgres-strict-encrypt/build.gradle @@ -4,12 +4,12 @@ plugins { } airbyteJavaConnector { - cdkVersionRequired = '0.11.1' + cdkVersionRequired = '0.12.0' features = [ 'db-sources', // required for tests 'db-destinations' ] - useLocalCdk = true + useLocalCdk = false } //remove once upgrading the CDK version to 0.4.x or later diff --git a/airbyte-integrations/connectors/destination-postgres-strict-encrypt/metadata.yaml b/airbyte-integrations/connectors/destination-postgres-strict-encrypt/metadata.yaml index 389254e3dafd6..9cd1928961ec3 100644 --- a/airbyte-integrations/connectors/destination-postgres-strict-encrypt/metadata.yaml +++ b/airbyte-integrations/connectors/destination-postgres-strict-encrypt/metadata.yaml @@ -9,7 +9,7 @@ data: connectorSubtype: database connectorType: destination definitionId: 25c5221d-dce2-4163-ade9-739ef790f503 - dockerImageTag: 0.5.2 + dockerImageTag: 0.5.3 dockerRepository: airbyte/destination-postgres-strict-encrypt githubIssueLabel: destination-postgres icon: postgresql.svg diff --git a/airbyte-integrations/connectors/destination-postgres/build.gradle b/airbyte-integrations/connectors/destination-postgres/build.gradle index 33dca5c89a4fd..ed2e3d3ffcbdd 100644 --- a/airbyte-integrations/connectors/destination-postgres/build.gradle +++ b/airbyte-integrations/connectors/destination-postgres/build.gradle @@ -4,12 +4,12 @@ plugins { } airbyteJavaConnector { - cdkVersionRequired = '0.11.1' + cdkVersionRequired = '0.12.0' features = [ 'db-sources', // required for tests 'db-destinations', ] - useLocalCdk = true + useLocalCdk = false } //remove once upgrading the CDK version to 0.4.x or later diff --git a/airbyte-integrations/connectors/destination-postgres/metadata.yaml b/airbyte-integrations/connectors/destination-postgres/metadata.yaml index 2af3b0b80fb2b..54f3288be39fd 100644 --- a/airbyte-integrations/connectors/destination-postgres/metadata.yaml +++ b/airbyte-integrations/connectors/destination-postgres/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: database connectorType: destination definitionId: 25c5221d-dce2-4163-ade9-739ef790f503 - dockerImageTag: 0.5.2 + dockerImageTag: 0.5.3 dockerRepository: airbyte/destination-postgres documentationUrl: https://docs.airbyte.com/integrations/destinations/postgres githubIssueLabel: destination-postgres diff --git a/docs/integrations/destinations/postgres.md b/docs/integrations/destinations/postgres.md index bb3d6c1f3b754..32dbef5d9243c 100644 --- a/docs/integrations/destinations/postgres.md +++ b/docs/integrations/destinations/postgres.md @@ -170,6 +170,7 @@ Now that you have set up the Postgres destination connector, check out the follo | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:----------------------------------------------------------------------------------------------------| +| 0.5.3 | 2024-01-10 | [34135](https://github.com/airbytehq/airbyte/pull/34135) | Use published CDK missed in previous release | | 0.5.2 | 2024-01-08 | [33875](https://github.com/airbytehq/airbyte/pull/33875) | Update CDK to get Tunnel heartbeats feature | | 0.5.1 | 2024-01-04 | [33873](https://github.com/airbytehq/airbyte/pull/33873) | Install normalization to enable DV2 beta | | 0.5.0 | 2023-12-18 | [33507](https://github.com/airbytehq/airbyte/pull/33507) | Upgrade to latest CDK; Fix DATs and tests | From 7d7f33c09f9502653d65cb155960c9ac6a5f6e56 Mon Sep 17 00:00:00 2001 From: "Aaron (\"AJ\") Steers" Date: Thu, 11 Jan 2024 00:16:04 -0800 Subject: [PATCH 055/574] Source Faker: Add support for PyPi and AirbyteLib entrypoints (#34033) --- .../java-connectors-generic/devcontainer.json | 2 +- .../devcontainer.json | 65 +++++++++++++++++++ .../connectors/source-faker/Dockerfile | 2 +- .../connectors/source-faker/main.py | 8 +-- .../connectors/source-faker/metadata.yaml | 6 +- .../connectors/source-faker/setup.py | 6 ++ .../source-faker/source_faker/run.py | 18 +++++ docs/integrations/sources/faker.md | 1 + 8 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 .devcontainer/python-connectors-generic/devcontainer.json create mode 100644 airbyte-integrations/connectors/source-faker/source_faker/run.py diff --git a/.devcontainer/java-connectors-generic/devcontainer.json b/.devcontainer/java-connectors-generic/devcontainer.json index b7041313bbc6c..c35b8502dd772 100644 --- a/.devcontainer/java-connectors-generic/devcontainer.json +++ b/.devcontainer/java-connectors-generic/devcontainer.json @@ -1,6 +1,6 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the { - "name": "Connector Development DevContainer (Generic)", + "name": "Java Development DevContainer (Generic)", "image": "mcr.microsoft.com/devcontainers/java:0-17", "features": { diff --git a/.devcontainer/python-connectors-generic/devcontainer.json b/.devcontainer/python-connectors-generic/devcontainer.json new file mode 100644 index 0000000000000..539a80499800e --- /dev/null +++ b/.devcontainer/python-connectors-generic/devcontainer.json @@ -0,0 +1,65 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +{ + "name": "Python Development DevContainer (Generic)", + + "image": "mcr.microsoft.com/devcontainers/python:0-3.10", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker": {}, + "ghcr.io/devcontainers/features/python:1": { + "installGradle": true, + "version": "3.10", + "installTools": true + }, + "ghcr.io/devcontainers-contrib/features/poetry:2": {} + }, + + // Deterministic order reduces cache busting + "overrideFeatureInstallOrder": [ + "ghcr.io/devcontainers/features/docker-in-docker", + "ghcr.io/devcontainers/features/python", + "ghcr.io/devcontainers-contrib/features/poetry" + ], + + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + // Python extensions: + "charliermarsh.ruff", + "matangover.mypy", + "ms-python.python", + "ms-python.vscode-pylance", + + // Toml support + "tamasfe.even-better-toml", + + // Yaml and JSON Schema support: + "redhat.vscode-yaml", + + // Contributing: + "GitHub.vscode-pull-request-github" + ], + "settings": { + "extensions.ignoreRecommendations": true, + "git.openRepositoryInParentFolders": "always" + } + } + }, + + // Mark the root directory as 'safe' for git. + "initializeCommand": "git config --add safe.directory /workspaces/airbyte", + + // Setup airbyte-ci on the container: + "postCreateCommand": "make tools.airbyte-ci-dev.install", + + "containerEnv": { + // Deterministic Poetry virtual env location: `./.venv` + "POETRY_VIRTUALENVS_IN_PROJECT": "true" + } + + // Override to change the directory that the IDE opens by default: + // "workspaceFolder": "/workspaces/airbyte" + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/airbyte-integrations/connectors/source-faker/Dockerfile b/airbyte-integrations/connectors/source-faker/Dockerfile index d0648a0212e19..e880e4f38beee 100644 --- a/airbyte-integrations/connectors/source-faker/Dockerfile +++ b/airbyte-integrations/connectors/source-faker/Dockerfile @@ -34,5 +34,5 @@ COPY source_faker ./source_faker ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] -LABEL io.airbyte.version=5.0.0 +LABEL io.airbyte.version=5.0.1 LABEL io.airbyte.name=airbyte/source-faker diff --git a/airbyte-integrations/connectors/source-faker/main.py b/airbyte-integrations/connectors/source-faker/main.py index 782659c7a6fbf..9df2974ae7bda 100644 --- a/airbyte-integrations/connectors/source-faker/main.py +++ b/airbyte-integrations/connectors/source-faker/main.py @@ -3,11 +3,7 @@ # -import sys - -from airbyte_cdk.entrypoint import launch -from source_faker import SourceFaker +from source_faker.run import run if __name__ == "__main__": - source = SourceFaker() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-faker/metadata.yaml b/airbyte-integrations/connectors/source-faker/metadata.yaml index 83aa3520b7110..fdd0575e480be 100644 --- a/airbyte-integrations/connectors/source-faker/metadata.yaml +++ b/airbyte-integrations/connectors/source-faker/metadata.yaml @@ -7,7 +7,7 @@ data: connectorSubtype: api connectorType: source definitionId: dfd88b22-b603-4c3d-aad7-3701784586b1 - dockerImageTag: 5.0.0 + dockerImageTag: 5.0.1 dockerRepository: airbyte/source-faker documentationUrl: https://docs.airbyte.com/integrations/sources/faker githubIssueLabel: source-faker @@ -42,6 +42,10 @@ data: - products - purchases supportLevel: community + remoteRegistries: + pypi: + enabled: true + packageName: airbyte-source-faker tags: - language:python metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-faker/setup.py b/airbyte-integrations/connectors/source-faker/setup.py index 1a16ba5ea4851..ab39ea2390373 100644 --- a/airbyte-integrations/connectors/source-faker/setup.py +++ b/airbyte-integrations/connectors/source-faker/setup.py @@ -24,4 +24,10 @@ extras_require={ "tests": TEST_REQUIREMENTS, }, + # register console entry points + entry_points={ + "console_scripts": [ + "source-faker=source_faker.run:run", + ], + }, ) diff --git a/airbyte-integrations/connectors/source-faker/source_faker/run.py b/airbyte-integrations/connectors/source-faker/source_faker/run.py new file mode 100644 index 0000000000000..5bf64ce0d7241 --- /dev/null +++ b/airbyte-integrations/connectors/source-faker/source_faker/run.py @@ -0,0 +1,18 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_faker import SourceFaker + + +def run(): + source = SourceFaker() + launch(source, sys.argv[1:]) + + +if __name__ == "__main__": + run() diff --git a/docs/integrations/sources/faker.md b/docs/integrations/sources/faker.md index 8e2feb4f99b1b..e7d8e34878144 100644 --- a/docs/integrations/sources/faker.md +++ b/docs/integrations/sources/faker.md @@ -96,6 +96,7 @@ None! | Version | Date | Pull Request | Subject | |:--------|:-----------|:----------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------| +| 5.0.1 | 2023-01-08 | [34033](https://github.com/airbytehq/airbyte/pull/34033) | Add standard entrypoints for usage with AirbyteLib | | 5.0.0 | 2023-08-08 | [29213](https://github.com/airbytehq/airbyte/pull/29213) | Change all `*id` fields and `products.year` to be integer | | 4.0.0 | 2023-07-19 | [28485](https://github.com/airbytehq/airbyte/pull/28485) | Bump to test publication | | 3.0.2 | 2023-07-07 | [27807](https://github.com/airbytehq/airbyte/pull/28060) | Bump to test publication | From 72b87e6a6ec9f4274c717686eb549878ae6e5b40 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 11 Jan 2024 11:10:14 +0100 Subject: [PATCH 056/574] airbyte-lib: Add uninstall (#34105) --- airbyte-lib/airbyte_lib/executor.py | 12 +++++ airbyte-lib/airbyte_lib/factories.py | 2 +- airbyte-lib/airbyte_lib/source.py | 9 ++++ airbyte-lib/docs/generated/airbyte_lib.html | 35 ++++++++------ .../docs/generated/airbyte_lib/datasets.html | 7 +++ .../docs/generated/airbyte_lib/factories.html | 46 +++++++++++++++++++ .../integration_tests/test_integration.py | 16 +++++++ 7 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 airbyte-lib/docs/generated/airbyte_lib/datasets.html create mode 100644 airbyte-lib/docs/generated/airbyte_lib/factories.html diff --git a/airbyte-lib/airbyte_lib/executor.py b/airbyte-lib/airbyte_lib/executor.py index 0b0815b509314..71dd1897c5fe1 100644 --- a/airbyte-lib/airbyte_lib/executor.py +++ b/airbyte-lib/airbyte_lib/executor.py @@ -38,6 +38,10 @@ def ensure_installation(self): def install(self): pass + @abstractmethod + def uninstall(self): + pass + @contextmanager def _stream_from_subprocess(args: List[str]) -> Generator[Iterable[str], None, None]: @@ -109,6 +113,11 @@ def _run_subprocess_and_raise_on_failure(self, args: List[str]): if result.returncode != 0: raise Exception(f"Install process exited with code {result.returncode}") + def uninstall(self): + venv_name = self._get_venv_name() + if os.path.exists(venv_name): + self._run_subprocess_and_raise_on_failure(["rm", "-rf", venv_name]) + def install(self): venv_name = self._get_venv_name() self._run_subprocess_and_raise_on_failure([sys.executable, "-m", "venv", venv_name]) @@ -187,6 +196,9 @@ def ensure_installation(self): def install(self): raise Exception(f"Connector {self.metadata.name} is not available - cannot install it") + def uninstall(self): + raise Exception(f"Connector {self.metadata.name} is installed manually and not managed by airbyte-lib - please remove it manually") + def execute(self, args: List[str]) -> Iterable[str]: with _stream_from_subprocess([self.metadata.name] + args) as stream: yield from stream diff --git a/airbyte-lib/airbyte_lib/factories.py b/airbyte-lib/airbyte_lib/factories.py index 7722541a03c5d..adb9e6388dd8e 100644 --- a/airbyte-lib/airbyte_lib/factories.py +++ b/airbyte-lib/airbyte_lib/factories.py @@ -19,7 +19,7 @@ def get_connector( pip_url: str | None = None, config: dict[str, Any] | None = None, use_local_install: bool = False, - install_if_missing: bool = False, + install_if_missing: bool = True, ): """ Get a connector by name and version. diff --git a/airbyte-lib/airbyte_lib/source.py b/airbyte-lib/airbyte_lib/source.py index 96528a760d3a9..612dee86f0998 100644 --- a/airbyte-lib/airbyte_lib/source.py +++ b/airbyte-lib/airbyte_lib/source.py @@ -172,8 +172,17 @@ def check(self): raise Exception(f"Connector did not return check status. Last logs: {self._last_log_messages}") def install(self): + """ + Install the connector if it is not yet installed. + """ self.executor.install() + def uninstall(self): + """ + Uninstall the connector if it is installed. This only works if the use_local_install flag wasn't used and installation is managed by airbyte-lib. + """ + self.executor.uninstall() + def _read(self) -> Iterable[AirbyteRecordMessage]: """ Call read on the connector. diff --git a/airbyte-lib/docs/generated/airbyte_lib.html b/airbyte-lib/docs/generated/airbyte_lib.html index 75181e7e55a90..18d441f26ff3b 100644 --- a/airbyte-lib/docs/generated/airbyte_lib.html +++ b/airbyte-lib/docs/generated/airbyte_lib.html @@ -4,7 +4,7 @@
def - get_connector( name: str, version: str = 'latest', config: Optional[Dict[str, Any]] = None, use_local_install: bool = False, install_if_missing: bool = False): + get_connector( name: str, version: str | None = None, pip_url: str | None = None, config: dict[str, typing.Any] | None = None, use_local_install: bool = False, install_if_missing: bool = True):
@@ -16,8 +16,9 @@
Parameters
  • name: connector name
  • -
  • version: connector version - if not provided, the most recent version will be used
  • -
  • config: connector config - if not provided, you need to set it later via the set_config method
  • +
  • version: connector version - if not provided, the currently installed version will be used. If no version is installed, the latest available version will be used. The version can also be set to "latest" to force the use of the latest available version.
  • +
  • pip_url: connector pip URL - if not provided, the pip url will be inferred from the connector name.
  • +
  • config: connector config - if not provided, you need to set it later via the set_config method.
  • use_local_install: whether to use a virtual environment to run the connector. If True, the connector is expected to be available on the path (e.g. installed via pip). If False, the connector will be installed automatically in a virtual environment.
  • install_if_missing: whether to install the connector if it is not available locally. This parameter is ignored if use_local_install is True.
@@ -196,17 +197,6 @@
Parameters
- -
-
- config: Optional[Dict[str, Any]] - - -
- - - -
@@ -309,7 +299,24 @@
Parameters
+

Install the connector if it is not yet installed.

+
+ + +
+
+
+ + def + uninstall(self): + + +
+ +

Uninstall the connector if it is installed. This only works if the use_local_install flag wasn't used and installation is managed by airbyte-lib.

+
+
diff --git a/airbyte-lib/docs/generated/airbyte_lib/datasets.html b/airbyte-lib/docs/generated/airbyte_lib/datasets.html new file mode 100644 index 0000000000000..c0d27ca14eaa0 --- /dev/null +++ b/airbyte-lib/docs/generated/airbyte_lib/datasets.html @@ -0,0 +1,7 @@ + +
+
+ + + + \ No newline at end of file diff --git a/airbyte-lib/docs/generated/airbyte_lib/factories.html b/airbyte-lib/docs/generated/airbyte_lib/factories.html new file mode 100644 index 0000000000000..0a91ab3f27296 --- /dev/null +++ b/airbyte-lib/docs/generated/airbyte_lib/factories.html @@ -0,0 +1,46 @@ + +
+
+
+ + def + get_in_memory_cache(): + + +
+ + + + +
+
+
+ + def + get_connector( name: str, version: str | None = None, pip_url: str | None = None, config: dict[str, typing.Any] | None = None, use_local_install: bool = False, install_if_missing: bool = True): + + +
+ + +

Get a connector by name and version.

+ +
Parameters
+ +
    +
  • name: connector name
  • +
  • version: connector version - if not provided, the currently installed version will be used. If no version is installed, the latest available version will be used. The version can also be set to "latest" to force the use of the latest available version.
  • +
  • pip_url: connector pip URL - if not provided, the pip url will be inferred from the connector name.
  • +
  • config: connector config - if not provided, you need to set it later via the set_config method.
  • +
  • use_local_install: whether to use a virtual environment to run the connector. If True, the connector is expected to be available on the path (e.g. installed via pip). If False, the connector will be installed automatically in a virtual environment.
  • +
  • install_if_missing: whether to install the connector if it is not available locally. This parameter is ignored if use_local_install is True.
  • +
+
+ + +
+
+ + + + \ No newline at end of file diff --git a/airbyte-lib/tests/integration_tests/test_integration.py b/airbyte-lib/tests/integration_tests/test_integration.py index 31de856d8b0ac..02347de6dfa88 100644 --- a/airbyte-lib/tests/integration_tests/test_integration.py +++ b/airbyte-lib/tests/integration_tests/test_integration.py @@ -149,3 +149,19 @@ def test_succeeding_path_connector(): source.check() os.environ["PATH"] = old_path + +def test_install_uninstall(): + source = ab.get_connector("source-test", pip_url="./tests/integration_tests/fixtures/source-test", config={"apiKey": "test"}, install_if_missing=False) + + source.uninstall() + + # assert that the venv is gone + assert not os.path.exists(".venv-source-test") + + # assert that the connector is not available + with pytest.raises(Exception): + source.check() + + source.install() + + source.check() \ No newline at end of file From 1da4dbca2c11ba48b3be78e210f12f458146f02b Mon Sep 17 00:00:00 2001 From: Patrick Nilan Date: Thu, 11 Jan 2024 03:28:15 -0800 Subject: [PATCH 057/574] Source Marketo: Increase test coverage, update QL (#33075) Co-authored-by: pnilan --- .../connectors/source-marketo/metadata.yaml | 2 +- .../source-marketo/unit_tests/test_source.py | 35 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/airbyte-integrations/connectors/source-marketo/metadata.yaml b/airbyte-integrations/connectors/source-marketo/metadata.yaml index be212d905e415..b8b9a21e5952c 100644 --- a/airbyte-integrations/connectors/source-marketo/metadata.yaml +++ b/airbyte-integrations/connectors/source-marketo/metadata.yaml @@ -1,6 +1,6 @@ data: ab_internal: - ql: 400 + ql: 200 sl: 200 allowedHosts: hosts: diff --git a/airbyte-integrations/connectors/source-marketo/unit_tests/test_source.py b/airbyte-integrations/connectors/source-marketo/unit_tests/test_source.py index e19ac926204dd..806f39da100d1 100644 --- a/airbyte-integrations/connectors/source-marketo/unit_tests/test_source.py +++ b/airbyte-integrations/connectors/source-marketo/unit_tests/test_source.py @@ -6,7 +6,7 @@ import os import tracemalloc from functools import partial -from unittest.mock import ANY, Mock, patch +from unittest.mock import ANY, MagicMock, Mock, patch import pendulum import pytest @@ -21,6 +21,7 @@ MarketoExportCreate, MarketoStream, Programs, + Segmentations, SourceMarketo, ) @@ -317,10 +318,40 @@ def test_check_connection(config, requests_mock, status_code, response, is_conne ("2020-08-01", "%Y-%m-%dT%H:%M:%SZ%z", "2020-08-01"), ), ) -def test_normalize_datetime(config, input, format, expected_result): +def test_programs_normalize_datetime(config, input, format, expected_result): stream = Programs(config) assert stream.normalize_datetime(input, format) == expected_result +def test_programs_next_page_token(config): + mock_json = MagicMock() + mock_json.return_value = {"result": [{"test": 'testValue'}]} + mocked_response = MagicMock() + mocked_response.json = mock_json + stream = Programs(config) + result = stream.next_page_token(mocked_response) + assert result == {"offset": 201} + +@pytest.mark.parametrize("input, stream_state, expected_result",[( + {"result": [{"id": "1", "createdAt": "2020-07-01T00:00:00Z+0000", "updatedAt": "2020-07-01T00:00:00Z+0000"}]}, + {"updatedAt": "2020-06-01T00:00:00Z"}, + [{"id": "1", "createdAt": "2020-07-01T00:00:00Z", "updatedAt": "2020-07-01T00:00:00Z"}], + )], +) +def test_programs_parse_response(mocker, config, input, stream_state, expected_result): + response = requests.Response() + mocker.patch.object(response, "json", return_value=input) + stream = Programs(config) + result = stream.parse_response(response, stream_state) + assert list(result) == expected_result + +def test_segmentations_next_page_token(config): + mock_json = MagicMock() + mock_json.return_value = {"result": [{"test": 'testValue'}]} + mocked_response = MagicMock() + mocked_response.json = mock_json + stream = Segmentations(config) + result = stream.next_page_token(mocked_response) + assert result == {"offset": 201} today = pendulum.now() yesterday = pendulum.now().subtract(days=1).strftime("%Y-%m-%dT%H:%M:%SZ") From 0c003033a65e2ec7fb064d37b4db8582b226e398 Mon Sep 17 00:00:00 2001 From: Artem Inzhyyants <36314070+artem1205@users.noreply.github.com> Date: Thu, 11 Jan 2024 12:55:36 +0100 Subject: [PATCH 058/574] =?UTF-8?q?=F0=9F=90=9B=20Source=20Bing=20Ads:=20S?= =?UTF-8?q?peed=20up=20record=20transformation=20(#34045)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration_tests/expected_records.jsonl | 54 +++++++++---------- .../connectors/source-bing-ads/metadata.yaml | 2 +- .../connectors/source-bing-ads/setup.py | 2 +- .../source_bing_ads/report_streams.py | 9 +++- docs/integrations/sources/bing-ads.md | 1 + 5 files changed, 37 insertions(+), 31 deletions(-) diff --git a/airbyte-integrations/connectors/source-bing-ads/integration_tests/expected_records.jsonl b/airbyte-integrations/connectors/source-bing-ads/integration_tests/expected_records.jsonl index a612db5825960..c6373cec4d5b9 100644 --- a/airbyte-integrations/connectors/source-bing-ads/integration_tests/expected_records.jsonl +++ b/airbyte-integrations/connectors/source-bing-ads/integration_tests/expected_records.jsonl @@ -1,29 +1,29 @@ -{"stream": "ad_groups", "data": {"AdRotation": null, "AudienceAdsBidAdjustment": null, "BiddingScheme": {"Type": "InheritFromParent", "InheritedBidStrategyType": "EnhancedCpc"}, "CpcBid": {"Amount": 2.27}, "EndDate": null, "FinalUrlSuffix": null, "ForwardCompatibilityMap": null, "Id": 1356799861840328, "Language": null, "Name": "keywords", "Network": "OwnedAndOperatedAndSyndicatedSearch", "PrivacyStatus": null, "Settings": null, "StartDate": {"Day": 7, "Month": 11, "Year": 2023}, "Status": "Active", "TrackingUrlTemplate": null, "UrlCustomParameters": null, "AdScheduleUseSearcherTimeZone": false, "AdGroupType": "SearchStandard", "CpvBid": {"Amount": null}, "CpmBid": {"Amount": null}, "CampaignId": 531016227, "AccountId": 180519267, "CustomerId": 251186883}, "emitted_at": 1702903270550} -{"stream": "ads", "data": {"AdFormatPreference": "All", "DevicePreference": 0, "EditorialStatus": "Active", "FinalAppUrls": null, "FinalMobileUrls": null, "FinalUrlSuffix": null, "FinalUrls": {"string": ["https://airbyte.com"]}, "ForwardCompatibilityMap": null, "Id": 84800390693061, "Status": "Active", "TrackingUrlTemplate": null, "Type": "ResponsiveSearch", "UrlCustomParameters": null, "Descriptions": {"AssetLink": [{"Asset": {"Id": 10239363892977, "Name": null, "Type": "TextAsset", "Text": "Connect, integrate, and sync data seamlessly with Airbyte's 800+ contributors and growing!"}, "AssetPerformanceLabel": "Learning", "EditorialStatus": "Active", "PinnedField": null}, {"Asset": {"Id": 10239363892976, "Name": null, "Type": "TextAsset", "Text": "Move data like a pro with our powerful tool trusted by 40,000+ engineers worldwide!"}, "AssetPerformanceLabel": "Learning", "EditorialStatus": "Active", "PinnedField": null}]}, "Domain": "airbyte.com", "Headlines": {"AssetLink": [{"Asset": {"Id": 10239363892979, "Name": null, "Type": "TextAsset", "Text": "Get synced with Airbyte"}, "AssetPerformanceLabel": "Good", "EditorialStatus": "Active", "PinnedField": null}, {"Asset": {"Id": 10239363893384, "Name": null, "Type": "TextAsset", "Text": "Data management made easy"}, "AssetPerformanceLabel": "Good", "EditorialStatus": "Active", "PinnedField": null}, {"Asset": {"Id": 10239363892978, "Name": null, "Type": "TextAsset", "Text": "Connectors for every need"}, "AssetPerformanceLabel": "Good", "EditorialStatus": "Active", "PinnedField": null}, {"Asset": {"Id": 10239363892980, "Name": null, "Type": "TextAsset", "Text": "Industry-leading connectors"}, "AssetPerformanceLabel": "Good", "EditorialStatus": "Active", "PinnedField": null}, {"Asset": {"Id": 10239363893383, "Name": null, "Type": "TextAsset", "Text": "Try Airbyte now for free"}, "AssetPerformanceLabel": "Good", "EditorialStatus": "Active", "PinnedField": null}]}, "Path1": null, "Path2": null, "AdGroupId": 1356799861840328, "AccountId": 180519267, "CustomerId": 251186883}, "emitted_at": 1702903282124} +{"stream":"ad_groups","data":{"AdRotation":null,"AudienceAdsBidAdjustment":null,"BiddingScheme":{"Type":"InheritFromParent","InheritedBidStrategyType":"EnhancedCpc"},"CpcBid":{"Amount":2.27},"EndDate":null,"FinalUrlSuffix":null,"ForwardCompatibilityMap":null,"Id":1356799861840328,"Language":null,"Name":"keywords","Network":"OwnedAndOperatedAndSyndicatedSearch","PrivacyStatus":null,"Settings":null,"StartDate":{"Day":7,"Month":11,"Year":2023},"Status":"Active","TrackingUrlTemplate":null,"UrlCustomParameters":null,"AdScheduleUseSearcherTimeZone":false,"AdGroupType":"SearchStandard","CpvBid":{"Amount":null},"CpmBid":{"Amount":null},"CampaignId":531016227,"AccountId":180519267,"CustomerId":251186883},"emitted_at":1704833256596} +{"stream":"ads","data":{"AdFormatPreference":"All","DevicePreference":0,"EditorialStatus":"Active","FinalAppUrls":null,"FinalMobileUrls":null,"FinalUrlSuffix":null,"FinalUrls":{"string":["https://airbyte.com"]},"ForwardCompatibilityMap":null,"Id":84800390693061,"Status":"Active","TrackingUrlTemplate":null,"Type":"ResponsiveSearch","UrlCustomParameters":null,"Descriptions":{"AssetLink":[{"Asset":{"Id":10239363892977,"Name":null,"Type":"TextAsset","Text":"Connect, integrate, and sync data seamlessly with Airbyte's 800+ contributors and growing!"},"AssetPerformanceLabel":"Learning","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363892976,"Name":null,"Type":"TextAsset","Text":"Move data like a pro with our powerful tool trusted by 40,000+ engineers worldwide!"},"AssetPerformanceLabel":"Learning","EditorialStatus":"Active","PinnedField":null}]},"Domain":"airbyte.com","Headlines":{"AssetLink":[{"Asset":{"Id":10239363892979,"Name":null,"Type":"TextAsset","Text":"Get synced with Airbyte"},"AssetPerformanceLabel":"Good","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363893384,"Name":null,"Type":"TextAsset","Text":"Data management made easy"},"AssetPerformanceLabel":"Best","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363892978,"Name":null,"Type":"TextAsset","Text":"Connectors for every need"},"AssetPerformanceLabel":"Low","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363892980,"Name":null,"Type":"TextAsset","Text":"Industry-leading connectors"},"AssetPerformanceLabel":"Low","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363893383,"Name":null,"Type":"TextAsset","Text":"Try Airbyte now for free"},"AssetPerformanceLabel":"Low","EditorialStatus":"Active","PinnedField":null}]},"Path1":null,"Path2":null,"AdGroupId":1356799861840328,"AccountId":180519267,"CustomerId":251186883},"emitted_at":1704833266594} {"stream": "campaigns", "data": {"AudienceAdsBidAdjustment": 0, "BiddingScheme": {"Type": "EnhancedCpc"}, "BudgetType": "DailyBudgetStandard", "DailyBudget": 2.0, "ExperimentId": null, "FinalUrlSuffix": null, "ForwardCompatibilityMap": null, "Id": 531016227, "MultimediaAdsBidAdjustment": 40, "Name": "Airbyte test", "Status": "Active", "SubType": null, "TimeZone": "CentralTimeUSCanada", "TrackingUrlTemplate": null, "UrlCustomParameters": null, "CampaignType": "Search", "Settings": {"Setting": [{"Type": "TargetSetting", "Details": {"TargetSettingDetail": [{"CriterionTypeGroup": "Audience", "TargetAndBid": false}]}}]}, "BudgetId": null, "Languages": {"string": ["English"]}, "AdScheduleUseSearcherTimeZone": false, "AccountId": 180519267, "CustomerId": 251186883}, "emitted_at": 1702903287209} {"stream": "accounts", "data": {"BillToCustomerId": 251186883, "CurrencyCode": "USD", "AccountFinancialStatus": "ClearFinancialStatus", "Id": 180535609, "Language": "English", "LastModifiedByUserId": 0, "LastModifiedTime": "2023-08-11T08:24:26.603000", "Name": "DEMO-ACCOUNT", "Number": "F149W3B6", "ParentCustomerId": 251186883, "PaymentMethodId": null, "PaymentMethodType": null, "PrimaryUserId": 138225488, "AccountLifeCycleStatus": "Pause", "TimeStamp": "AAAAAH10c1A=", "TimeZone": "Santiago", "PauseReason": 2, "ForwardCompatibilityMap": null, "LinkedAgencies": null, "SalesHouseCustomerId": null, "TaxInformation": null, "BackUpPaymentInstrumentId": null, "BillingThresholdAmount": null, "BusinessAddress": {"City": "San Francisco", "CountryCode": "US", "Id": 149694999, "Line1": "350 29th avenue", "Line2": null, "Line3": null, "Line4": null, "PostalCode": "94121", "StateOrProvince": "CA", "TimeStamp": null, "BusinessName": "Daxtarity Inc."}, "AutoTagType": "Inactive", "SoldToPaymentInstrumentId": null, "AccountMode": "Expert"}, "emitted_at": 1702903290287} -{"stream": "account_performance_report_daily", "data": {"AccountId": 180519267, "TimePeriod": "2023-12-18", "CurrencyCode": "USD", "AdDistribution": "Search", "DeviceType": "Computer", "Network": "Microsoft sites and select traffic", "DeliveredMatchType": "Broad", "DeviceOS": "Windows", "TopVsOther": "Microsoft sites and select traffic - other", "BidMatchType": "Broad", "AccountName": "Airbyte", "AccountNumber": "F149MJ18", "PhoneImpressions": 0, "PhoneCalls": 0, "Clicks": 0, "Ctr": null, "Spend": 0.0, "Impressions": 0, "CostPerConversion": null, "Ptr": null, "Assists": 0, "ReturnOnAdSpend": null, "CostPerAssist": null, "AverageCpc": 0.0, "AveragePosition": 0.0, "AverageCpm": 0.0, "Conversions": 0.0, "ConversionsQualified": 0.0, "ConversionRate": null, "LowQualityClicks": 0, "LowQualityClicksPercent": null, "LowQualityImpressions": 1, "LowQualitySophisticatedClicks": 0, "LowQualityConversions": 0, "LowQualityConversionRate": null, "Revenue": 0.0, "RevenuePerConversion": null, "RevenuePerAssist": null}, "emitted_at": 1702903303688} -{"stream": "account_performance_report_weekly", "data": {"AccountId": 180519267, "TimePeriod": "2023-12-17", "CurrencyCode": "USD", "AdDistribution": "Audience", "DeviceType": "Tablet", "Network": "Audience", "DeliveredMatchType": "Exact", "DeviceOS": "Android", "TopVsOther": "Audience network", "BidMatchType": "Broad", "AccountName": "Airbyte", "AccountNumber": "F149MJ18", "PhoneImpressions": 0, "PhoneCalls": 0, "Clicks": 0, "Ctr": 0.0, "Spend": 0.0, "Impressions": 6, "CostPerConversion": null, "Ptr": null, "Assists": 0, "ReturnOnAdSpend": null, "CostPerAssist": null, "AverageCpc": 0.0, "AveragePosition": 0.0, "AverageCpm": 0.0, "Conversions": 0.0, "ConversionsQualified": 0.0, "ConversionRate": null, "LowQualityClicks": 0, "LowQualityClicksPercent": null, "LowQualityImpressions": 2, "LowQualitySophisticatedClicks": 0, "LowQualityConversions": 0, "LowQualityConversionRate": null, "Revenue": 0.0, "RevenuePerConversion": null, "RevenuePerAssist": null}, "emitted_at": 1702903323501} -{"stream": "ad_group_performance_report_daily", "data": {"AccountId": 180519267, "CampaignId": 531016227, "AdGroupId": 1356799861840328, "TimePeriod": "2023-12-18", "CurrencyCode": "USD", "AdDistribution": "Search", "DeviceType": "Computer", "Network": "Microsoft sites and select traffic", "DeliveredMatchType": "Phrase", "DeviceOS": "Windows", "TopVsOther": "Microsoft sites and select traffic - top", "BidMatchType": "Broad", "Language": "English", "AccountName": "Airbyte", "CampaignName": "Airbyte test", "CampaignType": "Search & content", "AdGroupName": "keywords", "AdGroupType": "Standard", "Impressions": 2, "Clicks": 1, "Ctr": 50.0, "Spend": 1.48, "CostPerConversion": null, "QualityScore": 6.0, "ExpectedCtr": "2", "AdRelevance": 3.0, "LandingPageExperience": 2.0, "PhoneImpressions": 0, "PhoneCalls": 0, "Ptr": null, "Assists": 0, "CostPerAssist": null, "CustomParameters": null, "FinalUrlSuffix": null, "ViewThroughConversions": 0, "AllCostPerConversion": null, "AllReturnOnAdSpend": 0.0, "AllConversions": 0, "AllConversionRate": 0.0, "AllRevenue": 0.0, "AllRevenuePerConversion": null, "AverageCpc": 1.48, "AveragePosition": 0.0, "AverageCpm": 740.0, "Conversions": 0.0, "ConversionRate": 0.0, "ConversionsQualified": 0.0, "HistoricalQualityScore": null, "HistoricalExpectedCtr": null, "HistoricalAdRelevance": null, "HistoricalLandingPageExperience": null, "Revenue": 0.0, "RevenuePerConversion": null, "RevenuePerAssist": null}, "emitted_at": 1702903341609} -{"stream": "ad_group_performance_report_weekly", "data": {"AccountId": 180519267, "CampaignId": 531016227, "AdGroupId": 1356799861840328, "TimePeriod": "2023-12-17", "CurrencyCode": "USD", "AdDistribution": "Audience", "DeviceType": "Tablet", "Network": "Audience", "DeliveredMatchType": "Exact", "DeviceOS": "Android", "TopVsOther": "Audience network", "BidMatchType": "Broad", "Language": "English", "AccountName": "Airbyte", "CampaignName": "Airbyte test", "CampaignType": "Search & content", "AdGroupName": "keywords", "AdGroupType": "Standard", "Impressions": 6, "Clicks": 0, "Ctr": 0.0, "Spend": 0.0, "CostPerConversion": null, "QualityScore": 6.0, "ExpectedCtr": "2", "AdRelevance": 3.0, "LandingPageExperience": 2.0, "PhoneImpressions": 0, "PhoneCalls": 0, "Ptr": null, "Assists": 0, "CostPerAssist": null, "CustomParameters": null, "FinalUrlSuffix": null, "ViewThroughConversions": 0, "AllCostPerConversion": null, "AllReturnOnAdSpend": null, "AllConversions": 0, "AllConversionRate": null, "AllRevenue": 0.0, "AllRevenuePerConversion": null, "AverageCpc": 0.0, "AveragePosition": 0.0, "AverageCpm": 0.0, "Conversions": 0.0, "ConversionRate": null, "ConversionsQualified": 0.0, "HistoricalQualityScore": 6.0, "HistoricalExpectedCtr": 2.0, "HistoricalAdRelevance": 3.0, "HistoricalLandingPageExperience": 2.0, "Revenue": 0.0, "RevenuePerConversion": null, "RevenuePerAssist": null}, "emitted_at": 1702903364068} -{"stream": "ad_group_impression_performance_report_daily", "data": {"AccountName": "Airbyte", "AccountNumber": "F149MJ18", "AccountId": 180519267, "TimePeriod": "2023-12-18", "Status": "Active", "CampaignName": "Airbyte test", "CampaignId": 531016227, "AdGroupName": "keywords", "AdGroupId": 1356799861840328, "CurrencyCode": "USD", "AdDistribution": "Search", "Impressions": 2, "Clicks": 1, "Ctr": 50.0, "AverageCpc": 1.48, "Spend": 1.48, "AveragePosition": 0.0, "Conversions": 0, "ConversionRate": 0.0, "CostPerConversion": null, "DeviceType": "Computer", "Language": "English", "ImpressionSharePercent": null, "ImpressionLostToBudgetPercent": null, "ImpressionLostToRankAggPercent": null, "QualityScore": 6, "ExpectedCtr": 2.0, "AdRelevance": 3, "LandingPageExperience": 2, "HistoricalQualityScore": null, "HistoricalExpectedCtr": null, "HistoricalAdRelevance": null, "HistoricalLandingPageExperience": null, "PhoneImpressions": 0, "PhoneCalls": 0, "Ptr": null, "Network": "Microsoft sites and select traffic", "Assists": 0, "Revenue": 0.0, "ReturnOnAdSpend": 0.0, "CostPerAssist": null, "RevenuePerConversion": null, "RevenuePerAssist": null, "TrackingTemplate": null, "CustomParameters": null, "AccountStatus": "Active", "CampaignStatus": "Active", "AdGroupLabels": null, "ExactMatchImpressionSharePercent": null, "ClickSharePercent": null, "AbsoluteTopImpressionSharePercent": null, "FinalUrlSuffix": null, "CampaignType": "Search & content", "TopImpressionShareLostToRankPercent": null, "TopImpressionShareLostToBudgetPercent": null, "AbsoluteTopImpressionShareLostToRankPercent": null, "AbsoluteTopImpressionShareLostToBudgetPercent": null, "TopImpressionSharePercent": null, "AbsoluteTopImpressionRatePercent": 100.0, "TopImpressionRatePercent": 100.0, "BaseCampaignId": 531016227, "AllConversions": 0, "AllRevenue": 0.0, "AllConversionRate": 0.0, "AllCostPerConversion": null, "AllReturnOnAdSpend": 0.0, "AllRevenuePerConversion": null, "ViewThroughConversions": 0, "AudienceImpressionSharePercent": null, "AudienceImpressionLostToRankPercent": null, "AudienceImpressionLostToBudgetPercent": null, "RelativeCtr": null, "AdGroupType": "Standard", "AverageCpm": 740.0, "ConversionsQualified": 0.0, "AllConversionsQualified": 0.0, "ViewThroughConversionsQualified": null, "ViewThroughRevenue": 0.0, "VideoViews": 0, "ViewThroughRate": 0.0, "AverageCPV": null, "VideoViewsAt25Percent": 0, "VideoViewsAt50Percent": 0, "VideoViewsAt75Percent": 0, "CompletedVideoViews": 0, "VideoCompletionRate": null, "TotalWatchTimeInMS": 0, "AverageWatchTimePerVideoView": null, "AverageWatchTimePerImpression": 0.0, "Sales": 0, "CostPerSale": null, "RevenuePerSale": null, "Installs": 0, "CostPerInstall": null, "RevenuePerInstall": null}, "emitted_at": 1702903384097} -{"stream": "ad_group_impression_performance_report_weekly", "data": {"AccountName": "Airbyte", "AccountNumber": "F149MJ18", "AccountId": 180519267, "TimePeriod": "2023-12-17", "Status": "Active", "CampaignName": "Airbyte test", "CampaignId": 531016227, "AdGroupName": "keywords", "AdGroupId": 1356799861840328, "CurrencyCode": "USD", "AdDistribution": "Audience", "Impressions": 6, "Clicks": 0, "Ctr": 0.0, "AverageCpc": 0.0, "Spend": 0.0, "AveragePosition": 0.0, "Conversions": 0, "ConversionRate": null, "CostPerConversion": null, "DeviceType": "Tablet", "Language": "English", "ImpressionSharePercent": null, "ImpressionLostToBudgetPercent": null, "ImpressionLostToRankAggPercent": null, "QualityScore": 6, "ExpectedCtr": 2.0, "AdRelevance": 3, "LandingPageExperience": 2, "HistoricalQualityScore": 6, "HistoricalExpectedCtr": 2, "HistoricalAdRelevance": 3, "HistoricalLandingPageExperience": 2, "PhoneImpressions": 0, "PhoneCalls": 0, "Ptr": null, "Network": "Audience", "Assists": 0, "Revenue": 0.0, "ReturnOnAdSpend": null, "CostPerAssist": null, "RevenuePerConversion": null, "RevenuePerAssist": null, "TrackingTemplate": null, "CustomParameters": null, "AccountStatus": "Active", "CampaignStatus": "Active", "AdGroupLabels": null, "ExactMatchImpressionSharePercent": null, "ClickSharePercent": null, "AbsoluteTopImpressionSharePercent": null, "FinalUrlSuffix": null, "CampaignType": "Search & content", "TopImpressionShareLostToRankPercent": null, "TopImpressionShareLostToBudgetPercent": null, "AbsoluteTopImpressionShareLostToRankPercent": null, "AbsoluteTopImpressionShareLostToBudgetPercent": null, "TopImpressionSharePercent": null, "AbsoluteTopImpressionRatePercent": null, "TopImpressionRatePercent": null, "BaseCampaignId": 531016227, "AllConversions": 0, "AllRevenue": 0.0, "AllConversionRate": null, "AllCostPerConversion": null, "AllReturnOnAdSpend": null, "AllRevenuePerConversion": null, "ViewThroughConversions": 0, "AudienceImpressionSharePercent": null, "AudienceImpressionLostToRankPercent": null, "AudienceImpressionLostToBudgetPercent": null, "RelativeCtr": null, "AdGroupType": "Standard", "AverageCpm": 0.0, "ConversionsQualified": 0.0, "AllConversionsQualified": 0.0, "ViewThroughConversionsQualified": null, "ViewThroughRevenue": 0.0, "VideoViews": 0, "ViewThroughRate": 0.0, "AverageCPV": null, "VideoViewsAt25Percent": 0, "VideoViewsAt50Percent": 0, "VideoViewsAt75Percent": 0, "CompletedVideoViews": 0, "VideoCompletionRate": null, "TotalWatchTimeInMS": 0, "AverageWatchTimePerVideoView": null, "AverageWatchTimePerImpression": 0.0, "Sales": 0, "CostPerSale": null, "RevenuePerSale": null, "Installs": 0, "CostPerInstall": null, "RevenuePerInstall": null}, "emitted_at": 1702903407764} -{"stream": "ad_performance_report_daily", "data": {"AccountId": 180519267, "CampaignId": 531016227, "AdGroupId": 1356799861840328, "AdId": 84800390693061, "TimePeriod": "2023-12-18", "AbsoluteTopImpressionRatePercent": 100.0, "TopImpressionRatePercent": 100.0, "CurrencyCode": "USD", "AdDistribution": "Search", "DeviceType": "Computer", "Language": "English", "Network": "Microsoft sites and select traffic", "DeviceOS": "Windows", "TopVsOther": "Microsoft sites and select traffic - top", "BidMatchType": "Broad", "DeliveredMatchType": "Phrase", "AccountName": "Airbyte", "CampaignName": "Airbyte test", "CampaignType": "Search & content", "AdGroupName": "keywords", "Impressions": 2, "Clicks": 1, "Ctr": 50.0, "Spend": 1.48, "CostPerConversion": null, "DestinationUrl": null, "Assists": 0, "ReturnOnAdSpend": 0.0, "CostPerAssist": null, "CustomParameters": null, "FinalAppUrl": null, "AdDescription": null, "AdDescription2": null, "ViewThroughConversions": 0, "ViewThroughConversionsQualified": null, "AllCostPerConversion": null, "AllReturnOnAdSpend": 0.0, "Conversions": 0.0, "ConversionRate": 0.0, "ConversionsQualified": 0.0, "AverageCpc": 1.48, "AveragePosition": 0.0, "AverageCpm": 740.0, "AllConversions": 0, "AllConversionRate": 0.0, "AllRevenue": 0.0, "AllRevenuePerConversion": null, "Revenue": 0.0, "RevenuePerConversion": null, "RevenuePerAssist": null}, "emitted_at": 1702903426231} -{"stream": "ad_performance_report_weekly", "data": {"AccountId": 180519267, "CampaignId": 531016227, "AdGroupId": 1356799861840328, "AdId": 84800390693061, "TimePeriod": "2023-12-17", "AbsoluteTopImpressionRatePercent": 0.0, "TopImpressionRatePercent": 0.0, "CurrencyCode": "USD", "AdDistribution": "Audience", "DeviceType": "Tablet", "Language": "English", "Network": "Audience", "DeviceOS": "Android", "TopVsOther": "Audience network", "BidMatchType": "Broad", "DeliveredMatchType": "Exact", "AccountName": "Airbyte", "CampaignName": "Airbyte test", "CampaignType": "Search & content", "AdGroupName": "keywords", "Impressions": 6, "Clicks": 0, "Ctr": 0.0, "Spend": 0.0, "CostPerConversion": null, "DestinationUrl": null, "Assists": 0, "ReturnOnAdSpend": null, "CostPerAssist": null, "CustomParameters": null, "FinalAppUrl": null, "AdDescription": null, "AdDescription2": null, "ViewThroughConversions": 0, "ViewThroughConversionsQualified": null, "AllCostPerConversion": null, "AllReturnOnAdSpend": null, "Conversions": 0.0, "ConversionRate": null, "ConversionsQualified": 0.0, "AverageCpc": 0.0, "AveragePosition": 0.0, "AverageCpm": 0.0, "AllConversions": 0, "AllConversionRate": null, "AllRevenue": 0.0, "AllRevenuePerConversion": null, "Revenue": 0.0, "RevenuePerConversion": null, "RevenuePerAssist": null}, "emitted_at": 1702903447755} -{"stream": "budget_summary_report", "data": {"AccountName": "Airbyte", "AccountNumber": "F149MJ18", "AccountId": 180519267, "CampaignId": 531016227, "CampaignName": "Airbyte test", "Date": "2023-12-18", "MonthlyBudget": 60.8, "DailySpend": 1.48, "MonthToDateSpend": 36.0}, "emitted_at": 1702903465284} -{"stream": "campaign_performance_report_daily", "data": {"AccountId": 180519267, "CampaignId": 531016227, "TimePeriod": "2023-12-18", "CurrencyCode": "USD", "AdDistribution": "Search", "DeviceType": "Computer", "Network": "Microsoft sites and select traffic", "DeliveredMatchType": "Broad", "DeviceOS": "Windows", "TopVsOther": "Microsoft sites and select traffic - other", "BidMatchType": "Broad", "AccountName": "Airbyte", "CampaignName": "Airbyte test", "CampaignType": "Search & content", "CampaignStatus": "Active", "CampaignLabels": null, "Impressions": 0, "Clicks": 0, "Ctr": null, "Spend": 0.0, "CostPerConversion": null, "QualityScore": 6.0, "AdRelevance": 3.0, "LandingPageExperience": 2.0, "PhoneImpressions": 0, "PhoneCalls": 0, "Ptr": null, "Assists": 0, "ReturnOnAdSpend": null, "CostPerAssist": null, "CustomParameters": null, "ViewThroughConversions": 0, "AllCostPerConversion": null, "AllReturnOnAdSpend": null, "AllConversions": 0, "ConversionsQualified": 0.0, "AllConversionRate": null, "AllRevenue": 0.0, "AllRevenuePerConversion": null, "AverageCpc": 0.0, "AveragePosition": 0.0, "AverageCpm": 0.0, "Conversions": 0.0, "ConversionRate": null, "LowQualityClicks": 0, "LowQualityClicksPercent": null, "LowQualityImpressions": 1, "LowQualitySophisticatedClicks": 0, "LowQualityConversions": 0, "LowQualityConversionRate": null, "HistoricalQualityScore": null, "HistoricalExpectedCtr": null, "HistoricalAdRelevance": null, "HistoricalLandingPageExperience": null, "Revenue": 0.0, "RevenuePerConversion": null, "RevenuePerAssist": null, "BudgetName": null, "BudgetStatus": null, "BudgetAssociationStatus": "Current"}, "emitted_at": 1702903483803} -{"stream": "campaign_performance_report_weekly", "data": {"AccountId": 180519267, "CampaignId": 531016227, "TimePeriod": "2023-12-17", "CurrencyCode": "USD", "AdDistribution": "Audience", "DeviceType": "Tablet", "Network": "Audience", "DeliveredMatchType": "Exact", "DeviceOS": "Android", "TopVsOther": "Audience network", "BidMatchType": "Broad", "AccountName": "Airbyte", "CampaignName": "Airbyte test", "CampaignType": "Search & content", "CampaignStatus": "Active", "CampaignLabels": null, "Impressions": 6, "Clicks": 0, "Ctr": 0.0, "Spend": 0.0, "CostPerConversion": null, "QualityScore": 6.0, "AdRelevance": 3.0, "LandingPageExperience": 2.0, "PhoneImpressions": 0, "PhoneCalls": 0, "Ptr": null, "Assists": 0, "ReturnOnAdSpend": null, "CostPerAssist": null, "CustomParameters": null, "ViewThroughConversions": 0, "AllCostPerConversion": null, "AllReturnOnAdSpend": null, "AllConversions": 0, "ConversionsQualified": 0.0, "AllConversionRate": null, "AllRevenue": 0.0, "AllRevenuePerConversion": null, "AverageCpc": 0.0, "AveragePosition": 0.0, "AverageCpm": 0.0, "Conversions": 0.0, "ConversionRate": null, "LowQualityClicks": 0, "LowQualityClicksPercent": null, "LowQualityImpressions": 2, "LowQualitySophisticatedClicks": 0, "LowQualityConversions": 0, "LowQualityConversionRate": null, "HistoricalQualityScore": 6.0, "HistoricalExpectedCtr": 2.0, "HistoricalAdRelevance": 3.0, "HistoricalLandingPageExperience": 2.0, "Revenue": 0.0, "RevenuePerConversion": null, "RevenuePerAssist": null, "BudgetName": null, "BudgetStatus": null, "BudgetAssociationStatus": "Current"}, "emitted_at": 1702903504944} -{"stream": "campaign_impression_performance_report_daily", "data": {"AccountName": "Airbyte", "AccountNumber": "F149MJ18", "AccountId": 180519267, "TimePeriod": "2023-12-18", "CampaignStatus": "Active", "CampaignName": "Airbyte test", "CampaignId": 531016227, "CurrencyCode": "USD", "AdDistribution": "Search", "Impressions": 3, "Clicks": 1, "Ctr": 33.33, "AverageCpc": 1.48, "Spend": 1.48, "AveragePosition": 0.0, "Conversions": 0, "ConversionRate": null, "CostPerConversion": null, "LowQualityClicks": 0, "LowQualityClicksPercent": 0.0, "LowQualityImpressions": 1, "LowQualityImpressionsPercent": 25.0, "LowQualityConversions": 0, "LowQualityConversionRate": null, "DeviceType": "Computer", "ImpressionSharePercent": null, "ImpressionLostToBudgetPercent": null, "ImpressionLostToRankAggPercent": null, "QualityScore": 6.0, "ExpectedCtr": "2", "AdRelevance": 3.0, "LandingPageExperience": 2.0, "HistoricalQualityScore": null, "HistoricalExpectedCtr": null, "HistoricalAdRelevance": null, "HistoricalLandingPageExperience": null, "PhoneImpressions": 0, "PhoneCalls": 0, "Ptr": null, "Network": "Microsoft sites and select traffic", "Assists": 0, "Revenue": 0.0, "ReturnOnAdSpend": 0.0, "CostPerAssist": null, "RevenuePerConversion": null, "RevenuePerAssist": null, "TrackingTemplate": null, "CustomParameters": null, "AccountStatus": "Active", "LowQualityGeneralClicks": 0, "LowQualitySophisticatedClicks": 0, "CampaignLabels": null, "ExactMatchImpressionSharePercent": null, "ClickSharePercent": null, "AbsoluteTopImpressionSharePercent": null, "FinalUrlSuffix": null, "CampaignType": "Search & content", "TopImpressionShareLostToRankPercent": null, "TopImpressionShareLostToBudgetPercent": null, "AbsoluteTopImpressionShareLostToRankPercent": null, "AbsoluteTopImpressionShareLostToBudgetPercent": null, "TopImpressionSharePercent": null, "AbsoluteTopImpressionRatePercent": 100.0, "TopImpressionRatePercent": 100.0, "BaseCampaignId": 531016227, "AllConversions": 0, "AllRevenue": 0.0, "AllConversionRate": null, "AllCostPerConversion": null, "AllReturnOnAdSpend": 0.0, "AllRevenuePerConversion": null, "ViewThroughConversions": 0, "AudienceImpressionSharePercent": null, "AudienceImpressionLostToRankPercent": null, "AudienceImpressionLostToBudgetPercent": null, "RelativeCtr": null, "AverageCpm": 493.33, "ConversionsQualified": 0.0, "LowQualityConversionsQualified": 0.0, "AllConversionsQualified": 0.0, "ViewThroughConversionsQualified": null, "ViewThroughRevenue": 0.0, "VideoViews": 0, "ViewThroughRate": 0.0, "AverageCPV": null, "VideoViewsAt25Percent": 0, "VideoViewsAt50Percent": 0, "VideoViewsAt75Percent": 0, "CompletedVideoViews": 0, "VideoCompletionRate": null, "TotalWatchTimeInMS": 0, "AverageWatchTimePerVideoView": null, "AverageWatchTimePerImpression": 0.0, "Sales": 0, "CostPerSale": null, "RevenuePerSale": null, "Installs": 0, "CostPerInstall": null, "RevenuePerInstall": null}, "emitted_at": 1702903526946} -{"stream": "campaign_impression_performance_report_weekly", "data": {"AccountName": "Airbyte", "AccountNumber": "F149MJ18", "AccountId": 180519267, "TimePeriod": "2023-12-17", "CampaignStatus": "Active", "CampaignName": "Airbyte test", "CampaignId": 531016227, "CurrencyCode": "USD", "AdDistribution": "Audience", "Impressions": 6, "Clicks": 0, "Ctr": 0.0, "AverageCpc": 0.0, "Spend": 0.0, "AveragePosition": 0.0, "Conversions": 0, "ConversionRate": null, "CostPerConversion": null, "LowQualityClicks": 0, "LowQualityClicksPercent": null, "LowQualityImpressions": 2, "LowQualityImpressionsPercent": 25.0, "LowQualityConversions": 0, "LowQualityConversionRate": null, "DeviceType": "Tablet", "ImpressionSharePercent": null, "ImpressionLostToBudgetPercent": null, "ImpressionLostToRankAggPercent": null, "QualityScore": 6.0, "ExpectedCtr": "2", "AdRelevance": 3.0, "LandingPageExperience": 2.0, "HistoricalQualityScore": 6, "HistoricalExpectedCtr": 2, "HistoricalAdRelevance": 3, "HistoricalLandingPageExperience": 2, "PhoneImpressions": 0, "PhoneCalls": 0, "Ptr": null, "Network": "Audience", "Assists": 0, "Revenue": 0.0, "ReturnOnAdSpend": null, "CostPerAssist": null, "RevenuePerConversion": null, "RevenuePerAssist": null, "TrackingTemplate": null, "CustomParameters": null, "AccountStatus": "Active", "LowQualityGeneralClicks": 0, "LowQualitySophisticatedClicks": 0, "CampaignLabels": null, "ExactMatchImpressionSharePercent": null, "ClickSharePercent": null, "AbsoluteTopImpressionSharePercent": null, "FinalUrlSuffix": null, "CampaignType": "Search & content", "TopImpressionShareLostToRankPercent": null, "TopImpressionShareLostToBudgetPercent": null, "AbsoluteTopImpressionShareLostToRankPercent": null, "AbsoluteTopImpressionShareLostToBudgetPercent": null, "TopImpressionSharePercent": null, "AbsoluteTopImpressionRatePercent": 0.0, "TopImpressionRatePercent": 0.0, "BaseCampaignId": 531016227, "AllConversions": 0, "AllRevenue": 0.0, "AllConversionRate": null, "AllCostPerConversion": null, "AllReturnOnAdSpend": null, "AllRevenuePerConversion": null, "ViewThroughConversions": 0, "AudienceImpressionSharePercent": null, "AudienceImpressionLostToRankPercent": null, "AudienceImpressionLostToBudgetPercent": null, "RelativeCtr": null, "AverageCpm": 0.0, "ConversionsQualified": 0.0, "LowQualityConversionsQualified": 0.0, "AllConversionsQualified": 0.0, "ViewThroughConversionsQualified": null, "ViewThroughRevenue": 0.0, "VideoViews": 0, "ViewThroughRate": 0.0, "AverageCPV": null, "VideoViewsAt25Percent": 0, "VideoViewsAt50Percent": 0, "VideoViewsAt75Percent": 0, "CompletedVideoViews": 0, "VideoCompletionRate": null, "TotalWatchTimeInMS": 0, "AverageWatchTimePerVideoView": null, "AverageWatchTimePerImpression": 0.0, "Sales": 0, "CostPerSale": null, "RevenuePerSale": null, "Installs": 0, "CostPerInstall": null, "RevenuePerInstall": null}, "emitted_at": 1702903556219} -{"stream": "keyword_performance_report_daily", "data": {"AccountId": 180519267, "CampaignId": 531016227, "AdGroupId": 1356799861840328, "KeywordId": 84801135055370, "Keyword": "Airbyte", "AdId": 84800390693061, "TimePeriod": "2023-12-18", "CurrencyCode": "USD", "DeliveredMatchType": "Phrase", "AdDistribution": "Search", "DeviceType": "Computer", "Language": "English", "Network": "Microsoft sites and select traffic", "DeviceOS": "Windows", "TopVsOther": "Microsoft sites and select traffic - top", "BidMatchType": "Broad", "AccountName": "Airbyte", "CampaignName": "Airbyte test", "AdGroupName": "keywords", "KeywordStatus": "Active", "HistoricalExpectedCtr": null, "HistoricalAdRelevance": null, "HistoricalLandingPageExperience": null, "HistoricalQualityScore": null, "Impressions": 2, "Clicks": 1, "Ctr": 50.0, "CurrentMaxCpc": 2.27, "Spend": 1.48, "CostPerConversion": null, "QualityScore": 10.0, "ExpectedCtr": "3", "AdRelevance": 3.0, "LandingPageExperience": 3.0, "QualityImpact": 0.0, "Assists": 0, "ReturnOnAdSpend": 0.0, "CostPerAssist": null, "CustomParameters": null, "FinalAppUrl": null, "Mainline1Bid": 2.3, "MainlineBid": 0.28, "FirstPageBid": 0.13, "FinalUrlSuffix": null, "ViewThroughConversions": 0, "ViewThroughConversionsQualified": null, "AllCostPerConversion": null, "AllReturnOnAdSpend": 0.0, "Conversions": 0.0, "ConversionRate": 0.0, "ConversionsQualified": 0.0, "AverageCpc": 1.48, "AveragePosition": 0.0, "AverageCpm": 740.0, "AllConversions": 0, "AllConversionRate": 0.0, "AllRevenue": 0.0, "AllRevenuePerConversion": null, "Revenue": 0.0, "RevenuePerConversion": null, "RevenuePerAssist": null}, "emitted_at": 1702903577431} -{"stream": "keyword_performance_report_weekly", "data": {"AccountId": 180519267, "CampaignId": 531016227, "AdGroupId": 1356799861840328, "KeywordId": 84801135055370, "Keyword": "Airbyte", "AdId": 84800390693061, "TimePeriod": "2023-12-17", "CurrencyCode": "USD", "DeliveredMatchType": "Broad", "AdDistribution": "Search", "DeviceType": "Computer", "Language": "Spanish", "Network": "Microsoft sites and select traffic", "DeviceOS": "Windows", "TopVsOther": "Microsoft sites and select traffic - other", "BidMatchType": "Broad", "AccountName": "Airbyte", "CampaignName": "Airbyte test", "AdGroupName": "keywords", "KeywordStatus": "Active", "Impressions": 1, "Clicks": 0, "Ctr": 0.0, "CurrentMaxCpc": 2.27, "Spend": 0.0, "CostPerConversion": null, "QualityScore": 10.0, "ExpectedCtr": "3", "AdRelevance": 3.0, "LandingPageExperience": 3.0, "QualityImpact": 0.0, "Assists": 0, "ReturnOnAdSpend": null, "CostPerAssist": null, "CustomParameters": null, "FinalAppUrl": null, "Mainline1Bid": 2.3, "MainlineBid": 0.28, "FirstPageBid": 0.13, "FinalUrlSuffix": null, "ViewThroughConversions": 0, "ViewThroughConversionsQualified": null, "AllCostPerConversion": null, "AllReturnOnAdSpend": null, "Conversions": 0.0, "ConversionRate": null, "ConversionsQualified": 0.0, "AverageCpc": 0.0, "AveragePosition": 0.0, "AverageCpm": 0.0, "AllConversions": 0, "AllConversionRate": null, "AllRevenue": 0.0, "AllRevenuePerConversion": null, "Revenue": 0.0, "RevenuePerConversion": null, "RevenuePerAssist": null}, "emitted_at": 1702903603645} -{"stream": "geographic_performance_report_daily", "data": {"AccountId": 180519267, "CampaignId": 531016227, "AdGroupId": 1356799861840328, "TimePeriod": "2023-12-18", "AccountNumber": "F149MJ18", "Country": "Singapore", "State": null, "MetroArea": null, "City": null, "ProximityTargetLocation": null, "Radius": "0", "LocationType": "Physical location", "MostSpecificLocation": "Singapore", "AccountStatus": "Active", "CampaignStatus": "Active", "AdGroupStatus": "Active", "County": null, "PostalCode": null, "LocationId": "164", "BaseCampaignId": "531016227", "Goal": null, "GoalType": null, "AbsoluteTopImpressionRatePercent": 100.0, "TopImpressionRatePercent": "100.00", "AllConversionsQualified": "0.00", "Neighborhood": null, "ViewThroughRevenue": "0.00", "CampaignType": "Search & content", "AssetGroupId": null, "AssetGroupName": null, "AssetGroupStatus": null, "CurrencyCode": "USD", "DeliveredMatchType": "Phrase", "AdDistribution": "Search", "DeviceType": "Computer", "Language": "English", "Network": "Microsoft sites and select traffic", "DeviceOS": "Windows", "TopVsOther": "Microsoft sites and select traffic - top", "BidMatchType": "Broad", "AccountName": "Airbyte", "CampaignName": "Airbyte test", "AdGroupName": "keywords", "Impressions": 1, "Clicks": 1, "Ctr": 100.0, "Spend": 1.48, "CostPerConversion": null, "Assists": 0, "ReturnOnAdSpend": 0.0, "CostPerAssist": null, "ViewThroughConversions": 0, "ViewThroughConversionsQualified": null, "AllCostPerConversion": null, "AllReturnOnAdSpend": 0.0, "Conversions": 0.0, "ConversionRate": 0.0, "ConversionsQualified": 0.0, "AverageCpc": 1.48, "AveragePosition": 0.0, "AverageCpm": 1480.0, "AllConversions": 0, "AllConversionRate": 0.0, "AllRevenue": 0.0, "AllRevenuePerConversion": null, "Revenue": 0.0, "RevenuePerConversion": null, "RevenuePerAssist": null}, "emitted_at": 1702903622357} -{"stream": "geographic_performance_report_weekly", "data": {"AccountId": 180519267, "CampaignId": 531016227, "AdGroupId": 1356799861840328, "TimePeriod": "2023-12-17", "AccountNumber": "F149MJ18", "Country": "South Africa", "State": "Gauteng", "MetroArea": "Ekurhuleni", "City": "Springs", "ProximityTargetLocation": null, "Radius": "0", "LocationType": "Physical location", "MostSpecificLocation": "Springs", "AccountStatus": "Active", "CampaignStatus": "Active", "AdGroupStatus": "Active", "County": null, "PostalCode": null, "LocationId": "137889", "BaseCampaignId": "531016227", "Goal": null, "GoalType": null, "AbsoluteTopImpressionRatePercent": 0.0, "TopImpressionRatePercent": "0.00", "AllConversionsQualified": "0.00", "Neighborhood": null, "ViewThroughRevenue": "0.00", "CampaignType": "Search & content", "AssetGroupId": null, "AssetGroupName": null, "AssetGroupStatus": null, "CurrencyCode": "USD", "DeliveredMatchType": "Broad", "AdDistribution": "Search", "DeviceType": "Computer", "Language": "English", "Network": "Microsoft sites and select traffic", "DeviceOS": "Windows", "TopVsOther": "Microsoft sites and select traffic - other", "BidMatchType": "Broad", "AccountName": "Airbyte", "CampaignName": "Airbyte test", "AdGroupName": "keywords", "Impressions": 1, "Clicks": 0, "Ctr": 0.0, "Spend": 0.0, "CostPerConversion": null, "Assists": 0, "ReturnOnAdSpend": null, "CostPerAssist": null, "ViewThroughConversions": 0, "ViewThroughConversionsQualified": null, "AllCostPerConversion": null, "AllReturnOnAdSpend": null, "Conversions": 0.0, "ConversionRate": null, "ConversionsQualified": 0.0, "AverageCpc": 0.0, "AveragePosition": 0.0, "AverageCpm": 0.0, "AllConversions": 0, "AllConversionRate": null, "AllRevenue": 0.0, "AllRevenuePerConversion": null, "Revenue": 0.0, "RevenuePerConversion": null, "RevenuePerAssist": null}, "emitted_at": 1702903731352} -{"stream": "age_gender_audience_report_daily", "data": {"AccountId": 180519267, "AgeGroup": "50-64", "Gender": "Male", "TimePeriod": "2023-12-18", "AllConversions": 0, "AccountName": "Airbyte", "AccountNumber": "F149MJ18", "CampaignName": "Airbyte test", "CampaignId": 531016227, "AdGroupName": "keywords", "AdGroupId": 1356799861840328, "AdDistribution": "Search", "Impressions": 1, "Clicks": 1, "Conversions": 0.0, "Spend": 1.48, "Revenue": 0.0, "ExtendedCost": 0.0, "Assists": 0, "Language": "English", "AccountStatus": "Active", "CampaignStatus": "Active", "AdGroupStatus": "Active", "BaseCampaignId": "531016227", "AllRevenue": 0.0, "ViewThroughConversions": 0, "Goal": null, "GoalType": null, "AbsoluteTopImpressionRatePercent": 100.0, "TopImpressionRatePercent": 100.0, "ConversionsQualified": 0.0, "AllConversionsQualified": 0.0, "ViewThroughConversionsQualified": null, "ViewThroughRevenue": 0.0}, "emitted_at": 1702903750352} -{"stream": "age_gender_audience_report_weekly", "data": {"AccountId": 180519267, "AgeGroup": "65+", "Gender": "Female", "TimePeriod": "2023-12-17", "AllConversions": 0, "AccountName": "Airbyte", "AccountNumber": "F149MJ18", "CampaignName": "Airbyte test", "CampaignId": 531016227, "AdGroupName": "keywords", "AdGroupId": 1356799861840328, "AdDistribution": "Audience", "Impressions": 3, "Clicks": 0, "Conversions": 0.0, "Spend": 0.0, "Revenue": 0.0, "ExtendedCost": 0.0, "Assists": 0, "Language": "English", "AccountStatus": "Active", "CampaignStatus": "Active", "AdGroupStatus": "Active", "BaseCampaignId": "531016227", "AllRevenue": 0.0, "ViewThroughConversions": 0, "Goal": null, "GoalType": null, "AbsoluteTopImpressionRatePercent": 0.0, "TopImpressionRatePercent": 0.0, "ConversionsQualified": 0.0, "AllConversionsQualified": 0.0, "ViewThroughConversionsQualified": null, "ViewThroughRevenue": 0.0}, "emitted_at": 1702903769451} -{"stream": "search_query_performance_report_daily", "data": {"AccountName": "Airbyte", "AccountNumber": "F149MJ18", "AccountId": 180519267, "TimePeriod": "2023-12-18", "CampaignName": "Airbyte test", "CampaignId": 531016227, "AdGroupName": "keywords", "AdGroupId": 1356799861840328, "AdId": 84800390693061, "AdType": "Responsive search ad", "DestinationUrl": null, "BidMatchType": "Broad", "DeliveredMatchType": "Phrase", "CampaignStatus": "Active", "AdStatus": "Active", "Impressions": 1, "Clicks": 1, "Ctr": 100.0, "AverageCpc": 1.48, "Spend": 1.48, "AveragePosition": 0.0, "SearchQuery": "airbyte connectors", "Keyword": "Airbyte", "AdGroupCriterionId": null, "Conversions": 0, "ConversionRate": 0.0, "CostPerConversion": null, "Language": "English", "KeywordId": 84801135055370, "Network": "Microsoft sites and select traffic", "TopVsOther": "Microsoft sites and select traffic - top", "DeviceType": "Computer", "DeviceOS": "Windows", "Assists": 0, "Revenue": 0.0, "ReturnOnAdSpend": 0.0, "CostPerAssist": null, "RevenuePerConversion": null, "RevenuePerAssist": null, "AccountStatus": "Active", "AdGroupStatus": "Active", "KeywordStatus": "Active", "CampaignType": "Search & content", "CustomerId": 251186883, "CustomerName": "Daxtarity Inc.", "AllConversions": 0, "AllRevenue": 0.0, "AllConversionRate": 0.0, "AllCostPerConversion": null, "AllReturnOnAdSpend": 0.0, "AllRevenuePerConversion": null, "Goal": null, "GoalType": null, "AbsoluteTopImpressionRatePercent": 100.0, "TopImpressionRatePercent": 100.0, "AverageCpm": 1480.0, "ConversionsQualified": 0.0, "AllConversionsQualified": 0.0}, "emitted_at": 1702903787003} -{"stream": "search_query_performance_report_weekly", "data": {"AccountName": "Airbyte", "AccountNumber": "F149MJ18", "AccountId": 180519267, "TimePeriod": "2023-12-17", "CampaignName": "Airbyte test", "CampaignId": 531016227, "AdGroupName": "keywords", "AdGroupId": 1356799861840328, "AdId": 84800390693061, "AdType": "Responsive search ad", "DestinationUrl": null, "BidMatchType": "Broad", "DeliveredMatchType": "Broad", "CampaignStatus": "Active", "AdStatus": "Active", "Impressions": 1, "Clicks": 1, "Ctr": 100.0, "AverageCpc": 0.15, "Spend": 0.15, "AveragePosition": 0.0, "SearchQuery": "open source alternative", "Keyword": "ELT infrastructure", "AdGroupCriterionId": null, "Conversions": 0, "ConversionRate": 0.0, "CostPerConversion": null, "Language": "English", "KeywordId": 84801135055369, "Network": "Microsoft sites and select traffic", "TopVsOther": "Microsoft sites and select traffic - other", "DeviceType": "Computer", "DeviceOS": "Windows", "Assists": 0, "Revenue": 0.0, "ReturnOnAdSpend": 0.0, "CostPerAssist": null, "RevenuePerConversion": null, "RevenuePerAssist": null, "AccountStatus": "Active", "AdGroupStatus": "Active", "KeywordStatus": "Active", "CampaignType": "Search & content", "CustomerId": 251186883, "CustomerName": "Daxtarity Inc.", "AllConversions": 0, "AllRevenue": 0.0, "AllConversionRate": 0.0, "AllCostPerConversion": null, "AllReturnOnAdSpend": 0.0, "AllRevenuePerConversion": null, "Goal": null, "GoalType": null, "AbsoluteTopImpressionRatePercent": 0.0, "TopImpressionRatePercent": 0.0, "AverageCpm": 150.0, "ConversionsQualified": 0.0, "AllConversionsQualified": 0.0}, "emitted_at": 1702903806073} -{"stream": "user_location_performance_report_daily", "data": {"AccountName": "Airbyte", "AccountNumber": "F149MJ18", "AccountId": 180519267, "TimePeriod": "2023-12-18", "CampaignName": "Airbyte test", "CampaignId": 531016227, "AdGroupName": "keywords", "AdGroupId": 1356799861840328, "Country": "Singapore", "State": null, "MetroArea": null, "CurrencyCode": "USD", "AdDistribution": "Search", "Impressions": 1, "Clicks": 1, "Ctr": 100.0, "AverageCpc": 1.48, "Spend": 1.48, "AveragePosition": 0.0, "ProximityTargetLocation": null, "Radius": 0, "Language": "English", "City": null, "QueryIntentCountry": "Australia", "QueryIntentState": null, "QueryIntentCity": null, "QueryIntentDMA": null, "BidMatchType": "Broad", "DeliveredMatchType": "Phrase", "Network": "Microsoft sites and select traffic", "TopVsOther": "Microsoft sites and select traffic - top", "DeviceType": "Computer", "DeviceOS": "Windows", "Assists": 0, "Conversions": 0, "ConversionRate": 0.0, "Revenue": 0.0, "ReturnOnAdSpend": 0.0, "CostPerConversion": null, "CostPerAssist": null, "RevenuePerConversion": null, "RevenuePerAssist": null, "County": null, "PostalCode": null, "QueryIntentCounty": null, "QueryIntentPostalCode": null, "LocationId": 164, "QueryIntentLocationId": 9, "AllConversions": 0, "AllRevenue": 0.0, "AllConversionRate": 0.0, "AllCostPerConversion": null, "AllReturnOnAdSpend": 0.0, "AllRevenuePerConversion": null, "ViewThroughConversions": 0, "Goal": null, "GoalType": null, "AbsoluteTopImpressionRatePercent": 100.0, "TopImpressionRatePercent": 100.0, "AverageCpm": 1480.0, "ConversionsQualified": 0.0, "AllConversionsQualified": 0.0, "ViewThroughConversionsQualified": null, "Neighborhood": null, "QueryIntentNeighborhood": null, "ViewThroughRevenue": 0.0, "CampaignType": "Search & content", "AssetGroupId": null, "AssetGroupName": null}, "emitted_at": 1702903827074} -{"stream": "user_location_performance_report_weekly", "data": {"AccountName": "Airbyte", "AccountNumber": "F149MJ18", "AccountId": 180519267, "TimePeriod": "2023-12-17", "CampaignName": "Airbyte test", "CampaignId": 531016227, "AdGroupName": "keywords", "AdGroupId": 1356799861840328, "Country": "South Africa", "State": "Gauteng", "MetroArea": "Ekurhuleni", "CurrencyCode": "USD", "AdDistribution": "Search", "Impressions": 1, "Clicks": 0, "Ctr": 0.0, "AverageCpc": 0.0, "Spend": 0.0, "AveragePosition": 0.0, "ProximityTargetLocation": null, "Radius": 0, "Language": "English", "City": "Springs", "QueryIntentCountry": "United States", "QueryIntentState": null, "QueryIntentCity": null, "QueryIntentDMA": null, "BidMatchType": "Broad", "DeliveredMatchType": "Broad", "Network": "Microsoft sites and select traffic", "TopVsOther": "Microsoft sites and select traffic - other", "DeviceType": "Computer", "DeviceOS": "Windows", "Assists": 0, "Conversions": 0, "ConversionRate": null, "Revenue": 0.0, "ReturnOnAdSpend": null, "CostPerConversion": null, "CostPerAssist": null, "RevenuePerConversion": null, "RevenuePerAssist": null, "County": null, "PostalCode": null, "QueryIntentCounty": null, "QueryIntentPostalCode": null, "LocationId": 137889, "QueryIntentLocationId": 190, "AllConversions": 0, "AllRevenue": 0.0, "AllConversionRate": null, "AllCostPerConversion": null, "AllReturnOnAdSpend": null, "AllRevenuePerConversion": null, "ViewThroughConversions": 0, "Goal": null, "GoalType": null, "AbsoluteTopImpressionRatePercent": 0.0, "TopImpressionRatePercent": 0.0, "AverageCpm": 0.0, "ConversionsQualified": 0.0, "AllConversionsQualified": 0.0, "ViewThroughConversionsQualified": null, "Neighborhood": null, "QueryIntentNeighborhood": null, "ViewThroughRevenue": 0.0, "CampaignType": "Search & content", "AssetGroupId": null, "AssetGroupName": null}, "emitted_at": 1702903937707} -{"stream": "account_impression_performance_report_daily", "data": {"AccountName": "Airbyte", "AccountNumber": "F149MJ18", "AccountId": 180519267, "TimePeriod": "2023-12-18", "CurrencyCode": "USD", "AdDistribution": "Search", "Impressions": 3, "Clicks": 1, "Ctr": 33.33, "AverageCpc": 1.48, "Spend": 1.48, "AveragePosition": 0.0, "Conversions": 0, "ConversionRate": 0.0, "CostPerConversion": null, "LowQualityClicks": 0, "LowQualityClicksPercent": 0.0, "LowQualityImpressions": 1, "LowQualityImpressionsPercent": 25.0, "LowQualityConversions": 0, "LowQualityConversionRate": null, "DeviceType": "Computer", "ImpressionSharePercent": null, "ImpressionLostToBudgetPercent": null, "ImpressionLostToRankAggPercent": null, "PhoneImpressions": 0, "PhoneCalls": 0, "Ptr": null, "Network": "Microsoft sites and select traffic", "Assists": 0, "Revenue": 0.0, "ReturnOnAdSpend": 0.0, "CostPerAssist": null, "RevenuePerConversion": null, "RevenuePerAssist": null, "AccountStatus": "Active", "LowQualityGeneralClicks": 0, "LowQualitySophisticatedClicks": 0, "ExactMatchImpressionSharePercent": null, "ClickSharePercent": null, "AbsoluteTopImpressionSharePercent": null, "TopImpressionShareLostToRankPercent": null, "TopImpressionShareLostToBudgetPercent": null, "AbsoluteTopImpressionShareLostToRankPercent": null, "AbsoluteTopImpressionShareLostToBudgetPercent": null, "TopImpressionSharePercent": null, "AbsoluteTopImpressionRatePercent": 100.0, "TopImpressionRatePercent": 100.0, "AllConversions": 0, "AllRevenue": 0.0, "AllConversionRate": 0.0, "AllCostPerConversion": null, "AllReturnOnAdSpend": 0.0, "AllRevenuePerConversion": null, "ViewThroughConversions": 0, "AudienceImpressionSharePercent": null, "AudienceImpressionLostToRankPercent": null, "AudienceImpressionLostToBudgetPercent": null, "AverageCpm": 493.33, "ConversionsQualified": 0.0, "LowQualityConversionsQualified": 0.0, "AllConversionsQualified": 0.0, "ViewThroughConversionsQualified": null, "ViewThroughRevenue": 0.0, "VideoViews": 0, "ViewThroughRate": 0.0, "AverageCPV": null, "VideoViewsAt25Percent": 0, "VideoViewsAt50Percent": 0, "VideoViewsAt75Percent": 0, "CompletedVideoViews": 0, "VideoCompletionRate": null, "TotalWatchTimeInMS": 0, "AverageWatchTimePerVideoView": null, "AverageWatchTimePerImpression": 0.0, "Sales": 0, "CostPerSale": null, "RevenuePerSale": null, "Installs": 0, "CostPerInstall": null, "RevenuePerInstall": null}, "emitted_at": 1702903957244} -{"stream": "account_impression_performance_report_weekly", "data": {"AccountName": "Airbyte", "AccountNumber": "F149MJ18", "AccountId": 180519267, "TimePeriod": "2023-12-17", "CurrencyCode": "USD", "AdDistribution": "Audience", "Impressions": 6, "Clicks": 0, "Ctr": 0.0, "AverageCpc": 0.0, "Spend": 0.0, "AveragePosition": 0.0, "Conversions": 0, "ConversionRate": null, "CostPerConversion": null, "LowQualityClicks": 0, "LowQualityClicksPercent": null, "LowQualityImpressions": 2, "LowQualityImpressionsPercent": 25.0, "LowQualityConversions": 0, "LowQualityConversionRate": null, "DeviceType": "Tablet", "ImpressionSharePercent": null, "ImpressionLostToBudgetPercent": null, "ImpressionLostToRankAggPercent": null, "PhoneImpressions": 0, "PhoneCalls": 0, "Ptr": null, "Network": "Audience", "Assists": 0, "Revenue": 0.0, "ReturnOnAdSpend": null, "CostPerAssist": null, "RevenuePerConversion": null, "RevenuePerAssist": null, "AccountStatus": "Active", "LowQualityGeneralClicks": 0, "LowQualitySophisticatedClicks": 0, "ExactMatchImpressionSharePercent": null, "ClickSharePercent": null, "AbsoluteTopImpressionSharePercent": null, "TopImpressionShareLostToRankPercent": null, "TopImpressionShareLostToBudgetPercent": null, "AbsoluteTopImpressionShareLostToRankPercent": null, "AbsoluteTopImpressionShareLostToBudgetPercent": null, "TopImpressionSharePercent": null, "AbsoluteTopImpressionRatePercent": null, "TopImpressionRatePercent": null, "AllConversions": 0, "AllRevenue": 0.0, "AllConversionRate": null, "AllCostPerConversion": null, "AllReturnOnAdSpend": null, "AllRevenuePerConversion": null, "ViewThroughConversions": 0, "AudienceImpressionSharePercent": null, "AudienceImpressionLostToRankPercent": null, "AudienceImpressionLostToBudgetPercent": null, "AverageCpm": 0.0, "ConversionsQualified": 0.0, "LowQualityConversionsQualified": 0.0, "AllConversionsQualified": 0.0, "ViewThroughConversionsQualified": null, "ViewThroughRevenue": 0.0, "VideoViews": 0, "ViewThroughRate": 0.0, "AverageCPV": null, "VideoViewsAt25Percent": 0, "VideoViewsAt50Percent": 0, "VideoViewsAt75Percent": 0, "CompletedVideoViews": 0, "VideoCompletionRate": null, "TotalWatchTimeInMS": 0, "AverageWatchTimePerVideoView": null, "AverageWatchTimePerImpression": 0.0, "Sales": 0, "CostPerSale": null, "RevenuePerSale": null, "Installs": 0, "CostPerInstall": null, "RevenuePerInstall": null}, "emitted_at": 1702903981854} +{"stream":"account_performance_report_daily","data":{"AccountId":180519267,"TimePeriod":"2023-12-18","CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Network":"Syndicated search partners","DeliveredMatchType":"Exact","DeviceOS":"Windows","TopVsOther":"Syndicated search partners - Top","BidMatchType":"Broad","AccountName":"Airbyte","AccountNumber":"F149MJ18","PhoneImpressions":0,"PhoneCalls":0,"Clicks":0,"Ctr":0.0,"Spend":0.0,"Impressions":1,"CostPerConversion":null,"Ptr":null,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"Conversions":0.0,"ConversionsQualified":0.0,"ConversionRate":null,"LowQualityClicks":0,"LowQualityClicksPercent":null,"LowQualityImpressions":0,"LowQualitySophisticatedClicks":0,"LowQualityConversions":0,"LowQualityConversionRate":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1704833285214} +{"stream":"account_performance_report_weekly","data":{"AccountId":180519267,"TimePeriod":"2023-12-17","CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Network":"Syndicated search partners","DeliveredMatchType":"Exact","DeviceOS":"Unknown","TopVsOther":"Syndicated search partners - Top","BidMatchType":"Broad","AccountName":"Airbyte","AccountNumber":"F149MJ18","PhoneImpressions":0,"PhoneCalls":0,"Clicks":0,"Ctr":0.0,"Spend":0.0,"Impressions":5,"CostPerConversion":null,"Ptr":null,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"Conversions":0.0,"ConversionsQualified":0.0,"ConversionRate":null,"LowQualityClicks":0,"LowQualityClicksPercent":null,"LowQualityImpressions":4,"LowQualitySophisticatedClicks":0,"LowQualityConversions":0,"LowQualityConversionRate":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1704833307364} +{"stream":"ad_group_performance_report_daily","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"TimePeriod":"2023-12-18","CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Network":"Microsoft sites and select traffic","DeliveredMatchType":"Exact","DeviceOS":"Windows","TopVsOther":"Microsoft sites and select traffic - top","BidMatchType":"Broad","Language":"Portuguese","AccountName":"Airbyte","CampaignName":"Airbyte test","CampaignType":"Search & content","AdGroupName":"keywords","AdGroupType":"Standard","Impressions":2,"Clicks":1,"Ctr":50.0,"Spend":0.01,"CostPerConversion":null,"QualityScore":7.0,"ExpectedCtr":"2","AdRelevance":3.0,"LandingPageExperience":2.0,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Assists":0,"CostPerAssist":null,"CustomParameters":null,"FinalUrlSuffix":null,"ViewThroughConversions":0,"AllCostPerConversion":null,"AllReturnOnAdSpend":0.0,"AllConversions":0,"AllConversionRate":0.0,"AllRevenue":0.0,"AllRevenuePerConversion":null,"AverageCpc":0.01,"AveragePosition":0.0,"AverageCpm":5.0,"Conversions":0.0,"ConversionRate":0.0,"ConversionsQualified":0.0,"HistoricalQualityScore":6.0,"HistoricalExpectedCtr":2.0,"HistoricalAdRelevance":3.0,"HistoricalLandingPageExperience":2.0,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1704884363801} +{"stream":"ad_group_performance_report_weekly","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"TimePeriod":"2023-12-17","CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Network":"Syndicated search partners","DeliveredMatchType":"Exact","DeviceOS":"Unknown","TopVsOther":"Syndicated search partners - Top","BidMatchType":"Broad","Language":"German","AccountName":"Airbyte","CampaignName":"Airbyte test","CampaignType":"Search & content","AdGroupName":"keywords","AdGroupType":"Standard","Impressions":1,"Clicks":0,"Ctr":0.0,"Spend":0.0,"CostPerConversion":null,"QualityScore":7.0,"ExpectedCtr":"2","AdRelevance":3.0,"LandingPageExperience":2.0,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Assists":0,"CostPerAssist":null,"CustomParameters":null,"FinalUrlSuffix":null,"ViewThroughConversions":0,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"HistoricalQualityScore":6.0,"HistoricalExpectedCtr":2.0,"HistoricalAdRelevance":3.0,"HistoricalLandingPageExperience":2.0,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1704833349472} +{"stream":"ad_group_impression_performance_report_daily","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-12-18","Status":"Active","CampaignName":"Airbyte test","CampaignId":531016227,"AdGroupName":"keywords","AdGroupId":1356799861840328,"CurrencyCode":"USD","AdDistribution":"Search","Impressions":1,"Clicks":0,"Ctr":0.0,"AverageCpc":0.0,"Spend":0.0,"AveragePosition":0.0,"Conversions":0,"ConversionRate":null,"CostPerConversion":null,"DeviceType":"Computer","Language":"Czech","ImpressionSharePercent":null,"ImpressionLostToBudgetPercent":null,"ImpressionLostToRankAggPercent":null,"QualityScore":7,"ExpectedCtr":2.0,"AdRelevance":3,"LandingPageExperience":2,"HistoricalQualityScore":6,"HistoricalExpectedCtr":2,"HistoricalAdRelevance":3,"HistoricalLandingPageExperience":2,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Network":"Microsoft sites and select traffic","Assists":0,"Revenue":0.0,"ReturnOnAdSpend":null,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"TrackingTemplate":null,"CustomParameters":null,"AccountStatus":"Active","CampaignStatus":"Active","AdGroupLabels":null,"ExactMatchImpressionSharePercent":null,"ClickSharePercent":null,"AbsoluteTopImpressionSharePercent":null,"FinalUrlSuffix":null,"CampaignType":"Search & content","TopImpressionShareLostToRankPercent":null,"TopImpressionShareLostToBudgetPercent":null,"AbsoluteTopImpressionShareLostToRankPercent":null,"AbsoluteTopImpressionShareLostToBudgetPercent":null,"TopImpressionSharePercent":null,"AbsoluteTopImpressionRatePercent":100.0,"TopImpressionRatePercent":100.0,"BaseCampaignId":531016227,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"AudienceImpressionSharePercent":null,"AudienceImpressionLostToRankPercent":null,"AudienceImpressionLostToBudgetPercent":null,"RelativeCtr":null,"AdGroupType":"Standard","AverageCpm":0.0,"ConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0,"VideoViews":0,"ViewThroughRate":0.0,"AverageCPV":null,"VideoViewsAt25Percent":0,"VideoViewsAt50Percent":0,"VideoViewsAt75Percent":0,"CompletedVideoViews":0,"VideoCompletionRate":0.0,"TotalWatchTimeInMS":0,"AverageWatchTimePerVideoView":null,"AverageWatchTimePerImpression":0.0,"Sales":0,"CostPerSale":null,"RevenuePerSale":null,"Installs":0,"CostPerInstall":null,"RevenuePerInstall":null},"emitted_at":1704833929228} +{"stream":"ad_group_impression_performance_report_weekly","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-12-17","Status":"Active","CampaignName":"Airbyte test","CampaignId":531016227,"AdGroupName":"keywords","AdGroupId":1356799861840328,"CurrencyCode":"USD","AdDistribution":"Search","Impressions":3,"Clicks":0,"Ctr":0.0,"AverageCpc":0.0,"Spend":0.0,"AveragePosition":0.0,"Conversions":0,"ConversionRate":null,"CostPerConversion":null,"DeviceType":"Computer","Language":"Bulgarian","ImpressionSharePercent":13.64,"ImpressionLostToBudgetPercent":9.09,"ImpressionLostToRankAggPercent":77.27,"QualityScore":7,"ExpectedCtr":2.0,"AdRelevance":3,"LandingPageExperience":2,"HistoricalQualityScore":6,"HistoricalExpectedCtr":2,"HistoricalAdRelevance":3,"HistoricalLandingPageExperience":2,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Network":"Microsoft sites and select traffic","Assists":0,"Revenue":0.0,"ReturnOnAdSpend":null,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"TrackingTemplate":null,"CustomParameters":null,"AccountStatus":"Active","CampaignStatus":"Active","AdGroupLabels":null,"ExactMatchImpressionSharePercent":null,"ClickSharePercent":null,"AbsoluteTopImpressionSharePercent":null,"FinalUrlSuffix":null,"CampaignType":"Search & content","TopImpressionShareLostToRankPercent":null,"TopImpressionShareLostToBudgetPercent":null,"AbsoluteTopImpressionShareLostToRankPercent":null,"AbsoluteTopImpressionShareLostToBudgetPercent":null,"TopImpressionSharePercent":null,"AbsoluteTopImpressionRatePercent":100.0,"TopImpressionRatePercent":100.0,"BaseCampaignId":531016227,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"AudienceImpressionSharePercent":null,"AudienceImpressionLostToRankPercent":null,"AudienceImpressionLostToBudgetPercent":null,"RelativeCtr":null,"AdGroupType":"Standard","AverageCpm":0.0,"ConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0,"VideoViews":0,"ViewThroughRate":0.0,"AverageCPV":null,"VideoViewsAt25Percent":0,"VideoViewsAt50Percent":0,"VideoViewsAt75Percent":0,"CompletedVideoViews":0,"VideoCompletionRate":0.0,"TotalWatchTimeInMS":0,"AverageWatchTimePerVideoView":null,"AverageWatchTimePerImpression":0.0,"Sales":0,"CostPerSale":null,"RevenuePerSale":null,"Installs":0,"CostPerInstall":null,"RevenuePerInstall":null},"emitted_at":1704833951765} +{"stream":"ad_performance_report_daily","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"AdId":84800390693061,"TimePeriod":"2023-12-18","AbsoluteTopImpressionRatePercent":100.0,"TopImpressionRatePercent":100.0,"CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Language":"Czech","Network":"Microsoft sites and select traffic","DeviceOS":"Windows","TopVsOther":"Microsoft sites and select traffic - top","BidMatchType":"Broad","DeliveredMatchType":"Phrase","AccountName":"Airbyte","CampaignName":"Airbyte test","CampaignType":"Search & content","AdGroupName":"keywords","Impressions":1,"Clicks":0,"Ctr":0.0,"Spend":0.0,"CostPerConversion":null,"DestinationUrl":null,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"CustomParameters":null,"FinalAppUrl":null,"AdDescription":null,"AdDescription2":null,"ViewThroughConversions":0,"ViewThroughConversionsQualified":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1704833373752} +{"stream":"ad_performance_report_weekly","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"AdId":84800390693061,"TimePeriod":"2023-12-17","AbsoluteTopImpressionRatePercent":100.0,"TopImpressionRatePercent":100.0,"CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Language":"Bulgarian","Network":"Microsoft sites and select traffic","DeviceOS":"Windows","TopVsOther":"Microsoft sites and select traffic - top","BidMatchType":"Broad","DeliveredMatchType":"Phrase","AccountName":"Airbyte","CampaignName":"Airbyte test","CampaignType":"Search & content","AdGroupName":"keywords","Impressions":3,"Clicks":0,"Ctr":0.0,"Spend":0.0,"CostPerConversion":null,"DestinationUrl":null,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"CustomParameters":null,"FinalAppUrl":null,"AdDescription":null,"AdDescription2":null,"ViewThroughConversions":0,"ViewThroughConversionsQualified":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1704833394112} +{"stream":"budget_summary_report","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"CampaignId":531016227,"CampaignName":"Airbyte test","Date":"2023-12-18","MonthlyBudget":60.8,"DailySpend":2.06,"MonthToDateSpend":36.58},"emitted_at":1704833526694} +{"stream":"campaign_performance_report_daily","data":{"AccountId":180519267,"CampaignId":531016227,"TimePeriod":"2023-12-18","CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Network":"Syndicated search partners","DeliveredMatchType":"Exact","DeviceOS":"Windows","TopVsOther":"Syndicated search partners - Top","BidMatchType":"Broad","AccountName":"Airbyte","CampaignName":"Airbyte test","CampaignType":"Search & content","CampaignStatus":"Active","CampaignLabels":null,"Impressions":1,"Clicks":0,"Ctr":0.0,"Spend":0.0,"CostPerConversion":null,"QualityScore":7.0,"AdRelevance":3.0,"LandingPageExperience":2.0,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"CustomParameters":null,"ViewThroughConversions":0,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"AllConversions":0,"ConversionsQualified":0.0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"Conversions":0.0,"ConversionRate":null,"LowQualityClicks":0,"LowQualityClicksPercent":null,"LowQualityImpressions":0,"LowQualitySophisticatedClicks":0,"LowQualityConversions":0,"LowQualityConversionRate":null,"HistoricalQualityScore":6.0,"HistoricalExpectedCtr":2.0,"HistoricalAdRelevance":3.0,"HistoricalLandingPageExperience":2.0,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null,"BudgetName":null,"BudgetStatus":null,"BudgetAssociationStatus":"Current"},"emitted_at":1704833545467} +{"stream":"campaign_performance_report_weekly","data":{"AccountId":180519267,"CampaignId":531016227,"TimePeriod":"2023-12-17","CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Network":"Syndicated search partners","DeliveredMatchType":"Exact","DeviceOS":"Unknown","TopVsOther":"Syndicated search partners - Top","BidMatchType":"Broad","AccountName":"Airbyte","CampaignName":"Airbyte test","CampaignType":"Search & content","CampaignStatus":"Active","CampaignLabels":null,"Impressions":5,"Clicks":0,"Ctr":0.0,"Spend":0.0,"CostPerConversion":null,"QualityScore":7.0,"AdRelevance":3.0,"LandingPageExperience":2.0,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"CustomParameters":null,"ViewThroughConversions":0,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"AllConversions":0,"ConversionsQualified":0.0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"Conversions":0.0,"ConversionRate":null,"LowQualityClicks":0,"LowQualityClicksPercent":null,"LowQualityImpressions":4,"LowQualitySophisticatedClicks":0,"LowQualityConversions":0,"LowQualityConversionRate":null,"HistoricalQualityScore":6.0,"HistoricalExpectedCtr":2.0,"HistoricalAdRelevance":3.0,"HistoricalLandingPageExperience":2.0,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null,"BudgetName":null,"BudgetStatus":null,"BudgetAssociationStatus":"Current"},"emitted_at":1704833565296} +{"stream":"campaign_impression_performance_report_daily","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-12-18","CampaignStatus":"Active","CampaignName":"Airbyte test","CampaignId":531016227,"CurrencyCode":"USD","AdDistribution":"Search","Impressions":22,"Clicks":0,"Ctr":0.0,"AverageCpc":0.0,"Spend":0.0,"AveragePosition":0.0,"Conversions":0,"ConversionRate":null,"CostPerConversion":null,"LowQualityClicks":0,"LowQualityClicksPercent":null,"LowQualityImpressions":6,"LowQualityImpressionsPercent":21.43,"LowQualityConversions":0,"LowQualityConversionRate":null,"DeviceType":"Computer","ImpressionSharePercent":34.92,"ImpressionLostToBudgetPercent":1.59,"ImpressionLostToRankAggPercent":63.49,"QualityScore":7.0,"ExpectedCtr":"2","AdRelevance":3.0,"LandingPageExperience":2.0,"HistoricalQualityScore":6,"HistoricalExpectedCtr":2,"HistoricalAdRelevance":3,"HistoricalLandingPageExperience":2,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Network":"Syndicated search partners","Assists":0,"Revenue":0.0,"ReturnOnAdSpend":null,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"TrackingTemplate":null,"CustomParameters":null,"AccountStatus":"Active","LowQualityGeneralClicks":0,"LowQualitySophisticatedClicks":0,"CampaignLabels":null,"ExactMatchImpressionSharePercent":5.26,"ClickSharePercent":null,"AbsoluteTopImpressionSharePercent":10.2,"FinalUrlSuffix":null,"CampaignType":"Search & content","TopImpressionShareLostToRankPercent":68.0,"TopImpressionShareLostToBudgetPercent":0.0,"AbsoluteTopImpressionShareLostToRankPercent":89.8,"AbsoluteTopImpressionShareLostToBudgetPercent":0.0,"TopImpressionSharePercent":32.0,"AbsoluteTopImpressionRatePercent":22.73,"TopImpressionRatePercent":72.73,"BaseCampaignId":531016227,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"AudienceImpressionSharePercent":null,"AudienceImpressionLostToRankPercent":null,"AudienceImpressionLostToBudgetPercent":null,"RelativeCtr":null,"AverageCpm":0.0,"ConversionsQualified":0.0,"LowQualityConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0,"VideoViews":0,"ViewThroughRate":0.0,"AverageCPV":null,"VideoViewsAt25Percent":0,"VideoViewsAt50Percent":0,"VideoViewsAt75Percent":0,"CompletedVideoViews":0,"VideoCompletionRate":0.0,"TotalWatchTimeInMS":0,"AverageWatchTimePerVideoView":null,"AverageWatchTimePerImpression":0.0,"Sales":0,"CostPerSale":null,"RevenuePerSale":null,"Installs":0,"CostPerInstall":null,"RevenuePerInstall":null},"emitted_at":1704833589146} +{"stream":"campaign_impression_performance_report_weekly","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-12-17","CampaignStatus":"Active","CampaignName":"Airbyte test","CampaignId":531016227,"CurrencyCode":"USD","AdDistribution":"Search","Impressions":639,"Clicks":14,"Ctr":2.19,"AverageCpc":0.12,"Spend":1.74,"AveragePosition":0.0,"Conversions":0,"ConversionRate":null,"CostPerConversion":null,"LowQualityClicks":6,"LowQualityClicksPercent":30.0,"LowQualityImpressions":53,"LowQualityImpressionsPercent":7.66,"LowQualityConversions":0,"LowQualityConversionRate":0.0,"DeviceType":"Computer","ImpressionSharePercent":13.57,"ImpressionLostToBudgetPercent":17.96,"ImpressionLostToRankAggPercent":68.47,"QualityScore":7.0,"ExpectedCtr":"2","AdRelevance":3.0,"LandingPageExperience":2.0,"HistoricalQualityScore":6,"HistoricalExpectedCtr":2,"HistoricalAdRelevance":3,"HistoricalLandingPageExperience":2,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Network":"Syndicated search partners","Assists":0,"Revenue":0.0,"ReturnOnAdSpend":0.0,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"TrackingTemplate":null,"CustomParameters":null,"AccountStatus":"Active","LowQualityGeneralClicks":0,"LowQualitySophisticatedClicks":6,"CampaignLabels":null,"ExactMatchImpressionSharePercent":17.65,"ClickSharePercent":1.28,"AbsoluteTopImpressionSharePercent":3.2,"FinalUrlSuffix":null,"CampaignType":"Search & content","TopImpressionShareLostToRankPercent":74.15,"TopImpressionShareLostToBudgetPercent":18.25,"AbsoluteTopImpressionShareLostToRankPercent":78.51,"AbsoluteTopImpressionShareLostToBudgetPercent":18.29,"TopImpressionSharePercent":7.6,"AbsoluteTopImpressionRatePercent":22.69,"TopImpressionRatePercent":53.99,"BaseCampaignId":531016227,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":0.0,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"AudienceImpressionSharePercent":null,"AudienceImpressionLostToRankPercent":null,"AudienceImpressionLostToBudgetPercent":null,"RelativeCtr":null,"AverageCpm":2.72,"ConversionsQualified":0.0,"LowQualityConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0,"VideoViews":0,"ViewThroughRate":0.0,"AverageCPV":null,"VideoViewsAt25Percent":0,"VideoViewsAt50Percent":0,"VideoViewsAt75Percent":0,"CompletedVideoViews":0,"VideoCompletionRate":0.0,"TotalWatchTimeInMS":0,"AverageWatchTimePerVideoView":null,"AverageWatchTimePerImpression":0.0,"Sales":0,"CostPerSale":null,"RevenuePerSale":null,"Installs":0,"CostPerInstall":null,"RevenuePerInstall":null},"emitted_at":1704833610948} +{"stream":"keyword_performance_report_daily","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"KeywordId":84801135055365,"Keyword":"connector","AdId":84800390693061,"TimePeriod":"2023-12-18","CurrencyCode":"USD","DeliveredMatchType":"Exact","AdDistribution":"Audience","DeviceType":"Computer","Language":"English","Network":"Audience","DeviceOS":"Unknown","TopVsOther":"Audience network","BidMatchType":"Broad","AccountName":"Airbyte","CampaignName":"Airbyte test","AdGroupName":"keywords","KeywordStatus":"Active","HistoricalExpectedCtr":2.0,"HistoricalAdRelevance":3.0,"HistoricalLandingPageExperience":1.0,"HistoricalQualityScore":5.0,"Impressions":6,"Clicks":0,"Ctr":0.0,"CurrentMaxCpc":2.27,"Spend":0.0,"CostPerConversion":null,"QualityScore":5.0,"ExpectedCtr":"2","AdRelevance":3.0,"LandingPageExperience":1.0,"QualityImpact":0.0,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"CustomParameters":null,"FinalAppUrl":null,"Mainline1Bid":null,"MainlineBid":0.66,"FirstPageBid":0.3,"FinalUrlSuffix":null,"ViewThroughConversions":0,"ViewThroughConversionsQualified":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1704833634746} +{"stream":"keyword_performance_report_weekly","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"KeywordId":84801135055365,"Keyword":"connector","AdId":84800390693061,"TimePeriod":"2023-12-17","CurrencyCode":"USD","DeliveredMatchType":"Exact","AdDistribution":"Search","DeviceType":"Computer","Language":"Spanish","Network":"Microsoft sites and select traffic","DeviceOS":"Windows","TopVsOther":"Microsoft sites and select traffic - top","BidMatchType":"Broad","AccountName":"Airbyte","CampaignName":"Airbyte test","AdGroupName":"keywords","KeywordStatus":"Active","Impressions":1,"Clicks":0,"Ctr":0.0,"CurrentMaxCpc":2.27,"Spend":0.0,"CostPerConversion":null,"QualityScore":5.0,"ExpectedCtr":"2","AdRelevance":3.0,"LandingPageExperience":1.0,"QualityImpact":0.0,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"CustomParameters":null,"FinalAppUrl":null,"Mainline1Bid":null,"MainlineBid":0.66,"FirstPageBid":0.3,"FinalUrlSuffix":null,"ViewThroughConversions":0,"ViewThroughConversionsQualified":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1704833656374} +{"stream":"geographic_performance_report_daily","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"TimePeriod":"2023-12-18","AccountNumber":"F149MJ18","Country":"Argentina","State":null,"MetroArea":null,"City":null,"ProximityTargetLocation":null,"Radius":"0","LocationType":"Physical location","MostSpecificLocation":"Argentina","AccountStatus":"Active","CampaignStatus":"Active","AdGroupStatus":"Active","County":null,"PostalCode":null,"LocationId":"8","BaseCampaignId":"531016227","Goal":null,"GoalType":null,"AbsoluteTopImpressionRatePercent":33.33,"TopImpressionRatePercent":"100.00","AllConversionsQualified":"0.00","Neighborhood":null,"ViewThroughRevenue":"0.00","CampaignType":"Search & content","AssetGroupId":null,"AssetGroupName":null,"AssetGroupStatus":null,"CurrencyCode":"USD","DeliveredMatchType":"Phrase","AdDistribution":"Search","DeviceType":"Computer","Language":"Spanish","Network":"Syndicated search partners","DeviceOS":"Unknown","TopVsOther":"Syndicated search partners - Top","BidMatchType":"Broad","AccountName":"Airbyte","CampaignName":"Airbyte test","AdGroupName":"keywords","Impressions":3,"Clicks":0,"Ctr":0.0,"Spend":0.0,"CostPerConversion":null,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"ViewThroughConversions":0,"ViewThroughConversionsQualified":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1704833416620} +{"stream":"geographic_performance_report_weekly","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"TimePeriod":"2023-12-17","AccountNumber":"F149MJ18","Country":"United Arab Emirates","State":"Dubai","MetroArea":null,"City":"Dubai","ProximityTargetLocation":null,"Radius":"0","LocationType":"Physical location","MostSpecificLocation":"Dubai","AccountStatus":"Active","CampaignStatus":"Active","AdGroupStatus":"Active","County":null,"PostalCode":null,"LocationId":"154645","BaseCampaignId":"531016227","Goal":null,"GoalType":null,"AbsoluteTopImpressionRatePercent":0.0,"TopImpressionRatePercent":"0.00","AllConversionsQualified":"0.00","Neighborhood":null,"ViewThroughRevenue":"0.00","CampaignType":"Search & content","AssetGroupId":null,"AssetGroupName":null,"AssetGroupStatus":null,"CurrencyCode":"USD","DeliveredMatchType":"Exact","AdDistribution":"Audience","DeviceType":"Smartphone","Language":"English","Network":"Audience","DeviceOS":"Android","TopVsOther":"Audience network","BidMatchType":"Broad","AccountName":"Airbyte","CampaignName":"Airbyte test","AdGroupName":"keywords","Impressions":1,"Clicks":0,"Ctr":0.0,"Spend":0.0,"CostPerConversion":null,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"ViewThroughConversions":0,"ViewThroughConversionsQualified":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1704833479492} +{"stream":"age_gender_audience_report_daily","data":{"AccountId":180519267,"AgeGroup":"Unknown","Gender":"Unknown","TimePeriod":"2023-12-18","AllConversions":0,"AccountName":"Airbyte","AccountNumber":"F149MJ18","CampaignName":"Airbyte test","CampaignId":531016227,"AdGroupName":"keywords","AdGroupId":1356799861840328,"AdDistribution":"Search","Impressions":1,"Clicks":0,"Conversions":0.0,"Spend":0.0,"Revenue":0.0,"ExtendedCost":0.0,"Assists":0,"Language":"Czech","AccountStatus":"Active","CampaignStatus":"Active","AdGroupStatus":"Active","BaseCampaignId":"531016227","AllRevenue":0.0,"ViewThroughConversions":0,"Goal":null,"GoalType":null,"AbsoluteTopImpressionRatePercent":100.0,"TopImpressionRatePercent":100.0,"ConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0},"emitted_at":1704833673872} +{"stream":"age_gender_audience_report_weekly","data":{"AccountId":180519267,"AgeGroup":"Unknown","Gender":"Unknown","TimePeriod":"2023-12-17","AllConversions":0,"AccountName":"Airbyte","AccountNumber":"F149MJ18","CampaignName":"Airbyte test","CampaignId":531016227,"AdGroupName":"keywords","AdGroupId":1356799861840328,"AdDistribution":"Search","Impressions":1,"Clicks":0,"Conversions":0.0,"Spend":0.0,"Revenue":0.0,"ExtendedCost":0.0,"Assists":0,"Language":"Bulgarian","AccountStatus":"Active","CampaignStatus":"Active","AdGroupStatus":"Active","BaseCampaignId":"531016227","AllRevenue":0.0,"ViewThroughConversions":0,"Goal":null,"GoalType":null,"AbsoluteTopImpressionRatePercent":100.0,"TopImpressionRatePercent":100.0,"ConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0},"emitted_at":1704833693674} +{"stream":"search_query_performance_report_daily","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-12-18","CampaignName":"Airbyte test","CampaignId":531016227,"AdGroupName":"keywords","AdGroupId":1356799861840328,"AdId":84800390693061,"AdType":"Responsive search ad","DestinationUrl":null,"BidMatchType":"Broad","DeliveredMatchType":"Exact","CampaignStatus":"Active","AdStatus":"Active","Impressions":1,"Clicks":0,"Ctr":0.0,"AverageCpc":0.0,"Spend":0.0,"AveragePosition":0.0,"SearchQuery":"airbyte","Keyword":"Airbyte","AdGroupCriterionId":null,"Conversions":0,"ConversionRate":null,"CostPerConversion":null,"Language":"English","KeywordId":84801135055370,"Network":"Microsoft sites and select traffic","TopVsOther":"Microsoft sites and select traffic - top","DeviceType":"Computer","DeviceOS":"Windows","Assists":0,"Revenue":0.0,"ReturnOnAdSpend":null,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"AccountStatus":"Active","AdGroupStatus":"Active","KeywordStatus":"Active","CampaignType":"Search & content","CustomerId":251186883,"CustomerName":"Daxtarity Inc.","AllConversions":0,"AllRevenue":0.0,"AllConversionRate":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"AllRevenuePerConversion":null,"Goal":null,"GoalType":null,"AbsoluteTopImpressionRatePercent":100.0,"TopImpressionRatePercent":100.0,"AverageCpm":0.0,"ConversionsQualified":0.0,"AllConversionsQualified":0.0},"emitted_at":1704833715419} +{"stream":"search_query_performance_report_weekly","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-12-17","CampaignName":"Airbyte test","CampaignId":531016227,"AdGroupName":"keywords","AdGroupId":1356799861840328,"AdId":84800390693061,"AdType":"Responsive search ad","DestinationUrl":null,"BidMatchType":"Broad","DeliveredMatchType":"Exact","CampaignStatus":"Active","AdStatus":"Active","Impressions":1,"Clicks":1,"Ctr":100.0,"AverageCpc":0.04,"Spend":0.04,"AveragePosition":0.0,"SearchQuery":"airbyte","Keyword":"Airbyte","AdGroupCriterionId":null,"Conversions":0,"ConversionRate":0.0,"CostPerConversion":null,"Language":"Czech","KeywordId":84801135055370,"Network":"Microsoft sites and select traffic","TopVsOther":"Microsoft sites and select traffic - top","DeviceType":"Computer","DeviceOS":"Unknown","Assists":0,"Revenue":0.0,"ReturnOnAdSpend":0.0,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"AccountStatus":"Active","AdGroupStatus":"Active","KeywordStatus":"Active","CampaignType":"Search & content","CustomerId":251186883,"CustomerName":"Daxtarity Inc.","AllConversions":0,"AllRevenue":0.0,"AllConversionRate":0.0,"AllCostPerConversion":null,"AllReturnOnAdSpend":0.0,"AllRevenuePerConversion":null,"Goal":null,"GoalType":null,"AbsoluteTopImpressionRatePercent":100.0,"TopImpressionRatePercent":100.0,"AverageCpm":40.0,"ConversionsQualified":0.0,"AllConversionsQualified":0.0},"emitted_at":1704833737157} +{"stream":"user_location_performance_report_daily","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-12-18","CampaignName":"Airbyte test","CampaignId":531016227,"AdGroupName":"keywords","AdGroupId":1356799861840328,"Country":"Argentina","State":null,"MetroArea":null,"CurrencyCode":"USD","AdDistribution":"Search","Impressions":3,"Clicks":0,"Ctr":0.0,"AverageCpc":0.0,"Spend":0.0,"AveragePosition":0.0,"ProximityTargetLocation":null,"Radius":0,"Language":"Spanish","City":null,"QueryIntentCountry":"Argentina","QueryIntentState":null,"QueryIntentCity":null,"QueryIntentDMA":null,"BidMatchType":"Broad","DeliveredMatchType":"Phrase","Network":"Syndicated search partners","TopVsOther":"Syndicated search partners - Top","DeviceType":"Computer","DeviceOS":"Unknown","Assists":0,"Conversions":0,"ConversionRate":null,"Revenue":0.0,"ReturnOnAdSpend":null,"CostPerConversion":null,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"County":null,"PostalCode":null,"QueryIntentCounty":null,"QueryIntentPostalCode":null,"LocationId":8,"QueryIntentLocationId":8,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"Goal":null,"GoalType":null,"AbsoluteTopImpressionRatePercent":33.33,"TopImpressionRatePercent":100.0,"AverageCpm":0.0,"ConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"Neighborhood":null,"QueryIntentNeighborhood":null,"ViewThroughRevenue":0.0,"CampaignType":"Search & content","AssetGroupId":null,"AssetGroupName":null},"emitted_at":1704833762092} +{"stream":"user_location_performance_report_weekly","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-12-17","CampaignName":"Airbyte test","CampaignId":531016227,"AdGroupName":"keywords","AdGroupId":1356799861840328,"Country":"United Arab Emirates","State":"Dubai","MetroArea":null,"CurrencyCode":"USD","AdDistribution":"Audience","Impressions":1,"Clicks":0,"Ctr":0.0,"AverageCpc":0.0,"Spend":0.0,"AveragePosition":0.0,"ProximityTargetLocation":null,"Radius":0,"Language":"English","City":"Dubai","QueryIntentCountry":"United Arab Emirates","QueryIntentState":null,"QueryIntentCity":null,"QueryIntentDMA":null,"BidMatchType":"Broad","DeliveredMatchType":"Exact","Network":"Audience","TopVsOther":"Audience network","DeviceType":"Smartphone","DeviceOS":"Android","Assists":0,"Conversions":0,"ConversionRate":null,"Revenue":0.0,"ReturnOnAdSpend":null,"CostPerConversion":null,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"County":null,"PostalCode":null,"QueryIntentCounty":null,"QueryIntentPostalCode":null,"LocationId":154645,"QueryIntentLocationId":218,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"Goal":null,"GoalType":null,"AbsoluteTopImpressionRatePercent":0.0,"TopImpressionRatePercent":0.0,"AverageCpm":0.0,"ConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"Neighborhood":null,"QueryIntentNeighborhood":null,"ViewThroughRevenue":0.0,"CampaignType":"Search & content","AssetGroupId":null,"AssetGroupName":null},"emitted_at":1704833830043} +{"stream":"account_impression_performance_report_daily","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-12-18","CurrencyCode":"USD","AdDistribution":"Search","Impressions":22,"Clicks":0,"Ctr":0.0,"AverageCpc":0.0,"Spend":0.0,"AveragePosition":0.0,"Conversions":0,"ConversionRate":null,"CostPerConversion":null,"LowQualityClicks":0,"LowQualityClicksPercent":null,"LowQualityImpressions":6,"LowQualityImpressionsPercent":21.43,"LowQualityConversions":0,"LowQualityConversionRate":null,"DeviceType":"Computer","ImpressionSharePercent":34.92,"ImpressionLostToBudgetPercent":1.59,"ImpressionLostToRankAggPercent":63.49,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Network":"Syndicated search partners","Assists":0,"Revenue":0.0,"ReturnOnAdSpend":null,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"AccountStatus":"Active","LowQualityGeneralClicks":0,"LowQualitySophisticatedClicks":0,"ExactMatchImpressionSharePercent":5.26,"ClickSharePercent":null,"AbsoluteTopImpressionSharePercent":10.2,"TopImpressionShareLostToRankPercent":68.0,"TopImpressionShareLostToBudgetPercent":0.0,"AbsoluteTopImpressionShareLostToRankPercent":89.8,"AbsoluteTopImpressionShareLostToBudgetPercent":0.0,"TopImpressionSharePercent":32.0,"AbsoluteTopImpressionRatePercent":22.73,"TopImpressionRatePercent":72.73,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"AudienceImpressionSharePercent":null,"AudienceImpressionLostToRankPercent":null,"AudienceImpressionLostToBudgetPercent":null,"AverageCpm":0.0,"ConversionsQualified":0.0,"LowQualityConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0,"VideoViews":0,"ViewThroughRate":0.0,"AverageCPV":null,"VideoViewsAt25Percent":0,"VideoViewsAt50Percent":0,"VideoViewsAt75Percent":0,"CompletedVideoViews":0,"VideoCompletionRate":0.0,"TotalWatchTimeInMS":0,"AverageWatchTimePerVideoView":null,"AverageWatchTimePerImpression":0.0,"Sales":0,"CostPerSale":null,"RevenuePerSale":null,"Installs":0,"CostPerInstall":null,"RevenuePerInstall":null},"emitted_at":1704833886551} +{"stream":"account_impression_performance_report_weekly","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-12-17","CurrencyCode":"USD","AdDistribution":"Search","Impressions":639,"Clicks":14,"Ctr":2.19,"AverageCpc":0.12,"Spend":1.74,"AveragePosition":0.0,"Conversions":0,"ConversionRate":0.0,"CostPerConversion":null,"LowQualityClicks":6,"LowQualityClicksPercent":30.0,"LowQualityImpressions":53,"LowQualityImpressionsPercent":7.66,"LowQualityConversions":0,"LowQualityConversionRate":0.0,"DeviceType":"Computer","ImpressionSharePercent":13.57,"ImpressionLostToBudgetPercent":17.96,"ImpressionLostToRankAggPercent":68.47,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Network":"Syndicated search partners","Assists":0,"Revenue":0.0,"ReturnOnAdSpend":0.0,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"AccountStatus":"Active","LowQualityGeneralClicks":0,"LowQualitySophisticatedClicks":6,"ExactMatchImpressionSharePercent":17.65,"ClickSharePercent":1.28,"AbsoluteTopImpressionSharePercent":3.2,"TopImpressionShareLostToRankPercent":74.15,"TopImpressionShareLostToBudgetPercent":18.25,"AbsoluteTopImpressionShareLostToRankPercent":78.51,"AbsoluteTopImpressionShareLostToBudgetPercent":18.29,"TopImpressionSharePercent":7.6,"AbsoluteTopImpressionRatePercent":22.69,"TopImpressionRatePercent":53.99,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":0.0,"AllCostPerConversion":null,"AllReturnOnAdSpend":0.0,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"AudienceImpressionSharePercent":null,"AudienceImpressionLostToRankPercent":null,"AudienceImpressionLostToBudgetPercent":null,"AverageCpm":2.72,"ConversionsQualified":0.0,"LowQualityConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0,"VideoViews":0,"ViewThroughRate":0.0,"AverageCPV":null,"VideoViewsAt25Percent":0,"VideoViewsAt50Percent":0,"VideoViewsAt75Percent":0,"CompletedVideoViews":0,"VideoCompletionRate":0.0,"TotalWatchTimeInMS":0,"AverageWatchTimePerVideoView":null,"AverageWatchTimePerImpression":0.0,"Sales":0,"CostPerSale":null,"RevenuePerSale":null,"Installs":0,"CostPerInstall":null,"RevenuePerInstall":null},"emitted_at":1704833908003} diff --git a/airbyte-integrations/connectors/source-bing-ads/metadata.yaml b/airbyte-integrations/connectors/source-bing-ads/metadata.yaml index f077bc36b21dd..5420384149a1c 100644 --- a/airbyte-integrations/connectors/source-bing-ads/metadata.yaml +++ b/airbyte-integrations/connectors/source-bing-ads/metadata.yaml @@ -16,7 +16,7 @@ data: connectorSubtype: api connectorType: source definitionId: 47f25999-dd5e-4636-8c39-e7cea2453331 - dockerImageTag: 2.1.1 + dockerImageTag: 2.1.2 dockerRepository: airbyte/source-bing-ads documentationUrl: https://docs.airbyte.com/integrations/sources/bing-ads githubIssueLabel: source-bing-ads diff --git a/airbyte-integrations/connectors/source-bing-ads/setup.py b/airbyte-integrations/connectors/source-bing-ads/setup.py index 5c326b2d9aad6..e586d0ea27ae2 100644 --- a/airbyte-integrations/connectors/source-bing-ads/setup.py +++ b/airbyte-integrations/connectors/source-bing-ads/setup.py @@ -5,7 +5,7 @@ from setuptools import find_packages, setup -MAIN_REQUIREMENTS = ["airbyte-cdk", "bingads~=13.0.17", "urllib3<2.0", "pandas"] +MAIN_REQUIREMENTS = ["airbyte-cdk", "cached_property~=1.5", "bingads~=13.0.17", "urllib3<2.0", "pandas"] TEST_REQUIREMENTS = [ "freezegun", diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/report_streams.py b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/report_streams.py index dea9a5cb3c0d0..fafb43779e17c 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/report_streams.py +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/report_streams.py @@ -6,7 +6,7 @@ import xml.etree.ElementTree as ET from abc import ABC, abstractmethod from datetime import datetime -from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Union +from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Set, Tuple, Union from urllib.parse import urlparse import _csv @@ -19,6 +19,7 @@ from bingads.v13.internal.reporting.row_report import _RowReport from bingads.v13.internal.reporting.row_report_iterator import _RowReportRecord from bingads.v13.reporting import ReportingDownloadParameters +from cached_property import cached_property from source_bing_ads.base_streams import Accounts, BingAdsStream from source_bing_ads.utils import transform_date_format_to_rfc_3339, transform_report_hourly_datetime_format_to_rfc_3339 from suds import WebFault, sudsobject @@ -103,10 +104,14 @@ def get_column_value(self, row: _RowReportRecord, column: str) -> Union[str, Non return None if "%" in value: value = value.replace("%", "") - if value and set(self.get_json_schema()["properties"].get(column, {}).get("type")) & {"integer", "number"}: + if value and column in self._get_schema_numeric_properties: value = value.replace(",", "") return value + @cached_property + def _get_schema_numeric_properties(self) -> Set[str]: + return set(k for k, v in self.get_json_schema()["properties"].items() if set(v.get("type")) & {"integer", "number"}) + def get_request_date(self, reporting_service: ServiceClient, date: datetime) -> sudsobject.Object: """ Creates XML Date object based on datetime. diff --git a/docs/integrations/sources/bing-ads.md b/docs/integrations/sources/bing-ads.md index c1b653a9c00a2..fe74163adf144 100644 --- a/docs/integrations/sources/bing-ads.md +++ b/docs/integrations/sources/bing-ads.md @@ -226,6 +226,7 @@ The Bing Ads API limits the number of requests for all Microsoft Advertising cli | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------| +| 2.1.2 | 2024-01-09 | [34045](https://github.com/airbytehq/airbyte/pull/34045) | Speed up record transformation | | 2.1.1 | 2023-12-15 | [33500](https://github.com/airbytehq/airbyte/pull/33500) | Fix state setter when state was provided | | 2.1.0 | 2023-12-05 | [33095](https://github.com/airbytehq/airbyte/pull/33095) | Add account filtering | | 2.0.1 | 2023-11-16 | [32597](https://github.com/airbytehq/airbyte/pull/32597) | Fix start date parsing from stream state | From a3834dfefbae2e5adde8c0707c16903cf3ce9c56 Mon Sep 17 00:00:00 2001 From: Artem Inzhyyants <36314070+artem1205@users.noreply.github.com> Date: Thu, 11 Jan 2024 14:05:53 +0100 Subject: [PATCH 059/574] =?UTF-8?q?=F0=9F=90=9B=20Source=20Zendesk=20Suppo?= =?UTF-8?q?rt:=20Skip=20504=20Error=20for=20stream=20`Ticket=20Audits`=20(?= =?UTF-8?q?#34064)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration_tests/expected_records.jsonl | 6 +++--- .../source-zendesk-support/metadata.yaml | 4 ++-- .../source_zendesk_support/streams.py | 17 +++++++++++++++++ .../unit_tests/unit_test.py | 10 ++++++++++ docs/integrations/sources/zendesk-support.md | 3 ++- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/airbyte-integrations/connectors/source-zendesk-support/integration_tests/expected_records.jsonl b/airbyte-integrations/connectors/source-zendesk-support/integration_tests/expected_records.jsonl index d3c3a9ae08e7b..4123754c3b144 100644 --- a/airbyte-integrations/connectors/source-zendesk-support/integration_tests/expected_records.jsonl +++ b/airbyte-integrations/connectors/source-zendesk-support/integration_tests/expected_records.jsonl @@ -54,9 +54,9 @@ {"stream": "tickets", "data": {"url": "https://d3v-airbyte.zendesk.com/api/v2/tickets/125.json", "id": 125, "external_id": null, "via": {"channel": "web", "source": {"from": {}, "to": {}, "rel": null}}, "created_at": "2022-07-18T10:16:53Z", "updated_at": "2022-07-18T10:36:02Z", "type": "question", "subject": "Ticket Test 2", "raw_subject": "Ticket Test 2", "description": "238473846", "priority": "urgent", "status": "open", "recipient": null, "requester_id": 360786799676, "submitter_id": 360786799676, "assignee_id": 361089721035, "organization_id": 360033549136, "group_id": 5059439464079, "collaborator_ids": [360786799676], "follower_ids": [360786799676], "email_cc_ids": [], "forum_topic_id": null, "problem_id": null, "has_incidents": false, "is_public": false, "due_at": null, "tags": [], "custom_fields": [], "satisfaction_rating": {"score": "unoffered"}, "sharing_agreement_ids": [], "custom_status_id": 4044376, "fields": [], "followup_ids": [], "ticket_form_id": 360000084116, "brand_id": 360000358316, "allow_channelback": false, "allow_attachments": true, "from_messaging_channel": false, "generated_timestamp": 1658140562}, "emitted_at": 1697714865824} {"stream": "topics", "data": {"id": 7253394974479, "url": "https://d3v-airbyte.zendesk.com/api/v2/help_center/community/topics/7253394974479.json", "html_url": "https://d3v-airbyte.zendesk.com/hc/en-us/community/topics/7253394974479-Feature-Requests", "name": "Feature Requests", "description": null, "position": 0, "follower_count": 1, "community_id": 7253391140495, "created_at": "2023-06-22T00:32:21Z", "updated_at": "2023-06-22T00:32:21Z", "manageable_by": "managers", "user_segment_id": null}, "emitted_at": 1697714866838} {"stream": "topics", "data": {"id": 7253351897871, "url": "https://d3v-airbyte.zendesk.com/api/v2/help_center/community/topics/7253351897871.json", "html_url": "https://d3v-airbyte.zendesk.com/hc/en-us/community/topics/7253351897871-General-Discussion", "name": "General Discussion", "description": null, "position": 0, "follower_count": 1, "community_id": 7253391140495, "created_at": "2023-06-22T00:32:20Z", "updated_at": "2023-06-22T00:32:20Z", "manageable_by": "managers", "user_segment_id": null}, "emitted_at": 1697714866839} -{"stream": "users", "data": {"id": 4992781783439, "url": "https://d3v-airbyte.zendesk.com/api/v2/users/4992781783439.json", "name": "Caller +1 (689) 689-8023", "email": null, "created_at": "2022-06-17T14:49:19Z", "updated_at": "2022-06-17T14:49:19Z", "time_zone": "Pacific/Noumea", "iana_time_zone": "Pacific/Noumea", "phone": "+16896898023", "shared_phone_number": false, "photo": null, "locale_id": 1, "locale": "en-US", "organization_id": null, "role": "end-user", "verified": true, "external_id": null, "tags": [], "alias": null, "active": true, "shared": false, "shared_agent": false, "last_login_at": null, "two_factor_auth_enabled": false, "signature": null, "details": null, "notes": null, "role_type": null, "custom_role_id": null, "moderator": false, "ticket_restriction": "requested", "only_private_comments": false, "restricted_agent": true, "suspended": false, "default_group_id": null, "report_csv": false, "user_fields": {"test_display_name_checkbox_field": false, "test_display_name_decimal_field": null, "test_display_name_text_field": null}}, "emitted_at": 1703113150204} -{"stream": "users", "data": {"id": 4993467856015, "url": "https://d3v-airbyte.zendesk.com/api/v2/users/4993467856015.json", "name": "Caller +1 (912) 420-0314", "email": null, "created_at": "2022-06-17T19:52:38Z", "updated_at": "2022-06-17T19:52:38Z", "time_zone": "Pacific/Noumea", "iana_time_zone": "Pacific/Noumea", "phone": "+19124200314", "shared_phone_number": false, "photo": null, "locale_id": 1, "locale": "en-US", "organization_id": null, "role": "end-user", "verified": true, "external_id": null, "tags": [], "alias": null, "active": true, "shared": false, "shared_agent": false, "last_login_at": null, "two_factor_auth_enabled": false, "signature": null, "details": null, "notes": null, "role_type": null, "custom_role_id": null, "moderator": false, "ticket_restriction": "requested", "only_private_comments": false, "restricted_agent": true, "suspended": false, "default_group_id": null, "report_csv": false, "user_fields": {"test_display_name_checkbox_field": false, "test_display_name_decimal_field": null, "test_display_name_text_field": null}}, "emitted_at": 1703113150205} -{"stream": "users", "data": {"id": 5137812260495, "url": "https://d3v-airbyte.zendesk.com/api/v2/users/5137812260495.json", "name": "Caller +1 (607) 210-9549", "email": null, "created_at": "2022-07-13T14:34:04Z", "updated_at": "2022-07-13T14:34:04Z", "time_zone": "Pacific/Noumea", "iana_time_zone": "Pacific/Noumea", "phone": "+16072109549", "shared_phone_number": false, "photo": null, "locale_id": 1, "locale": "en-US", "organization_id": null, "role": "end-user", "verified": true, "external_id": null, "tags": [], "alias": null, "active": true, "shared": false, "shared_agent": false, "last_login_at": null, "two_factor_auth_enabled": false, "signature": null, "details": null, "notes": null, "role_type": null, "custom_role_id": null, "moderator": false, "ticket_restriction": "requested", "only_private_comments": false, "restricted_agent": true, "suspended": false, "default_group_id": null, "report_csv": false, "user_fields": {"test_display_name_checkbox_field": false, "test_display_name_decimal_field": null, "test_display_name_text_field": null}}, "emitted_at": 1703113150205} +{"stream":"users","data":{"id":4992781783439,"url":"https://d3v-airbyte.zendesk.com/api/v2/users/4992781783439.json","name":"Caller +1 (689) 689-8023","email":null,"created_at":"2022-06-17T14:49:19Z","updated_at":"2022-06-17T14:49:19Z","time_zone":"Pacific/Noumea","iana_time_zone":"Pacific/Noumea","phone":"+16896898023","shared_phone_number":false,"photo":null,"locale_id":1,"locale":"en-US","organization_id":null,"role":"end-user","verified":true,"external_id":null,"tags":[],"alias":null,"active":true,"shared":false,"shared_agent":false,"last_login_at":null,"two_factor_auth_enabled":null,"signature":null,"details":null,"notes":null,"role_type":null,"custom_role_id":null,"moderator":false,"ticket_restriction":"requested","only_private_comments":false,"restricted_agent":true,"suspended":false,"default_group_id":null,"report_csv":false,"user_fields":{"test_display_name_checkbox_field":false,"test_display_name_decimal_field":null,"test_display_name_text_field":null}},"emitted_at":1704976960493} +{"stream":"users","data":{"id":4993467856015,"url":"https://d3v-airbyte.zendesk.com/api/v2/users/4993467856015.json","name":"Caller +1 (912) 420-0314","email":null,"created_at":"2022-06-17T19:52:38Z","updated_at":"2022-06-17T19:52:38Z","time_zone":"Pacific/Noumea","iana_time_zone":"Pacific/Noumea","phone":"+19124200314","shared_phone_number":false,"photo":null,"locale_id":1,"locale":"en-US","organization_id":null,"role":"end-user","verified":true,"external_id":null,"tags":[],"alias":null,"active":true,"shared":false,"shared_agent":false,"last_login_at":null,"two_factor_auth_enabled":null,"signature":null,"details":null,"notes":null,"role_type":null,"custom_role_id":null,"moderator":false,"ticket_restriction":"requested","only_private_comments":false,"restricted_agent":true,"suspended":false,"default_group_id":null,"report_csv":false,"user_fields":{"test_display_name_checkbox_field":false,"test_display_name_decimal_field":null,"test_display_name_text_field":null}},"emitted_at":1704976960494} +{"stream":"users","data":{"id":5137812260495,"url":"https://d3v-airbyte.zendesk.com/api/v2/users/5137812260495.json","name":"Caller +1 (607) 210-9549","email":null,"created_at":"2022-07-13T14:34:04Z","updated_at":"2022-07-13T14:34:04Z","time_zone":"Pacific/Noumea","iana_time_zone":"Pacific/Noumea","phone":"+16072109549","shared_phone_number":false,"photo":null,"locale_id":1,"locale":"en-US","organization_id":null,"role":"end-user","verified":true,"external_id":null,"tags":[],"alias":null,"active":true,"shared":false,"shared_agent":false,"last_login_at":null,"two_factor_auth_enabled":null,"signature":null,"details":null,"notes":null,"role_type":null,"custom_role_id":null,"moderator":false,"ticket_restriction":"requested","only_private_comments":false,"restricted_agent":true,"suspended":false,"default_group_id":null,"report_csv":false,"user_fields":{"test_display_name_checkbox_field":false,"test_display_name_decimal_field":null,"test_display_name_text_field":null}},"emitted_at":1704976960494} {"stream": "brands", "data": {"url": "https://d3v-airbyte.zendesk.com/api/v2/brands/360000358316.json", "id": 360000358316, "name": "Airbyte", "brand_url": "https://d3v-airbyte.zendesk.com", "subdomain": "d3v-airbyte", "host_mapping": null, "has_help_center": true, "help_center_state": "enabled", "active": true, "default": true, "is_deleted": false, "logo": null, "ticket_form_ids": [360000084116], "signature_template": "{{agent.signature}}", "created_at": "2020-12-11T18:34:04Z", "updated_at": "2020-12-11T18:34:09Z"}, "emitted_at": 1697714873604} {"stream": "custom_roles", "data": {"id": 360000210636, "name": "Advisor", "description": "Can automate ticket workflows, manage channels and make private comments on tickets", "role_type": 0, "created_at": "2020-12-11T18:34:36Z", "updated_at": "2020-12-11T18:34:36Z", "configuration": {"chat_access": true, "end_user_list_access": "full", "forum_access_restricted_content": false, "light_agent": false, "manage_business_rules": true, "manage_dynamic_content": false, "manage_extensions_and_channels": true, "manage_facebook": true, "moderate_forums": false, "side_conversation_create": true, "ticket_access": "within-groups", "ticket_comment_access": "none", "ticket_deletion": false, "ticket_tag_editing": true, "twitter_search_access": false, "view_deleted_tickets": false, "voice_access": true, "group_access": false, "organization_editing": false, "organization_notes_editing": false, "assign_tickets_to_any_group": false, "end_user_profile_access": "readonly", "explore_access": "readonly", "forum_access": "readonly", "macro_access": "full", "report_access": "none", "ticket_editing": true, "ticket_merge": false, "user_view_access": "full", "view_access": "full", "voice_dashboard_access": false, "manage_automations": true, "manage_contextual_workspaces": false, "manage_organization_fields": false, "manage_skills": true, "manage_slas": true, "manage_ticket_fields": false, "manage_ticket_forms": false, "manage_user_fields": false, "ticket_redaction": false, "manage_roles": "none", "manage_groups": false, "manage_group_memberships": false, "manage_organizations": false, "manage_suspended_tickets": false, "manage_triggers": true, "view_reduced_count": false, "view_filter_tickets": true, "manage_macro_content_suggestions": false, "read_macro_content_suggestions": false, "custom_objects": {}}, "team_member_count": 1}, "emitted_at": 1698749854337} {"stream": "custom_roles", "data": {"id": 360000210596, "name": "Staff", "description": "Can edit tickets within their groups", "role_type": 0, "created_at": "2020-12-11T18:34:36Z", "updated_at": "2020-12-11T18:34:36Z", "configuration": {"chat_access": true, "end_user_list_access": "full", "forum_access_restricted_content": false, "light_agent": false, "manage_business_rules": false, "manage_dynamic_content": false, "manage_extensions_and_channels": false, "manage_facebook": false, "moderate_forums": false, "side_conversation_create": true, "ticket_access": "within-groups", "ticket_comment_access": "public", "ticket_deletion": false, "ticket_tag_editing": false, "twitter_search_access": false, "view_deleted_tickets": false, "voice_access": true, "group_access": false, "organization_editing": false, "organization_notes_editing": false, "assign_tickets_to_any_group": false, "end_user_profile_access": "readonly", "explore_access": "readonly", "forum_access": "readonly", "macro_access": "manage-personal", "report_access": "readonly", "ticket_editing": true, "ticket_merge": false, "user_view_access": "manage-personal", "view_access": "manage-personal", "voice_dashboard_access": false, "manage_automations": false, "manage_contextual_workspaces": false, "manage_organization_fields": false, "manage_skills": false, "manage_slas": false, "manage_ticket_fields": false, "manage_ticket_forms": false, "manage_user_fields": false, "ticket_redaction": false, "manage_roles": "none", "manage_groups": false, "manage_group_memberships": false, "manage_organizations": false, "manage_suspended_tickets": false, "manage_triggers": false, "view_reduced_count": false, "view_filter_tickets": true, "manage_macro_content_suggestions": false, "read_macro_content_suggestions": false, "custom_objects": {}}, "team_member_count": 1}, "emitted_at": 1698749854338} diff --git a/airbyte-integrations/connectors/source-zendesk-support/metadata.yaml b/airbyte-integrations/connectors/source-zendesk-support/metadata.yaml index a52ad62c5f27c..30befddb885f8 100644 --- a/airbyte-integrations/connectors/source-zendesk-support/metadata.yaml +++ b/airbyte-integrations/connectors/source-zendesk-support/metadata.yaml @@ -7,11 +7,11 @@ data: - ${subdomain}.zendesk.com - zendesk.com connectorBuildOptions: - baseImage: docker.io/airbyte/python-connector-base:1.1.0@sha256:bd98f6505c6764b1b5f99d3aedc23dfc9e9af631a62533f60eb32b1d3dbab20c + baseImage: docker.io/airbyte/python-connector-base:1.2.0@sha256:c22a9d97464b69d6ef01898edf3f8612dc11614f05a84984451dde195f337db9 connectorSubtype: api connectorType: source definitionId: 79c1aa37-dae3-42ae-b333-d1c105477715 - dockerImageTag: 2.2.5 + dockerImageTag: 2.2.6 dockerRepository: airbyte/source-zendesk-support documentationUrl: https://docs.airbyte.com/integrations/sources/zendesk-support githubIssueLabel: source-zendesk-support diff --git a/airbyte-integrations/connectors/source-zendesk-support/source_zendesk_support/streams.py b/airbyte-integrations/connectors/source-zendesk-support/source_zendesk_support/streams.py index 3572a60051c99..ab5316725f1a8 100644 --- a/airbyte-integrations/connectors/source-zendesk-support/source_zendesk_support/streams.py +++ b/airbyte-integrations/connectors/source-zendesk-support/source_zendesk_support/streams.py @@ -626,6 +626,23 @@ def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, response_json = response.json() return {"cursor": response.json().get("before_cursor")} if response_json.get("before_cursor") else None + def read_records( + self, + sync_mode: SyncMode, + cursor_field: Optional[List[str]] = None, + stream_slice: Optional[Mapping[str, Any]] = None, + stream_state: Optional[Mapping[str, Any]] = None, + ) -> Iterable[StreamData]: + try: + yield from super().read_records( + sync_mode=sync_mode, cursor_field=cursor_field, stream_slice=stream_slice, stream_state=stream_state + ) + except requests.exceptions.HTTPError as e: + if e.response.status_code == requests.codes.GATEWAY_TIMEOUT: + self.logger.error(f"Skipping stream `{self.name}`. Timed out waiting for response: {e.response.text}...") + else: + raise e + class Tags(FullRefreshZendeskSupportStream): """Tags stream: https://developer.zendesk.com/api-reference/ticketing/ticket-management/tags/""" diff --git a/airbyte-integrations/connectors/source-zendesk-support/unit_tests/unit_test.py b/airbyte-integrations/connectors/source-zendesk-support/unit_tests/unit_test.py index 9a676d4334ae6..6dde9b4f99e8e 100644 --- a/airbyte-integrations/connectors/source-zendesk-support/unit_tests/unit_test.py +++ b/airbyte-integrations/connectors/source-zendesk-support/unit_tests/unit_test.py @@ -1101,3 +1101,13 @@ def test_read_non_json_error(requests_mock, caplog): ) read_full_refresh(stream) assert expected_message in (record.message for record in caplog.records if record.levelname == "ERROR") + + +def test_read_ticket_audits_504_error(requests_mock, caplog): + requests_mock.get("https://subdomain.zendesk.com/api/v2/ticket_audits", status_code=504, text="upstream request timeout") + stream = TicketAudits(subdomain="subdomain", start_date="2020-01-01T00:00:00Z") + expected_message = ( + "Skipping stream `ticket_audits`. Timed out waiting for response: upstream request timeout..." + ) + read_full_refresh(stream) + assert expected_message in (record.message for record in caplog.records if record.levelname == "ERROR") diff --git a/docs/integrations/sources/zendesk-support.md b/docs/integrations/sources/zendesk-support.md index 9eda60be0aaf8..a5f93a80766bf 100644 --- a/docs/integrations/sources/zendesk-support.md +++ b/docs/integrations/sources/zendesk-support.md @@ -158,7 +158,8 @@ The Zendesk connector ideally should not run into Zendesk API limitations under | Version | Date | Pull Request | Subject | |:---------|:-----------|:---------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 2.2.5 | 2024-01-08 | [34010](https://github.com/airbytehq/airbyte/pull/34010) | prepare for airbyte-lib | +| `2.2.6` | 2024-01-11 | [34064](https://github.com/airbytehq/airbyte/pull/34064) | Skip 504 Error for stream `Ticket Audits` | +| `2.2.5` | 2024-01-08 | [34010](https://github.com/airbytehq/airbyte/pull/34010) | prepare for airbyte-lib | | `2.2.4` | 2023-12-20 | [33680](https://github.com/airbytehq/airbyte/pull/33680) | Fix pagination issue for streams related to incremental export sync | | `2.2.3` | 2023-12-14 | [33435](https://github.com/airbytehq/airbyte/pull/33435) | Fix 504 Error for stream Ticket Audits | | `2.2.2` | 2023-12-01 | [33012](https://github.com/airbytehq/airbyte/pull/33012) | Increase number of retries for backoff policy to 10 | From 5b0c717b6058546a3c73232eac3b3e23076ceab0 Mon Sep 17 00:00:00 2001 From: Maxime Carbonneau-Leclerc <3360483+maxi297@users.noreply.github.com> Date: Thu, 11 Jan 2024 10:50:54 -0500 Subject: [PATCH 060/574] =?UTF-8?q?=F0=9F=90=9B=20Source=20Stripe:=20addin?= =?UTF-8?q?g=20integration=20tests=20(#33306)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: maxi297 Co-authored-by: Catherine Noll Co-authored-by: Brian Lai <51336873+brianjlai@users.noreply.github.com> --- .../connectors/source-stripe/metadata.yaml | 2 +- .../connectors/source-stripe/setup.py | 5 +- .../source-stripe/source_stripe/streams.py | 7 + .../source-stripe/unit_tests/conftest.py | 1 + .../unit_tests/integration/__init__.py | 0 .../unit_tests/integration/config.py | 36 + .../unit_tests/integration/pagination.py | 11 + .../unit_tests/integration/request_builder.py | 143 ++++ .../integration/response_builder.py | 10 + .../integration/test_application_fees.py | 376 +++++++++++ .../test_application_fees_refunds.py | 518 ++++++++++++++ .../integration/test_authorizations.py | 374 ++++++++++ .../integration/test_bank_accounts.py | 563 ++++++++++++++++ .../unit_tests/integration/test_cards.py | 374 ++++++++++ .../integration/test_early_fraud_warnings.py | 342 ++++++++++ .../unit_tests/integration/test_events.py | 272 ++++++++ .../test_external_account_bank_accounts.py | 361 ++++++++++ .../test_external_account_cards.py | 366 ++++++++++ .../integration/test_payment_methods.py | 347 ++++++++++ .../unit_tests/integration/test_persons.py | 636 ++++++++++++++++++ .../unit_tests/integration/test_reviews.py | 374 ++++++++++ .../integration/test_transactions.py | 374 ++++++++++ .../resource/http/response/400.json | 7 + .../resource/http/response/401.json | 6 + .../resource/http/response/403.json | 8 + .../resource/http/response/429.json | 8 + .../resource/http/response/500.json | 3 + .../resource/http/response/accounts.json | 136 ++++ .../http/response/application_fees.json | 138 ++++ .../response/application_fees_refunds.json | 17 + .../resource/http/response/bank_accounts.json | 23 + .../customers_expand_data_source.json | 43 ++ .../resource/http/response/events.json | 58 ++ .../http/response/external_account_cards.json | 34 + .../http/response/external_bank_accounts.json | 23 + .../http/response/issuing_authorizations.json | 142 ++++ .../resource/http/response/issuing_cards.json | 82 +++ .../http/response/issuing_transactions.json | 38 ++ .../http/response/payment_methods.json | 52 ++ .../resource/http/response/persons.json | 65 ++ .../response/radar_early_fraud_warnings.json | 16 + .../resource/http/response/refunds.json | 32 + .../resource/http/response/reviews.json | 23 + docs/integrations/sources/stripe.md | 173 ++--- 44 files changed, 6530 insertions(+), 89 deletions(-) create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/__init__.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/config.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/pagination.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/request_builder.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/response_builder.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_application_fees.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_application_fees_refunds.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_authorizations.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_bank_accounts.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_cards.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_early_fraud_warnings.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_events.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_external_account_bank_accounts.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_external_account_cards.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_payment_methods.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_persons.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_reviews.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_transactions.py create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/400.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/401.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/403.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/429.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/500.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/accounts.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/application_fees.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/application_fees_refunds.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/bank_accounts.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/customers_expand_data_source.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/events.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/external_account_cards.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/external_bank_accounts.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/issuing_authorizations.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/issuing_cards.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/issuing_transactions.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/payment_methods.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/persons.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/radar_early_fraud_warnings.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/refunds.json create mode 100644 airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/reviews.json diff --git a/airbyte-integrations/connectors/source-stripe/metadata.yaml b/airbyte-integrations/connectors/source-stripe/metadata.yaml index ee417c24c7a94..1301154967ab6 100644 --- a/airbyte-integrations/connectors/source-stripe/metadata.yaml +++ b/airbyte-integrations/connectors/source-stripe/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: e094cb9a-26de-4645-8761-65c0c425d1de - dockerImageTag: 5.1.2 + dockerImageTag: 5.1.3 dockerRepository: airbyte/source-stripe documentationUrl: https://docs.airbyte.com/integrations/sources/stripe githubIssueLabel: source-stripe diff --git a/airbyte-integrations/connectors/source-stripe/setup.py b/airbyte-integrations/connectors/source-stripe/setup.py index 2d05b4aec1c85..4c78d7e312c08 100644 --- a/airbyte-integrations/connectors/source-stripe/setup.py +++ b/airbyte-integrations/connectors/source-stripe/setup.py @@ -5,9 +5,10 @@ from setuptools import find_packages, setup -MAIN_REQUIREMENTS = ["airbyte-cdk==0.55.2", "stripe==2.56.0", "pendulum==2.1.2"] +MAIN_REQUIREMENTS = ["airbyte-cdk==0.57.6", "stripe==2.56.0", "pendulum==2.1.2"] -TEST_REQUIREMENTS = ["pytest-mock~=3.6.1", "pytest~=6.1", "requests-mock", "requests_mock~=1.8", "freezegun==1.2.2"] +# we set `requests-mock~=1.11.0` to ensure concurrency is supported +TEST_REQUIREMENTS = ["pytest-mock~=3.6.1", "pytest~=6.1", "requests-mock~=1.11.0", "requests_mock~=1.8", "freezegun==1.2.2"] setup( name="source_stripe", diff --git a/airbyte-integrations/connectors/source-stripe/source_stripe/streams.py b/airbyte-integrations/connectors/source-stripe/source_stripe/streams.py index 7c7185549d432..81ca02264d7d5 100644 --- a/airbyte-integrations/connectors/source-stripe/source_stripe/streams.py +++ b/airbyte-integrations/connectors/source-stripe/source_stripe/streams.py @@ -21,6 +21,7 @@ STRIPE_API_VERSION = "2022-11-15" CACHE_DISABLED = os.environ.get("CACHE_DISABLED") +IS_TESTING = os.environ.get("DEPLOYMENT_MODE") == "testing" USE_CACHE = not CACHE_DISABLED @@ -197,6 +198,12 @@ def request_headers(self, **kwargs) -> Mapping[str, Any]: headers["Stripe-Account"] = self.account_id return headers + def retry_factor(self) -> float: + """ + Override for testing purposes + """ + return 0 if IS_TESTING else super(StripeStream, self).retry_factor + class IStreamSelector(ABC): @abstractmethod diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/conftest.py b/airbyte-integrations/connectors/source-stripe/unit_tests/conftest.py index 884543613e5fd..8e81ce9683066 100644 --- a/airbyte-integrations/connectors/source-stripe/unit_tests/conftest.py +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/conftest.py @@ -8,6 +8,7 @@ from airbyte_cdk.sources.streams.http.auth import TokenAuthenticator os.environ["CACHE_DISABLED"] = "true" +os.environ["DEPLOYMENT_MODE"] = "testing" @pytest.fixture(name="config") diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/__init__.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/config.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/config.py new file mode 100644 index 0000000000000..d048407320d19 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/config.py @@ -0,0 +1,36 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from datetime import datetime +from typing import Any, Dict + + +class ConfigBuilder: + def __init__(self) -> None: + self._config: Dict[str, Any] = { + "client_secret": "ConfigBuilder default client secret", + "account_id": "ConfigBuilder default account id", + "start_date": "2020-05-01T00:00:00Z" + } + + def with_account_id(self, account_id: str) -> "ConfigBuilder": + self._config["account_id"] = account_id + return self + + def with_client_secret(self, client_secret: str) -> "ConfigBuilder": + self._config["client_secret"] = client_secret + return self + + def with_start_date(self, start_datetime: datetime) -> "ConfigBuilder": + self._config["start_date"] = start_datetime.isoformat()[:-13]+"Z" + return self + + def with_lookback_window_in_days(self, number_of_days: int) -> "ConfigBuilder": + self._config["lookback_window_days"] = number_of_days + return self + + def with_slice_range_in_days(self, number_of_days: int) -> "ConfigBuilder": + self._config["slice_range"] = number_of_days + return self + + def build(self) -> Dict[str, Any]: + return self._config diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/pagination.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/pagination.py new file mode 100644 index 0000000000000..acfe9a6132711 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/pagination.py @@ -0,0 +1,11 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from typing import Any, Dict + +from airbyte_cdk.test.mock_http.response_builder import PaginationStrategy + + +class StripePaginationStrategy(PaginationStrategy): + @staticmethod + def update(response: Dict[str, Any]) -> None: + response["has_more"] = True diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/request_builder.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/request_builder.py new file mode 100644 index 0000000000000..7a2c8219c5d89 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/request_builder.py @@ -0,0 +1,143 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from datetime import datetime +from typing import List, Optional + +from airbyte_cdk.test.mock_http import HttpRequest +from airbyte_cdk.test.mock_http.request import ANY_QUERY_PARAMS + + +class StripeRequestBuilder: + + @classmethod + def accounts_endpoint(cls, account_id: str, client_secret: str) -> "StripeRequestBuilder": + return cls("accounts", account_id, client_secret) + + @classmethod + def application_fees_endpoint(cls, account_id: str, client_secret: str) -> "StripeRequestBuilder": + return cls("application_fees", account_id, client_secret) + + @classmethod + def application_fees_refunds_endpoint(cls, application_fee_id: str, account_id: str, client_secret: str) -> "StripeRequestBuilder": + return cls(f"application_fees/{application_fee_id}/refunds", account_id, client_secret) + + @classmethod + def customers_endpoint(cls, account_id: str, client_secret: str) -> "StripeRequestBuilder": + return cls("customers", account_id, client_secret) + + @classmethod + def customers_bank_accounts_endpoint(cls, customer_id: str, account_id: str, client_secret: str) -> "StripeRequestBuilder": + return cls(f"customers/{customer_id}/bank_accounts", account_id, client_secret) + + @classmethod + def events_endpoint(cls, account_id: str, client_secret: str) -> "StripeRequestBuilder": + return cls("events", account_id, client_secret) + + @classmethod + def external_accounts_endpoint(cls, account_id: str, client_secret: str) -> "StripeRequestBuilder": + return cls(f"accounts/{account_id}/external_accounts", account_id, client_secret) + + @classmethod + def issuing_authorizations_endpoint(cls, account_id: str, client_secret: str) -> "StripeRequestBuilder": + return cls("issuing/authorizations", account_id, client_secret) + + @classmethod + def issuing_cards_endpoint(cls, account_id: str, client_secret: str) -> "StripeRequestBuilder": + return cls("issuing/cards", account_id, client_secret) + + @classmethod + def issuing_transactions_endpoint(cls, account_id: str, client_secret: str) -> "StripeRequestBuilder": + return cls("issuing/transactions", account_id, client_secret) + + @classmethod + def payment_methods_endpoint(cls, account_id: str, client_secret: str) -> "StripeRequestBuilder": + return cls("payment_methods", account_id, client_secret) + + @classmethod + def persons_endpoint(cls, parent_account_id: str, account_id: str, client_secret: str, ) -> "StripeRequestBuilder": + return cls(f"accounts/{parent_account_id}/persons", account_id, client_secret) + + @classmethod + def radar_early_fraud_warnings_endpoint(cls, account_id: str, client_secret: str) -> "StripeRequestBuilder": + return cls("radar/early_fraud_warnings", account_id, client_secret) + + @classmethod + def reviews_endpoint(cls, account_id: str, client_secret: str) -> "StripeRequestBuilder": + return cls("reviews", account_id, client_secret) + + @classmethod + def _for_endpoint(cls, endpoint: str, account_id: str, client_secret: str) -> "StripeRequestBuilder": + return cls(endpoint, account_id, client_secret) + + def __init__(self, resource: str, account_id: str, client_secret: str) -> None: + self._resource = resource + self._account_id = account_id + self._client_secret = client_secret + self._any_query_params = False + self._created_gte: Optional[datetime] = None + self._created_lte: Optional[datetime] = None + self._limit: Optional[int] = None + self._object: Optional[str] = None + self._starting_after_id: Optional[str] = None + self._types: List[str] = [] + self._expands: List[str] = [] + + def with_created_gte(self, created_gte: datetime) -> "StripeRequestBuilder": + self._created_gte = created_gte + return self + + def with_created_lte(self, created_lte: datetime) -> "StripeRequestBuilder": + self._created_lte = created_lte + return self + + def with_limit(self, limit: int) -> "StripeRequestBuilder": + self._limit = limit + return self + + def with_object(self, object_name: str) -> "StripeRequestBuilder": + self._object = object_name + return self + + def with_starting_after(self, starting_after_id: str) -> "StripeRequestBuilder": + self._starting_after_id = starting_after_id + return self + + def with_any_query_params(self) -> "StripeRequestBuilder": + self._any_query_params = True + return self + + def with_types(self, types: List[str]) -> "StripeRequestBuilder": + self._types = types + return self + + def with_expands(self, expands: List[str]) -> "StripeRequestBuilder": + self._expands = expands + return self + + def build(self) -> HttpRequest: + query_params = {} + if self._created_gte: + query_params["created[gte]"] = str(int(self._created_gte.timestamp())) + if self._created_lte: + query_params["created[lte]"] = str(int(self._created_lte.timestamp())) + if self._limit: + query_params["limit"] = str(self._limit) + if self._starting_after_id: + query_params["starting_after"] = self._starting_after_id + if self._types: + query_params["types[]"] = self._types + if self._object: + query_params["object"] = self._object + if self._expands: + query_params["expand[]"] = self._expands + + if self._any_query_params: + if query_params: + raise ValueError(f"Both `any_query_params` and {list(query_params.keys())} were configured. Provide only one of none but not both.") + query_params = ANY_QUERY_PARAMS + + return HttpRequest( + url=f"https://api.stripe.com/v1/{self._resource}", + query_params=query_params, + headers={"Stripe-Account": self._account_id, "Authorization": f"Bearer {self._client_secret}"}, + ) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/response_builder.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/response_builder.py new file mode 100644 index 0000000000000..7495bffeb4a9e --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/response_builder.py @@ -0,0 +1,10 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import json + +from airbyte_cdk.test.mock_http import HttpResponse +from airbyte_cdk.test.mock_http.response_builder import find_template + + +def a_response_with_status(status_code: int) -> HttpResponse: + return HttpResponse(json.dumps(find_template(str(status_code), __file__)), status_code) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_application_fees.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_application_fees.py new file mode 100644 index 0000000000000..9619010e4c242 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_application_fees.py @@ -0,0 +1,376 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import json +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, Optional +from unittest import TestCase + +import freezegun +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +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 ConfiguredAirbyteCatalog, FailureType, SyncMode +from integration.config import ConfigBuilder +from integration.pagination import StripePaginationStrategy +from integration.request_builder import StripeRequestBuilder +from integration.response_builder import a_response_with_status +from source_stripe import SourceStripe + +_EVENT_TYPES = ["application_fee.created", "application_fee.refunded"] + +_DATA_FIELD = NestedPath(["data", "object"]) +_STREAM_NAME = "application_fees" +_ENDPOINT_TEMPLATE_NAME = "application_fees" +_NOW = datetime.now(timezone.utc) +_A_START_DATE = _NOW - timedelta(days=60) +_ACCOUNT_ID = "account_id" +_CLIENT_SECRET = "client_secret" +_NO_STATE = {} +_AVOIDING_INCLUSIVE_BOUNDARIES = timedelta(seconds=1) + + +def _application_fees_request() -> StripeRequestBuilder: + return StripeRequestBuilder.application_fees_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _events_request() -> StripeRequestBuilder: + return StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _config() -> ConfigBuilder: + return ConfigBuilder().with_start_date(_NOW - timedelta(days=75)).with_account_id(_ACCOUNT_ID).with_client_secret(_CLIENT_SECRET) + + +def _catalog(sync_mode: SyncMode) -> ConfiguredAirbyteCatalog: + return CatalogBuilder().with_stream(_STREAM_NAME, sync_mode).build() + + +def _source(catalog: ConfiguredAirbyteCatalog, config: Dict[str, Any]) -> SourceStripe: + return SourceStripe(catalog, config) + + +def _an_event() -> RecordBuilder: + return create_record_builder( + find_template("events", __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _events_response() -> HttpResponseBuilder: + return create_response_builder( + find_template("events", __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _an_application_fee() -> RecordBuilder: + return create_record_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _application_fees_response() -> HttpResponseBuilder: + return create_response_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _given_application_fees_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.application_fees_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _application_fees_response().build() + ) + + +def _given_events_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _events_response().build() + ) + + +def _read( + config_builder: ConfigBuilder, + sync_mode: SyncMode, + state: Optional[Dict[str, Any]] = None, + expecting_exception: bool = False +) -> EntrypointOutput: + catalog = _catalog(sync_mode) + config = config_builder.build() + return read(_source(catalog, config), config, catalog, state, expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class FullRefreshTest(TestCase): + + @HttpMocker() + def test_given_one_page_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _application_fees_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response().with_record(_an_application_fee()).with_record(_an_application_fee()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_many_pages_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _application_fees_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response().with_pagination().with_record(_an_application_fee().with_id("last_record_id_from_first_page")).build(), + ) + http_mocker.get( + _application_fees_request().with_starting_after("last_record_id_from_first_page").with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response().with_record(_an_application_fee()).with_record(_an_application_fee()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_no_state_when_read_then_return_ignore_lookback(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _application_fees_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response().with_record(_an_application_fee()).build(), + ) + + self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + # request matched http_mocker + + @HttpMocker() + def test_when_read_then_add_cursor_field(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _application_fees_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response().with_record(_an_application_fee()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + assert output.records[0].record.data["updated"] == output.records[0].record.data["created"] + + @HttpMocker() + def test_given_slice_range_when_read_then_perform_multiple_requests(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=30) + slice_range = timedelta(days=20) + slice_datetime = start_date + slice_range + + _given_events_availability_check(http_mocker) + http_mocker.get( + _application_fees_request().with_created_gte(start_date).with_created_lte(slice_datetime).with_limit(100).build(), + _application_fees_response().build(), + ) + http_mocker.get( + _application_fees_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response().build(), + ) + + self._read(_config().with_start_date(start_date).with_slice_range_in_days(slice_range.days)) + + # request matched http_mocker + + @HttpMocker() + def test_given_http_status_400_when_read_then_stream_is_ignored(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _application_fees_request().with_any_query_params().build(), + a_response_with_status(400), + ) + output = self._read(_config()) + assert len(output.get_stream_statuses(_STREAM_NAME)) == 0 + + @HttpMocker() + def test_given_http_status_401_when_read_then_system_error(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _application_fees_request().with_any_query_params().build(), + a_response_with_status(401), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_rate_limited_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _application_fees_request().with_any_query_params().build(), + [ + a_response_with_status(429), + _application_fees_response().with_record(_an_application_fee()).build(), + ], + ) + output = self._read(_config().with_start_date(_A_START_DATE)) + assert len(output.records) == 1 + + @HttpMocker() + def test_given_http_status_500_once_before_200_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _application_fees_request().with_any_query_params().build(), + [a_response_with_status(500), _application_fees_response().with_record(_an_application_fee()).build()], + ) + output = self._read(_config()) + 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: + http_mocker.get( + _application_fees_request().with_any_query_params().build(), + a_response_with_status(500), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_small_slice_range_when_read_then_availability_check_performs_too_many_queries(self, http_mocker: HttpMocker) -> None: + # see https://github.com/airbytehq/airbyte/issues/33499 + events_requests = StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build() + http_mocker.get( + events_requests, + _events_response().build() # it is important that the event response does not have a record. This is not far fetched as this is what would happend 30 days before now + ) + http_mocker.get( + _application_fees_request().with_any_query_params().build(), + _application_fees_response().build(), + ) + + self._read(_config().with_start_date(_NOW - timedelta(days=60)).with_slice_range_in_days(1)) + + http_mocker.assert_number_of_calls(events_requests, 30) + + def _read(self, config: ConfigBuilder, expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.full_refresh, expecting_exception=expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class IncrementalTest(TestCase): + + @HttpMocker() + def test_given_no_state_when_read_then_use_application_fees_endpoint(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + cursor_value = int(_A_START_DATE.timestamp()) + 1 + http_mocker.get( + _application_fees_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response().with_record(_an_application_fee().with_cursor(cursor_value)).build(), + ) + output = self._read(_config().with_start_date(_A_START_DATE), _NO_STATE) + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_when_read_then_query_events_using_types_and_state_value_plus_1(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=40) + state_datetime = _NOW - timedelta(days=5) + cursor_value = int(state_datetime.timestamp()) + 1 + + _given_application_fees_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record( + _an_event().with_cursor(cursor_value).with_field(_DATA_FIELD, _an_application_fee().build()) + ).build(), + ) + + output = self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_and_pagination_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_application_fees_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + state_datetime = _NOW - timedelta(days=5) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_pagination().with_record( + _an_event().with_id("last_record_id_from_first_page").with_field(_DATA_FIELD, _an_application_fee().build()) + ).build(), + ) + http_mocker.get( + _events_request().with_starting_after("last_record_id_from_first_page").with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record( + self._an_application_fee_event() + ).build(), + ) + + output = self._read( + _config(), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_state_and_small_slice_range_when_read_then_perform_multiple_queries(self, http_mocker: HttpMocker) -> None: + state_datetime = _NOW - timedelta(days=5) + slice_range = timedelta(days=3) + slice_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + slice_range + + _given_application_fees_availability_check(http_mocker) + _given_events_availability_check(http_mocker) # the availability check does not consider the state so we need to define a generic availability check + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(slice_datetime).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_application_fee_event()).build(), + ) + http_mocker.get( + _events_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_application_fee_event()).with_record(self._an_application_fee_event()).build(), + ) + + output = self._read( + _config().with_start_date(_NOW - timedelta(days=30)).with_slice_range_in_days(slice_range.days), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_state_earlier_than_30_days_when_read_then_query_events_using_types_and_event_lower_boundary(self, http_mocker: HttpMocker) -> None: + # this seems odd as we would miss some data between start_date and events_lower_boundary. In that case, we should hit the + # application fees endpoint + _given_application_fees_availability_check(http_mocker) + start_date = _NOW - timedelta(days=40) + state_value = _NOW - timedelta(days=39) + events_lower_boundary = _NOW - timedelta(days=30) + http_mocker.get( + _events_request().with_created_gte(events_lower_boundary).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_application_fee_event()).build(), + ) + + self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_value.timestamp())}).build(), + ) + + # request matched http_mocker + + def _an_application_fee_event(self) -> RecordBuilder: + return _an_event().with_field(_DATA_FIELD, _an_application_fee().build()) + + def _read(self, config: ConfigBuilder, state: Optional[Dict[str, Any]], expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.incremental, state, expecting_exception) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_application_fees_refunds.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_application_fees_refunds.py new file mode 100644 index 0000000000000..f01c6da9689eb --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_application_fees_refunds.py @@ -0,0 +1,518 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + + +import json +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, Optional +from unittest import TestCase + +import freezegun +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +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 ConfiguredAirbyteCatalog, FailureType, SyncMode +from integration.config import ConfigBuilder +from integration.pagination import StripePaginationStrategy +from integration.request_builder import StripeRequestBuilder +from integration.response_builder import a_response_with_status +from source_stripe import SourceStripe + +_EVENT_TYPES = ["application_fee.refund.updated"] + +_DATA_FIELD = NestedPath(["data", "object"]) +_REFUNDS_FIELD = FieldPath("refunds") +_STREAM_NAME = "application_fees_refunds" +_APPLICATION_FEES_TEMPLATE_NAME = "application_fees" +_REFUNDS_TEMPLATE_NAME = "application_fees_refunds" +_NOW = datetime.now(timezone.utc) +_A_START_DATE = _NOW - timedelta(days=60) +_ACCOUNT_ID = "account_id" +_CLIENT_SECRET = "client_secret" +_NO_STATE = {} +_AVOIDING_INCLUSIVE_BOUNDARIES = timedelta(seconds=1) + + +def _application_fees_request() -> StripeRequestBuilder: + return StripeRequestBuilder.application_fees_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _application_fees_refunds_request(application_fee_id: str) -> StripeRequestBuilder: + return StripeRequestBuilder.application_fees_refunds_endpoint(application_fee_id, _ACCOUNT_ID, _CLIENT_SECRET) + + +def _events_request() -> StripeRequestBuilder: + return StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _config() -> ConfigBuilder: + return ConfigBuilder().with_start_date(_NOW - timedelta(days=75)).with_account_id(_ACCOUNT_ID).with_client_secret(_CLIENT_SECRET) + + +def _catalog(sync_mode: SyncMode) -> ConfiguredAirbyteCatalog: + return CatalogBuilder().with_stream(_STREAM_NAME, sync_mode).build() + + +def _source(catalog: ConfiguredAirbyteCatalog, config: Dict[str, Any]) -> SourceStripe: + return SourceStripe(catalog, config) + + +def _an_event() -> RecordBuilder: + return create_record_builder( + find_template("events", __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _events_response() -> HttpResponseBuilder: + return create_response_builder( + find_template("events", __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _an_application_fee() -> RecordBuilder: + return create_record_builder( + find_template(_APPLICATION_FEES_TEMPLATE_NAME, __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _application_fees_response() -> HttpResponseBuilder: + return create_response_builder( + find_template(_APPLICATION_FEES_TEMPLATE_NAME, __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _a_refund() -> RecordBuilder: + return create_record_builder( + find_template(_REFUNDS_TEMPLATE_NAME, __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _refunds_response() -> HttpResponseBuilder: + return create_response_builder( + find_template(_REFUNDS_TEMPLATE_NAME, __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _given_application_fees_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.application_fees_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _application_fees_response().with_record(_an_application_fee()).build() # there needs to be a record in the parent stream for the child to be available + ) + + +def _given_events_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _events_response().build() + ) + + +def _as_dict(response_builder: HttpResponseBuilder) -> Dict[str, Any]: + return json.loads(response_builder.build().body) + + +def _read( + config_builder: ConfigBuilder, + sync_mode: SyncMode, + state: Optional[Dict[str, Any]] = None, + expecting_exception: bool = False +) -> EntrypointOutput: + catalog = _catalog(sync_mode) + config = config_builder.build() + return read(_source(catalog, config), config, catalog, state, expecting_exception) + + +def _assert_not_available(output: EntrypointOutput) -> None: + # right now, no stream statuses means stream unavailable + assert len(output.get_stream_statuses(_STREAM_NAME)) == 0 + + +@freezegun.freeze_time(_NOW.isoformat()) +class FullRefreshTest(TestCase): + @HttpMocker() + def test_given_one_page_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _application_fees_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response() + .with_record( + _an_application_fee() + .with_field( + _REFUNDS_FIELD, + _as_dict( + _refunds_response() + .with_record(_a_refund()) + .with_record(_a_refund()) + ) + ) + ) + .with_record( + _an_application_fee() + .with_field(_REFUNDS_FIELD, _as_dict(_refunds_response().with_record(_a_refund()))) + ).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_multiple_refunds_pages_when_read_then_query_pagination_on_child(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _application_fees_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response() + .with_record( + _an_application_fee() + .with_id("parent_id") + .with_field( + _REFUNDS_FIELD, + _as_dict( + _refunds_response() + .with_pagination() + .with_record(_a_refund().with_id("latest_refund_id")) + ) + ) + ).build(), + ) + http_mocker.get( + # we do not use slice boundaries here because: + # * there should be no duplicates parents (application fees) returned by the stripe API as it is using cursor pagination + # * it is implicitly lower bounder by the parent creation + # * the upper boundary is not configurable and is always + _application_fees_refunds_request("parent_id").with_limit(100).with_starting_after("latest_refund_id").build(), + _refunds_response().with_record(_a_refund()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_multiple_application_fees_pages_when_read_then_query_pagination_on_parent(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _application_fees_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response() + .with_pagination() + .with_record( + _an_application_fee() + .with_id("parent_id") + .with_field( + _REFUNDS_FIELD, + _as_dict( + _refunds_response() + .with_record(_a_refund()) + ) + ) + ).build(), + ) + http_mocker.get( + _application_fees_request().with_starting_after("parent_id").with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response() + .with_record( + _an_application_fee() + .with_field( + _REFUNDS_FIELD, + _as_dict( + _refunds_response() + .with_record(_a_refund()) + ) + ) + ).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_parent_stream_without_refund_when_read_then_stream_is_unavailable(self, http_mocker: HttpMocker) -> None: + # events stream is not validated as application fees is validated first + http_mocker.get( + _application_fees_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response().build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + _assert_not_available(output) + + @HttpMocker() + def test_given_slice_range_when_read_then_perform_multiple_requests(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=30) + slice_range = timedelta(days=20) + slice_datetime = start_date + slice_range + + _given_events_availability_check(http_mocker) + http_mocker.get( + _application_fees_request().with_created_gte(start_date).with_created_lte(slice_datetime).with_limit(100).build(), + _application_fees_response().with_record( + _an_application_fee() + .with_field(_REFUNDS_FIELD, _as_dict(_refunds_response().with_record(_a_refund()))) + ).build(), + ) + http_mocker.get( + _application_fees_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response().with_record( + _an_application_fee() + .with_field(_REFUNDS_FIELD, _as_dict(_refunds_response().with_record(_a_refund()))) + ).build(), + ) + + output = self._read(_config().with_start_date(start_date).with_slice_range_in_days(slice_range.days)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_slice_range_and_refunds_pagination_when_read_then_do_not_slice_child(self, http_mocker: HttpMocker) -> None: + """ + This means that if the user attempt to configure the slice range, it will only apply on the parent stream + """ + start_date = _NOW - timedelta(days=30) + slice_range = timedelta(days=20) + slice_datetime = start_date + slice_range + + _given_events_availability_check(http_mocker) + http_mocker.get( + StripeRequestBuilder.application_fees_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _application_fees_response().build() + ) # catching subsequent slicing request that we don't really care for this test + http_mocker.get( + _application_fees_request().with_created_gte(start_date).with_created_lte(slice_datetime).with_limit(100).build(), + _application_fees_response().with_record( + _an_application_fee() + .with_id("parent_id") + .with_field( + _REFUNDS_FIELD, + _as_dict( + _refunds_response() + .with_pagination() + .with_record(_a_refund().with_id("latest_refund_id")) + ) + ) + ).build(), + ) + http_mocker.get( + # slice range is not applied here + _application_fees_refunds_request("parent_id").with_limit(100).with_starting_after("latest_refund_id").build(), + _refunds_response().with_record(_a_refund()).build(), + ) + + self._read(_config().with_start_date(start_date).with_slice_range_in_days(slice_range.days)) + + # request matched http_mocker + + @HttpMocker() + def test_given_no_state_when_read_then_return_ignore_lookback(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _application_fees_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response().with_record(_an_application_fee()).build(), + ) + + self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + # request matched http_mocker + + @HttpMocker() + def test_given_one_page_when_read_then_cursor_field_is_set(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _application_fees_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response() + .with_record( + _an_application_fee() + .with_field( + _REFUNDS_FIELD, + _as_dict( + _refunds_response() + .with_record(_a_refund()) + ) + ) + ).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert output.records[0].record.data["updated"] == output.records[0].record.data["created"] + + @HttpMocker() + def test_given_http_status_401_when_read_then_system_error(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _application_fees_request().with_any_query_params().build(), + a_response_with_status(401), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_rate_limited_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _application_fees_request().with_any_query_params().build(), + [ + a_response_with_status(429), + _application_fees_response().with_record(_an_application_fee().with_field( + _REFUNDS_FIELD, + _as_dict( + _refunds_response() + .with_record(_a_refund()) + ) + )).build(), + ], + ) + output = self._read(_config().with_start_date(_A_START_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: + request = _application_fees_request().with_any_query_params().build() + http_mocker.get( + request, + a_response_with_status(500), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + def _read(self, config: ConfigBuilder, expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.full_refresh, expecting_exception=expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class IncrementalTest(TestCase): + + @HttpMocker() + def test_given_no_state_when_read_then_use_application_fees_endpoint(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + cursor_value = int(_A_START_DATE.timestamp()) + 1 + http_mocker.get( + _application_fees_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _application_fees_response().with_record( + _an_application_fee() + .with_field(_REFUNDS_FIELD, _as_dict(_refunds_response().with_record(_a_refund().with_cursor(cursor_value)))) + ).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE), _NO_STATE) + + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_when_read_then_query_events_using_types_and_state_value_plus_1(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=40) + state_datetime = _NOW - timedelta(days=5) + cursor_value = int(state_datetime.timestamp()) + 1 + + _given_application_fees_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record( + _an_event().with_cursor(cursor_value).with_field(_DATA_FIELD, _a_refund().build()) + ).build(), + ) + + output = self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_and_pagination_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_application_fees_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + state_datetime = _NOW - timedelta(days=5) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_pagination().with_record( + _an_event().with_id("last_record_id_from_first_page").with_field(_DATA_FIELD, _a_refund().build()) + ).build(), + ) + http_mocker.get( + _events_request().with_starting_after("last_record_id_from_first_page").with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_refund_event()).build(), + ) + + output = self._read( + _config(), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_state_and_small_slice_range_when_read_then_perform_multiple_queries(self, http_mocker: HttpMocker) -> None: + state_datetime = _NOW - timedelta(days=5) + slice_range = timedelta(days=3) + slice_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + slice_range + + _given_application_fees_availability_check(http_mocker) + _given_events_availability_check(http_mocker) # the availability check does not consider the state so we need to define a generic availability check + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(slice_datetime).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_refund_event()).build(), + ) + http_mocker.get( + _events_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_refund_event()).with_record(self._a_refund_event()).build(), + ) + + output = self._read( + _config().with_start_date(_NOW - timedelta(days=30)).with_slice_range_in_days(slice_range.days), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_state_earlier_than_30_days_when_read_then_query_events_using_types_and_event_lower_boundary(self, http_mocker: HttpMocker) -> None: + # this seems odd as we would miss some data between start_date and events_lower_boundary. In that case, we should hit the + # application fees endpoint + _given_application_fees_availability_check(http_mocker) + start_date = _NOW - timedelta(days=40) + state_value = _NOW - timedelta(days=39) + events_lower_boundary = _NOW - timedelta(days=30) + http_mocker.get( + _events_request().with_created_gte(events_lower_boundary).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_refund_event()).build(), + ) + + self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_value.timestamp())}).build(), + ) + + # request matched http_mocker + + def _a_refund_event(self) -> RecordBuilder: + return _an_event().with_field(_DATA_FIELD, _a_refund().build()) + + def _read(self, config: ConfigBuilder, state: Optional[Dict[str, Any]], expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.incremental, state, expecting_exception) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_authorizations.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_authorizations.py new file mode 100644 index 0000000000000..ab140559840f3 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_authorizations.py @@ -0,0 +1,374 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import json +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, Optional +from unittest import TestCase + +import freezegun +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +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 ConfiguredAirbyteCatalog, FailureType, SyncMode +from integration.config import ConfigBuilder +from integration.pagination import StripePaginationStrategy +from integration.request_builder import StripeRequestBuilder +from integration.response_builder import a_response_with_status +from source_stripe import SourceStripe + +_EVENT_TYPES = ["issuing_authorization.created", "issuing_authorization.request", "issuing_authorization.updated"] + +_DATA_FIELD = NestedPath(["data", "object"]) +_STREAM_NAME = "authorizations" +_ENDPOINT_TEMPLATE_NAME = "issuing_authorizations" +_NOW = datetime.now(timezone.utc) +_A_START_DATE = _NOW - timedelta(days=60) +_ACCOUNT_ID = "account_id" +_CLIENT_SECRET = "client_secret" +_NO_STATE = {} +_AVOIDING_INCLUSIVE_BOUNDARIES = timedelta(seconds=1) + + +def _authorizations_request() -> StripeRequestBuilder: + return StripeRequestBuilder.issuing_authorizations_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _events_request() -> StripeRequestBuilder: + return StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _config() -> ConfigBuilder: + return ConfigBuilder().with_start_date(_NOW - timedelta(days=75)).with_account_id(_ACCOUNT_ID).with_client_secret(_CLIENT_SECRET) + + +def _catalog(sync_mode: SyncMode) -> ConfiguredAirbyteCatalog: + return CatalogBuilder().with_stream(_STREAM_NAME, sync_mode).build() + + +def _source(catalog: ConfiguredAirbyteCatalog, config: Dict[str, Any]) -> SourceStripe: + return SourceStripe(catalog, config) + + +def _an_event() -> RecordBuilder: + return create_record_builder( + find_template("events", __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _events_response() -> HttpResponseBuilder: + return create_response_builder( + find_template("events", __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _an_authorization() -> RecordBuilder: + return create_record_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _authorizations_response() -> HttpResponseBuilder: + return create_response_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _given_authorizations_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.issuing_authorizations_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _authorizations_response().build() + ) + + +def _given_events_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _events_response().build() + ) + + +def _read( + config_builder: ConfigBuilder, + sync_mode: SyncMode, + state: Optional[Dict[str, Any]] = None, + expecting_exception: bool = False +) -> EntrypointOutput: + catalog = _catalog(sync_mode) + config = config_builder.build() + return read(_source(catalog, config), config, catalog, state, expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class FullRefreshTest(TestCase): + + @HttpMocker() + def test_given_one_page_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _authorizations_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _authorizations_response().with_record(_an_authorization()).with_record(_an_authorization()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_many_pages_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _authorizations_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _authorizations_response().with_pagination().with_record(_an_authorization().with_id("last_record_id_from_first_page")).build(), + ) + http_mocker.get( + _authorizations_request().with_starting_after("last_record_id_from_first_page").with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _authorizations_response().with_record(_an_authorization()).with_record(_an_authorization()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_no_state_when_read_then_return_ignore_lookback(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _authorizations_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _authorizations_response().with_record(_an_authorization()).build(), + ) + + self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + # request matched http_mocker + + @HttpMocker() + def test_when_read_then_add_cursor_field(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _authorizations_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _authorizations_response().with_record(_an_authorization()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + assert output.records[0].record.data["updated"] == output.records[0].record.data["created"] + + @HttpMocker() + def test_given_slice_range_when_read_then_perform_multiple_requests(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=30) + slice_range = timedelta(days=20) + slice_datetime = start_date + slice_range + + _given_events_availability_check(http_mocker) + http_mocker.get( + _authorizations_request().with_created_gte(start_date).with_created_lte(slice_datetime).with_limit(100).build(), + _authorizations_response().build(), + ) + http_mocker.get( + _authorizations_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).build(), + _authorizations_response().build(), + ) + + self._read(_config().with_start_date(start_date).with_slice_range_in_days(slice_range.days)) + + # request matched http_mocker + + @HttpMocker() + def test_given_http_status_400_when_read_then_stream_is_ignored(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _authorizations_request().with_any_query_params().build(), + a_response_with_status(400), + ) + output = self._read(_config()) + assert len(output.get_stream_statuses(_STREAM_NAME)) == 0 + + @HttpMocker() + def test_given_http_status_401_when_read_then_system_error(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _authorizations_request().with_any_query_params().build(), + a_response_with_status(401), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_rate_limited_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _authorizations_request().with_any_query_params().build(), + [ + a_response_with_status(429), + _authorizations_response().with_record(_an_authorization()).build(), + ], + ) + output = self._read(_config().with_start_date(_A_START_DATE)) + assert len(output.records) == 1 + + @HttpMocker() + def test_given_http_status_500_once_before_200_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _authorizations_request().with_any_query_params().build(), + [a_response_with_status(500), _authorizations_response().with_record(_an_authorization()).build()], + ) + output = self._read(_config()) + 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: + http_mocker.get( + _authorizations_request().with_any_query_params().build(), + a_response_with_status(500), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_small_slice_range_when_read_then_availability_check_performs_too_many_queries(self, http_mocker: HttpMocker) -> None: + # see https://github.com/airbytehq/airbyte/issues/33499 + events_requests = StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build() + http_mocker.get( + events_requests, + _events_response().build() # it is important that the event response does not have a record. This is not far fetched as this is what would happend 30 days before now + ) + http_mocker.get( + _authorizations_request().with_any_query_params().build(), + _authorizations_response().build(), + ) + + self._read(_config().with_start_date(_NOW - timedelta(days=60)).with_slice_range_in_days(1)) + + http_mocker.assert_number_of_calls(events_requests, 30) + + def _read(self, config: ConfigBuilder, expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.full_refresh, expecting_exception=expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class IncrementalTest(TestCase): + + @HttpMocker() + def test_given_no_state_when_read_then_use_authorizations_endpoint(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + cursor_value = int(_A_START_DATE.timestamp()) + 1 + http_mocker.get( + _authorizations_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _authorizations_response().with_record(_an_authorization().with_cursor(cursor_value)).build(), + ) + output = self._read(_config().with_start_date(_A_START_DATE), _NO_STATE) + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_when_read_then_query_events_using_types_and_state_value_plus_1(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=40) + state_datetime = _NOW - timedelta(days=5) + cursor_value = int(state_datetime.timestamp()) + 1 + + _given_authorizations_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record( + _an_event().with_cursor(cursor_value).with_field(_DATA_FIELD, _an_authorization().build()) + ).build(), + ) + + output = self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_and_pagination_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_authorizations_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + state_datetime = _NOW - timedelta(days=5) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_pagination().with_record( + _an_event().with_id("last_record_id_from_first_page").with_field(_DATA_FIELD, _an_authorization().build()) + ).build(), + ) + http_mocker.get( + _events_request().with_starting_after("last_record_id_from_first_page").with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_authorization_event()).build(), + ) + + output = self._read( + _config(), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_state_and_small_slice_range_when_read_then_perform_multiple_queries(self, http_mocker: HttpMocker) -> None: + state_datetime = _NOW - timedelta(days=5) + slice_range = timedelta(days=3) + slice_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + slice_range + + _given_authorizations_availability_check(http_mocker) + _given_events_availability_check(http_mocker) # the availability check does not consider the state so we need to define a generic availability check + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(slice_datetime).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_authorization_event()).build(), + ) + http_mocker.get( + _events_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_authorization_event()).with_record(self._an_authorization_event()).build(), + ) + + output = self._read( + _config().with_start_date(_NOW - timedelta(days=30)).with_slice_range_in_days(slice_range.days), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_state_earlier_than_30_days_when_read_then_query_events_using_types_and_event_lower_boundary(self, http_mocker: HttpMocker) -> None: + # this seems odd as we would miss some data between start_date and events_lower_boundary. In that case, we should hit the + # authorizations endpoint + _given_authorizations_availability_check(http_mocker) + start_date = _NOW - timedelta(days=40) + state_value = _NOW - timedelta(days=39) + events_lower_boundary = _NOW - timedelta(days=30) + http_mocker.get( + _events_request().with_created_gte(events_lower_boundary).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_authorization_event()).build(), + ) + + self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_value.timestamp())}).build(), + ) + + # request matched http_mocker + + def _an_authorization_event(self) -> RecordBuilder: + return _an_event().with_field(_DATA_FIELD, _an_authorization().build()) + + def _read(self, config: ConfigBuilder, state: Optional[Dict[str, Any]], expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.incremental, state, expecting_exception) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_bank_accounts.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_bank_accounts.py new file mode 100644 index 0000000000000..c0995a6c11b33 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_bank_accounts.py @@ -0,0 +1,563 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + + +import json +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, Optional +from unittest import TestCase + +import freezegun +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +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 ConfiguredAirbyteCatalog, FailureType, SyncMode +from integration.config import ConfigBuilder +from integration.pagination import StripePaginationStrategy +from integration.request_builder import StripeRequestBuilder +from integration.response_builder import a_response_with_status +from source_stripe import SourceStripe + +_EVENT_TYPES = ["customer.source.created", "customer.source.expiring", "customer.source.updated", "customer.source.deleted"] + +_DATA_FIELD = NestedPath(["data", "object"]) +_SOURCES_FIELD = FieldPath("sources") +_STREAM_NAME = "bank_accounts" +_CUSTOMERS_TEMPLATE_NAME = "customers_expand_data_source" +_BANK_ACCOUNTS_TEMPLATE_NAME = "bank_accounts" +_NOW = datetime.now(timezone.utc) +_A_START_DATE = _NOW - timedelta(days=60) +_ACCOUNT_ID = "account_id" +_CLIENT_SECRET = "client_secret" +# FIXME expand[] is not documented anymore in stripe API doc (see https://github.com/airbytehq/airbyte/issues/33714) +_EXPANDS = ["data.sources"] +_OBJECT = "bank_account" +_NOT_A_BANK_ACCOUNT = RecordBuilder({"object": "NOT a bank account"}, None, None) +_NO_STATE = {} +_AVOIDING_INCLUSIVE_BOUNDARIES = timedelta(seconds=1) + + +def _customers_request() -> StripeRequestBuilder: + return StripeRequestBuilder.customers_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _customers_bank_accounts_request(customer_id: str) -> StripeRequestBuilder: + return StripeRequestBuilder.customers_bank_accounts_endpoint(customer_id, _ACCOUNT_ID, _CLIENT_SECRET) + + +def _events_request() -> StripeRequestBuilder: + return StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _config() -> ConfigBuilder: + return ConfigBuilder().with_start_date(_NOW - timedelta(days=75)).with_account_id(_ACCOUNT_ID).with_client_secret(_CLIENT_SECRET) + + +def _catalog(sync_mode: SyncMode) -> ConfiguredAirbyteCatalog: + return CatalogBuilder().with_stream(_STREAM_NAME, sync_mode).build() + + +def _source(catalog: ConfiguredAirbyteCatalog, config: Dict[str, Any]) -> SourceStripe: + return SourceStripe(catalog, config) + + +def _an_event() -> RecordBuilder: + return create_record_builder( + find_template("events", __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _events_response() -> HttpResponseBuilder: + return create_response_builder( + find_template("events", __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _a_customer() -> RecordBuilder: + return create_record_builder( + find_template(_CUSTOMERS_TEMPLATE_NAME, __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _customers_response() -> HttpResponseBuilder: + return create_response_builder( + find_template(_CUSTOMERS_TEMPLATE_NAME, __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _a_bank_account() -> RecordBuilder: + return create_record_builder( + find_template(_BANK_ACCOUNTS_TEMPLATE_NAME, __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + ) + + +def _bank_accounts_response() -> HttpResponseBuilder: + return create_response_builder( + find_template(_BANK_ACCOUNTS_TEMPLATE_NAME, __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _given_customers_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.customers_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _customers_response().with_record(_a_customer()).build() # there needs to be a record in the parent stream for the child to be available + ) + + +def _given_events_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _events_response().build() + ) + + +def _as_dict(response_builder: HttpResponseBuilder) -> Dict[str, Any]: + return json.loads(response_builder.build().body) + + +def _read( + config_builder: ConfigBuilder, + sync_mode: SyncMode, + state: Optional[Dict[str, Any]] = None, + expecting_exception: bool = False +) -> EntrypointOutput: + catalog = _catalog(sync_mode) + config = config_builder.build() + return read(_source(catalog, config), config, catalog, state, expecting_exception) + + +def _assert_not_available(output: EntrypointOutput) -> None: + # right now, no stream statuses means stream unavailable + assert len(output.get_stream_statuses(_STREAM_NAME)) == 0 + + +@freezegun.freeze_time(_NOW.isoformat()) +class FullRefreshTest(TestCase): + @HttpMocker() + def test_given_one_page_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _customers_request().with_expands(_EXPANDS).with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _customers_response() + .with_record( + _a_customer() + .with_field( + _SOURCES_FIELD, + _as_dict( + _bank_accounts_response() + .with_record(_a_bank_account()) + .with_record(_a_bank_account()) + ) + ) + ) + .with_record( + _a_customer() + .with_field(_SOURCES_FIELD, _as_dict(_bank_accounts_response().with_record(_a_bank_account()))) + ).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_source_is_not_bank_account_when_read_then_filter_record(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _customers_request().with_expands(_EXPANDS).with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _customers_response() + .with_record( + _a_customer() + .with_field( + _SOURCES_FIELD, + _as_dict( + _bank_accounts_response() + .with_record(_NOT_A_BANK_ACCOUNT) + ) + ) + ).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 0 + + @HttpMocker() + def test_given_multiple_bank_accounts_pages_when_read_then_query_pagination_on_child(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _customers_request().with_expands(_EXPANDS).with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _customers_response() + .with_record( + _a_customer() + .with_id("parent_id") + .with_field( + _SOURCES_FIELD, + _as_dict( + _bank_accounts_response() + .with_pagination() + .with_record(_a_bank_account().with_id("latest_bank_account_id")) + ) + ) + ).build(), + ) + http_mocker.get( + # we do not use slice boundaries here because: + # * there should be no duplicates parents (application fees) returned by the stripe API as it is using cursor pagination + # * it is implicitly lower bounder by the parent creation + # * the upper boundary is not configurable and is always + _customers_bank_accounts_request("parent_id").with_limit(100).with_starting_after("latest_bank_account_id").build(), + _bank_accounts_response().with_record(_a_bank_account()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_multiple_customers_pages_when_read_then_query_pagination_on_parent(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _customers_request().with_expands(_EXPANDS).with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _customers_response() + .with_pagination() + .with_record( + _a_customer() + .with_id("parent_id") + .with_field( + _SOURCES_FIELD, + _as_dict( + _bank_accounts_response() + .with_record(_a_bank_account()) + ) + ) + ).build(), + ) + http_mocker.get( + _customers_request().with_expands(_EXPANDS).with_starting_after("parent_id").with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _customers_response() + .with_record( + _a_customer() + .with_field( + _SOURCES_FIELD, + _as_dict( + _bank_accounts_response() + .with_record(_a_bank_account()) + ) + ) + ).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_parent_stream_without_bank_accounts_when_read_then_stream_is_unavailable(self, http_mocker: HttpMocker) -> None: + # events stream is not validated as application fees is validated first + http_mocker.get( + _customers_request().with_expands(_EXPANDS).with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _customers_response().build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + _assert_not_available(output) + + @HttpMocker() + def test_given_slice_range_when_read_then_perform_multiple_requests(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=30) + slice_range = timedelta(days=20) + slice_datetime = start_date + slice_range + + _given_events_availability_check(http_mocker) + http_mocker.get( + _customers_request().with_expands(_EXPANDS).with_created_gte(start_date).with_created_lte(slice_datetime).with_limit(100).build(), + _customers_response().with_record( + _a_customer() + .with_field(_SOURCES_FIELD, _as_dict(_bank_accounts_response().with_record(_a_bank_account()))) + ).build(), + ) + http_mocker.get( + _customers_request().with_expands(_EXPANDS).with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).build(), + _customers_response().with_record( + _a_customer() + .with_field(_SOURCES_FIELD, _as_dict(_bank_accounts_response().with_record(_a_bank_account()))) + ).build(), + ) + + output = self._read(_config().with_start_date(start_date).with_slice_range_in_days(slice_range.days)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_slice_range_and_bank_accounts_pagination_when_read_then_do_not_slice_child(self, http_mocker: HttpMocker) -> None: + """ + This means that if the user attempt to configure the slice range, it will only apply on the parent stream + """ + start_date = _NOW - timedelta(days=30) + slice_range = timedelta(days=20) + slice_datetime = start_date + slice_range + + _given_events_availability_check(http_mocker) + http_mocker.get( + StripeRequestBuilder.customers_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _customers_response().build() + ) # catching subsequent slicing request that we don't really care for this test + http_mocker.get( + _customers_request().with_expands(_EXPANDS).with_created_gte(start_date).with_created_lte(slice_datetime).with_limit(100).build(), + _customers_response().with_record( + _a_customer() + .with_id("parent_id") + .with_field( + _SOURCES_FIELD, + _as_dict( + _bank_accounts_response() + .with_pagination() + .with_record(_a_bank_account().with_id("latest_bank_account_id")) + ) + ) + ).build(), + ) + http_mocker.get( + # slice range is not applied here + _customers_bank_accounts_request("parent_id").with_limit(100).with_starting_after("latest_bank_account_id").build(), + _bank_accounts_response().with_record(_a_bank_account()).build(), + ) + + self._read(_config().with_start_date(start_date).with_slice_range_in_days(slice_range.days)) + + # request matched http_mocker + + @HttpMocker() + def test_given_no_state_when_read_then_return_ignore_lookback(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _customers_request().with_expands(_EXPANDS).with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _customers_response().with_record(_a_customer()).build(), + ) + + self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + # request matched http_mocker + + @HttpMocker() + def test_given_one_page_when_read_then_cursor_field_is_set(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _customers_request().with_expands(_EXPANDS).with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _customers_response() + .with_record( + _a_customer() + .with_field( + _SOURCES_FIELD, + _as_dict( + _bank_accounts_response() + .with_record(_a_bank_account()) + ) + ) + ).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert output.records[0].record.data["updated"] == int(_NOW.timestamp()) + + @HttpMocker() + def test_given_http_status_401_when_read_then_system_error(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _customers_request().with_any_query_params().build(), + a_response_with_status(401), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_rate_limited_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _customers_request().with_any_query_params().build(), + [ + a_response_with_status(429), + _customers_response().with_record(_a_customer().with_field( + _SOURCES_FIELD, + _as_dict( + _bank_accounts_response() + .with_record(_a_bank_account()) + ) + )).build(), + ], + ) + output = self._read(_config().with_start_date(_A_START_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: + request = _customers_request().with_any_query_params().build() + http_mocker.get( + request, + a_response_with_status(500), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + def _read(self, config: ConfigBuilder, expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.full_refresh, expecting_exception=expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class IncrementalTest(TestCase): + + @HttpMocker() + def test_given_no_state_and_successful_sync_when_read_then_set_state_to_now(self, http_mocker: HttpMocker) -> None: + # If stripe takes some time to ingest the data, we should recommend to use a lookback window when syncing the bank_accounts stream + # to make sure that we don't lose data between the first and the second sync + _given_events_availability_check(http_mocker) + http_mocker.get( + _customers_request().with_expands(_EXPANDS).with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _customers_response().with_record( + _a_customer() + .with_field(_SOURCES_FIELD, _as_dict(_bank_accounts_response().with_record(_a_bank_account()))) + ).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE), _NO_STATE) + + assert output.most_recent_state == {_STREAM_NAME: {"updated": int(_NOW.timestamp())}} + + @HttpMocker() + def test_given_state_when_read_then_query_events_using_types_and_state_value_plus_1(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=40) + state_datetime = _NOW - timedelta(days=5) + cursor_value = int(state_datetime.timestamp()) + 1 + + _given_customers_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record( + _an_event().with_cursor(cursor_value).with_field(_DATA_FIELD, _a_bank_account().build()) + ).build(), + ) + + output = self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_and_pagination_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_customers_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + state_datetime = _NOW - timedelta(days=5) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_pagination().with_record( + _an_event().with_id("last_record_id_from_first_page").with_field(_DATA_FIELD, _a_bank_account().build()) + ).build(), + ) + http_mocker.get( + _events_request().with_starting_after("last_record_id_from_first_page").with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_bank_account_event()).build(), + ) + + output = self._read( + _config(), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_state_and_small_slice_range_when_read_then_perform_multiple_queries(self, http_mocker: HttpMocker) -> None: + state_datetime = _NOW - timedelta(days=5) + slice_range = timedelta(days=3) + slice_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + slice_range + + _given_customers_availability_check(http_mocker) + _given_events_availability_check(http_mocker) # the availability check does not consider the state so we need to define a generic availability check + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(slice_datetime).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_bank_account_event()).build(), + ) + http_mocker.get( + _events_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_bank_account_event()).with_record(self._a_bank_account_event()).build(), + ) + + output = self._read( + _config().with_start_date(_NOW - timedelta(days=30)).with_slice_range_in_days(slice_range.days), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_state_earlier_than_30_days_when_read_then_query_events_using_types_and_event_lower_boundary(self, http_mocker: HttpMocker) -> None: + # this seems odd as we would miss some data between start_date and events_lower_boundary. In that case, we should hit the + # customer endpoint + _given_customers_availability_check(http_mocker) + start_date = _NOW - timedelta(days=40) + state_value = _NOW - timedelta(days=39) + events_lower_boundary = _NOW - timedelta(days=30) + http_mocker.get( + _events_request().with_created_gte(events_lower_boundary).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_bank_account_event()).build(), + ) + + self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_value.timestamp())}).build(), + ) + + # request matched http_mocker + + @HttpMocker() + def test_given_source_is_not_bank_account_when_read_then_filter_record(self, http_mocker: HttpMocker) -> None: + _given_customers_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + state_datetime = _NOW - timedelta(days=5) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record( + _an_event().with_field(_DATA_FIELD, _NOT_A_BANK_ACCOUNT.build()) + ).build(), + ) + + output = self._read( + _config(), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 0 + + def _a_bank_account_event(self) -> RecordBuilder: + return _an_event().with_field(_DATA_FIELD, _a_bank_account().build()) + + def _read(self, config: ConfigBuilder, state: Optional[Dict[str, Any]], expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.incremental, state, expecting_exception) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_cards.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_cards.py new file mode 100644 index 0000000000000..573ba8824a9ec --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_cards.py @@ -0,0 +1,374 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import json +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, Optional +from unittest import TestCase + +import freezegun +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +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 ConfiguredAirbyteCatalog, FailureType, SyncMode +from integration.config import ConfigBuilder +from integration.pagination import StripePaginationStrategy +from integration.request_builder import StripeRequestBuilder +from integration.response_builder import a_response_with_status +from source_stripe import SourceStripe + +_EVENT_TYPES = ["issuing_card.created", "issuing_card.updated"] + +_DATA_FIELD = NestedPath(["data", "object"]) +_STREAM_NAME = "cards" +_ENDPOINT_TEMPLATE_NAME = "issuing_cards" +_NOW = datetime.now(timezone.utc) +_A_START_DATE = _NOW - timedelta(days=60) +_ACCOUNT_ID = "account_id" +_CLIENT_SECRET = "client_secret" +_NO_STATE = {} +_AVOIDING_INCLUSIVE_BOUNDARIES = timedelta(seconds=1) + + +def _cards_request() -> StripeRequestBuilder: + return StripeRequestBuilder.issuing_cards_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _events_request() -> StripeRequestBuilder: + return StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _config() -> ConfigBuilder: + return ConfigBuilder().with_start_date(_NOW - timedelta(days=75)).with_account_id(_ACCOUNT_ID).with_client_secret(_CLIENT_SECRET) + + +def _catalog(sync_mode: SyncMode) -> ConfiguredAirbyteCatalog: + return CatalogBuilder().with_stream(_STREAM_NAME, sync_mode).build() + + +def _source(catalog: ConfiguredAirbyteCatalog, config: Dict[str, Any]) -> SourceStripe: + return SourceStripe(catalog, config) + + +def _an_event() -> RecordBuilder: + return create_record_builder( + find_template("events", __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _events_response() -> HttpResponseBuilder: + return create_response_builder( + find_template("events", __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _a_card() -> RecordBuilder: + return create_record_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _cards_response() -> HttpResponseBuilder: + return create_response_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _given_cards_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.issuing_cards_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _cards_response().build() + ) + + +def _given_events_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _events_response().build() + ) + + +def _read( + config_builder: ConfigBuilder, + sync_mode: SyncMode, + state: Optional[Dict[str, Any]] = None, + expecting_exception: bool = False +) -> EntrypointOutput: + catalog = _catalog(sync_mode) + config = config_builder.build() + return read(_source(catalog, config), config, catalog, state, expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class FullRefreshTest(TestCase): + + @HttpMocker() + def test_given_one_page_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _cards_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _cards_response().with_record(_a_card()).with_record(_a_card()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_many_pages_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _cards_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _cards_response().with_pagination().with_record(_a_card().with_id("last_record_id_from_first_page")).build(), + ) + http_mocker.get( + _cards_request().with_starting_after("last_record_id_from_first_page").with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _cards_response().with_record(_a_card()).with_record(_a_card()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_no_state_when_read_then_return_ignore_lookback(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _cards_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _cards_response().with_record(_a_card()).build(), + ) + + self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + # request matched http_mocker + + @HttpMocker() + def test_when_read_then_add_cursor_field(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _cards_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _cards_response().with_record(_a_card()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + assert output.records[0].record.data["updated"] == output.records[0].record.data["created"] + + @HttpMocker() + def test_given_slice_range_when_read_then_perform_multiple_requests(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=30) + slice_range = timedelta(days=20) + slice_datetime = start_date + slice_range + + _given_events_availability_check(http_mocker) + http_mocker.get( + _cards_request().with_created_gte(start_date).with_created_lte(slice_datetime).with_limit(100).build(), + _cards_response().build(), + ) + http_mocker.get( + _cards_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).build(), + _cards_response().build(), + ) + + self._read(_config().with_start_date(start_date).with_slice_range_in_days(slice_range.days)) + + # request matched http_mocker + + @HttpMocker() + def test_given_http_status_400_when_read_then_stream_is_ignored(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _cards_request().with_any_query_params().build(), + a_response_with_status(400), + ) + output = self._read(_config()) + assert len(output.get_stream_statuses(_STREAM_NAME)) == 0 + + @HttpMocker() + def test_given_http_status_401_when_read_then_system_error(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _cards_request().with_any_query_params().build(), + a_response_with_status(401), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_rate_limited_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _cards_request().with_any_query_params().build(), + [ + a_response_with_status(429), + _cards_response().with_record(_a_card()).build(), + ], + ) + output = self._read(_config().with_start_date(_A_START_DATE)) + assert len(output.records) == 1 + + @HttpMocker() + def test_given_http_status_500_once_before_200_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _cards_request().with_any_query_params().build(), + [a_response_with_status(500), _cards_response().with_record(_a_card()).build()], + ) + output = self._read(_config()) + 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: + http_mocker.get( + _cards_request().with_any_query_params().build(), + a_response_with_status(500), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_small_slice_range_when_read_then_availability_check_performs_too_many_queries(self, http_mocker: HttpMocker) -> None: + # see https://github.com/airbytehq/airbyte/issues/33499 + events_requests = StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build() + http_mocker.get( + events_requests, + _events_response().build() # it is important that the event response does not have a record. This is not far fetched as this is what would happend 30 days before now + ) + http_mocker.get( + _cards_request().with_any_query_params().build(), + _cards_response().build(), + ) + + self._read(_config().with_start_date(_NOW - timedelta(days=60)).with_slice_range_in_days(1)) + + http_mocker.assert_number_of_calls(events_requests, 30) + + def _read(self, config: ConfigBuilder, expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.full_refresh, expecting_exception=expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class IncrementalTest(TestCase): + + @HttpMocker() + def test_given_no_state_when_read_then_use_cards_endpoint(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + cursor_value = int(_A_START_DATE.timestamp()) + 1 + http_mocker.get( + _cards_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _cards_response().with_record(_a_card().with_cursor(cursor_value)).build(), + ) + output = self._read(_config().with_start_date(_A_START_DATE), _NO_STATE) + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_when_read_then_query_events_using_types_and_state_value_plus_1(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=40) + state_datetime = _NOW - timedelta(days=5) + cursor_value = int(state_datetime.timestamp()) + 1 + + _given_cards_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record( + _an_event().with_cursor(cursor_value).with_field(_DATA_FIELD, _a_card().build()) + ).build(), + ) + + output = self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_and_pagination_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_cards_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + state_datetime = _NOW - timedelta(days=5) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_pagination().with_record( + _an_event().with_id("last_record_id_from_first_page").with_field(_DATA_FIELD, _a_card().build()) + ).build(), + ) + http_mocker.get( + _events_request().with_starting_after("last_record_id_from_first_page").with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_card_event()).build(), + ) + + output = self._read( + _config(), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_state_and_small_slice_range_when_read_then_perform_multiple_queries(self, http_mocker: HttpMocker) -> None: + state_datetime = _NOW - timedelta(days=5) + slice_range = timedelta(days=3) + slice_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + slice_range + + _given_cards_availability_check(http_mocker) + _given_events_availability_check(http_mocker) # the availability check does not consider the state so we need to define a generic availability check + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(slice_datetime).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_card_event()).build(), + ) + http_mocker.get( + _events_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_card_event()).with_record(self._a_card_event()).build(), + ) + + output = self._read( + _config().with_start_date(_NOW - timedelta(days=30)).with_slice_range_in_days(slice_range.days), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_state_earlier_than_30_days_when_read_then_query_events_using_types_and_event_lower_boundary(self, http_mocker: HttpMocker) -> None: + # this seems odd as we would miss some data between start_date and events_lower_boundary. In that case, we should hit the + # cards endpoint + _given_cards_availability_check(http_mocker) + start_date = _NOW - timedelta(days=40) + state_value = _NOW - timedelta(days=39) + events_lower_boundary = _NOW - timedelta(days=30) + http_mocker.get( + _events_request().with_created_gte(events_lower_boundary).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_card_event()).build(), + ) + + self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_value.timestamp())}).build(), + ) + + # request matched http_mocker + + def _a_card_event(self) -> RecordBuilder: + return _an_event().with_field(_DATA_FIELD, _a_card().build()) + + def _read(self, config: ConfigBuilder, state: Optional[Dict[str, Any]], expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.incremental, state, expecting_exception) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_early_fraud_warnings.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_early_fraud_warnings.py new file mode 100644 index 0000000000000..f4c2165c582ae --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_early_fraud_warnings.py @@ -0,0 +1,342 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import json +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, Optional +from unittest import TestCase + +import freezegun +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +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 ConfiguredAirbyteCatalog, FailureType, SyncMode +from integration.config import ConfigBuilder +from integration.pagination import StripePaginationStrategy +from integration.request_builder import StripeRequestBuilder +from integration.response_builder import a_response_with_status +from source_stripe import SourceStripe + +_EVENT_TYPES = ["radar.early_fraud_warning.created", "radar.early_fraud_warning.updated"] + +_DATA_FIELD = NestedPath(["data", "object"]) +_STREAM_NAME = "early_fraud_warnings" +_ENDPOINT_TEMPLATE_NAME = "radar_early_fraud_warnings" +_NOW = datetime.now(timezone.utc) +_A_START_DATE = _NOW - timedelta(days=60) +_ACCOUNT_ID = "account_id" +_CLIENT_SECRET = "client_secret" +_NO_STATE = {} +_AVOIDING_INCLUSIVE_BOUNDARIES = timedelta(seconds=1) + + +def _early_fraud_warnings_request() -> StripeRequestBuilder: + return StripeRequestBuilder.radar_early_fraud_warnings_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _events_request() -> StripeRequestBuilder: + return StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _config() -> ConfigBuilder: + return ConfigBuilder().with_start_date(_NOW - timedelta(days=75)).with_account_id(_ACCOUNT_ID).with_client_secret(_CLIENT_SECRET) + + +def _catalog(sync_mode: SyncMode) -> ConfiguredAirbyteCatalog: + return CatalogBuilder().with_stream(_STREAM_NAME, sync_mode).build() + + +def _source(catalog: ConfiguredAirbyteCatalog, config: Dict[str, Any]) -> SourceStripe: + return SourceStripe(catalog, config) + + +def _an_event() -> RecordBuilder: + return create_record_builder( + find_template("events", __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _events_response() -> HttpResponseBuilder: + return create_response_builder( + find_template("events", __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _an_early_fraud_warning() -> RecordBuilder: + return create_record_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _early_fraud_warnings_response() -> HttpResponseBuilder: + return create_response_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _given_early_fraud_warnings_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.radar_early_fraud_warnings_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _early_fraud_warnings_response().build() + ) + + +def _given_events_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _events_response().build() + ) + + +def _read( + config_builder: ConfigBuilder, + sync_mode: SyncMode, + state: Optional[Dict[str, Any]] = None, + expecting_exception: bool = False +) -> EntrypointOutput: + catalog = _catalog(sync_mode) + config = config_builder.build() + return read(_source(catalog, config), config, catalog, state, expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class FullRefreshTest(TestCase): + + @HttpMocker() + def test_given_one_page_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _early_fraud_warnings_request().with_limit(100).build(), + _early_fraud_warnings_response().with_record(_an_early_fraud_warning()).with_record(_an_early_fraud_warning()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_many_pages_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _early_fraud_warnings_request().with_limit(100).build(), + _early_fraud_warnings_response().with_pagination().with_record(_an_early_fraud_warning().with_id("last_record_id_from_first_page")).build(), + ) + http_mocker.get( + _early_fraud_warnings_request().with_starting_after("last_record_id_from_first_page").with_limit(100).build(), + _early_fraud_warnings_response().with_record(_an_early_fraud_warning()).with_record(_an_early_fraud_warning()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 3 + + @HttpMocker() + def test_when_read_then_add_cursor_field(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _early_fraud_warnings_request().with_limit(100).build(), + _early_fraud_warnings_response().with_record(_an_early_fraud_warning()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + assert output.records[0].record.data["updated"] == output.records[0].record.data["created"] + + @HttpMocker() + def test_given_http_status_400_when_read_then_stream_is_ignored(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _early_fraud_warnings_request().with_any_query_params().build(), + a_response_with_status(400), + ) + output = self._read(_config()) + assert len(output.get_stream_statuses(_STREAM_NAME)) == 0 + + @HttpMocker() + def test_given_http_status_401_when_read_then_system_error(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _early_fraud_warnings_request().with_any_query_params().build(), + a_response_with_status(401), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_rate_limited_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _early_fraud_warnings_request().with_any_query_params().build(), + [ + a_response_with_status(429), + _early_fraud_warnings_response().with_record(_an_early_fraud_warning()).build(), + ], + ) + output = self._read(_config().with_start_date(_A_START_DATE)) + assert len(output.records) == 1 + + @HttpMocker() + def test_given_http_status_500_once_before_200_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _early_fraud_warnings_request().with_any_query_params().build(), + [a_response_with_status(500), _early_fraud_warnings_response().with_record(_an_early_fraud_warning()).build()], + ) + output = self._read(_config()) + 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: + http_mocker.get( + _early_fraud_warnings_request().with_any_query_params().build(), + a_response_with_status(500), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_small_slice_range_when_read_then_availability_check_performs_too_many_queries(self, http_mocker: HttpMocker) -> None: + # see https://github.com/airbytehq/airbyte/issues/33499 + events_requests = StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build() + http_mocker.get( + events_requests, + _events_response().build() # it is important that the event response does not have a record. This is not far fetched as this is what would happend 30 days before now + ) + http_mocker.get( + _early_fraud_warnings_request().with_any_query_params().build(), + _early_fraud_warnings_response().build(), + ) + + self._read(_config().with_start_date(_NOW - timedelta(days=60)).with_slice_range_in_days(1)) + + http_mocker.assert_number_of_calls(events_requests, 30) + + def _read(self, config: ConfigBuilder, expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.full_refresh, expecting_exception=expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class IncrementalTest(TestCase): + + @HttpMocker() + def test_given_no_state_when_read_then_use_early_fraud_warnings_endpoint(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + cursor_value = int(_A_START_DATE.timestamp()) + 1 + http_mocker.get( + _early_fraud_warnings_request().with_limit(100).build(), + _early_fraud_warnings_response().with_record(_an_early_fraud_warning().with_cursor(cursor_value)).build(), + ) + output = self._read(_config().with_start_date(_A_START_DATE), _NO_STATE) + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_when_read_then_query_events_using_types_and_state_value_plus_1(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=40) + state_datetime = _NOW - timedelta(days=5) + cursor_value = int(state_datetime.timestamp()) + 1 + + _given_early_fraud_warnings_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record( + _an_event().with_cursor(cursor_value).with_field(_DATA_FIELD, _an_early_fraud_warning().build()) + ).build(), + ) + + output = self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_and_pagination_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_early_fraud_warnings_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + state_datetime = _NOW - timedelta(days=5) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_pagination().with_record( + _an_event().with_id("last_record_id_from_first_page").with_field(_DATA_FIELD, _an_early_fraud_warning().build()) + ).build(), + ) + http_mocker.get( + _events_request().with_starting_after("last_record_id_from_first_page").with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_early_fraud_warning_event()).build(), + ) + + output = self._read( + _config(), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_state_and_small_slice_range_when_read_then_perform_multiple_queries(self, http_mocker: HttpMocker) -> None: + state_datetime = _NOW - timedelta(days=5) + slice_range = timedelta(days=3) + slice_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + slice_range + + _given_early_fraud_warnings_availability_check(http_mocker) + _given_events_availability_check(http_mocker) # the availability check does not consider the state so we need to define a generic availability check + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(slice_datetime).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_early_fraud_warning_event()).build(), + ) + http_mocker.get( + _events_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_early_fraud_warning_event()).with_record(self._an_early_fraud_warning_event()).build(), + ) + + output = self._read( + _config().with_start_date(_NOW - timedelta(days=30)).with_slice_range_in_days(slice_range.days), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_state_earlier_than_30_days_when_read_then_query_events_using_types_and_event_lower_boundary(self, http_mocker: HttpMocker) -> None: + # this seems odd as we would miss some data between start_date and events_lower_boundary. In that case, we should hit the + # radar/early_fraud_warnings endpoint + _given_early_fraud_warnings_availability_check(http_mocker) + start_date = _NOW - timedelta(days=40) + state_value = _NOW - timedelta(days=39) + events_lower_boundary = _NOW - timedelta(days=30) + http_mocker.get( + _events_request().with_created_gte(events_lower_boundary).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_early_fraud_warning_event()).build(), + ) + + self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_value.timestamp())}).build(), + ) + + # request matched http_mocker + + def _an_early_fraud_warning_event(self) -> RecordBuilder: + return _an_event().with_field(_DATA_FIELD, _an_early_fraud_warning().build()) + + def _read(self, config: ConfigBuilder, state: Optional[Dict[str, Any]], expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.incremental, state, expecting_exception) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_events.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_events.py new file mode 100644 index 0000000000000..c93bc691a96e3 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_events.py @@ -0,0 +1,272 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import json +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, Optional +from unittest import TestCase + +import freezegun +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read +from airbyte_cdk.test.mock_http import HttpMocker, HttpResponse +from airbyte_cdk.test.mock_http.response_builder import ( + FieldPath, + HttpResponseBuilder, + RecordBuilder, + create_record_builder, + create_response_builder, + find_template, +) +from airbyte_cdk.test.state_builder import StateBuilder +from airbyte_protocol.models import ConfiguredAirbyteCatalog, FailureType, SyncMode +from integration.config import ConfigBuilder +from integration.pagination import StripePaginationStrategy +from integration.request_builder import StripeRequestBuilder +from integration.response_builder import a_response_with_status +from source_stripe import SourceStripe + +_STREAM_NAME = "events" +_NOW = datetime.now(timezone.utc) +_A_START_DATE = _NOW - timedelta(days=60) +_ACCOUNT_ID = "account_id" +_CLIENT_SECRET = "client_secret" +_NO_STATE = {} +_AVOIDING_INCLUSIVE_BOUNDARIES = timedelta(seconds=1) +_SECOND_REQUEST = timedelta(seconds=1) +_THIRD_REQUEST = timedelta(seconds=2) + + +def _a_request() -> StripeRequestBuilder: + return StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _config() -> ConfigBuilder: + return ConfigBuilder().with_start_date(_NOW - timedelta(days=73)).with_account_id(_ACCOUNT_ID).with_client_secret(_CLIENT_SECRET) + + +def _catalog(sync_mode: SyncMode) -> ConfiguredAirbyteCatalog: + return CatalogBuilder().with_stream(_STREAM_NAME, sync_mode).build() + + +def _source(catalog: ConfiguredAirbyteCatalog, config: Dict[str, Any]) -> SourceStripe: + return SourceStripe(catalog, config) + + +def _a_record() -> RecordBuilder: + return create_record_builder( + find_template("events", __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _a_response() -> HttpResponseBuilder: + return create_response_builder(find_template("events", __file__), FieldPath("data"), pagination_strategy=StripePaginationStrategy()) + + +def _read( + config_builder: ConfigBuilder, + sync_mode: SyncMode, + state: Optional[Dict[str, Any]] = None, + expecting_exception: bool = False +) -> EntrypointOutput: + catalog = _catalog(sync_mode) + config = config_builder.build() + return read(_source(catalog, config), config, catalog, state, expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class FullRefreshTest(TestCase): + + @HttpMocker() + def test_given_one_page_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _a_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _a_response().with_record(_a_record()).with_record(_a_record()).build(), + ) + output = self._read(_config().with_start_date(_A_START_DATE)) + assert len(output.records) == 2 + + @HttpMocker() + def test_given_many_pages_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _a_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _a_response().with_pagination().with_record(_a_record().with_id("last_record_id_from_first_page")).build(), + ) + http_mocker.get( + _a_request().with_starting_after("last_record_id_from_first_page").with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _a_response().with_record(_a_record()).with_record(_a_record()).build(), + ) + output = self._read(_config().with_start_date(_A_START_DATE)) + assert len(output.records) == 3 + + @HttpMocker() + def test_given_start_date_before_30_days_stripe_limit_and_slice_range_when_read_then_perform_request_before_30_days(self, http_mocker: HttpMocker) -> None: + """ + This case is special because the source queries for a time range that is before 30 days. That being said as of 2023-12-13, the API + mentions that "We only guarantee access to events through the Retrieve Event API for 30 days." (see + https://stripe.com/docs/api/events) + """ + start_date = _NOW - timedelta(days=61) + slice_range = timedelta(days=30) + slice_datetime = start_date + slice_range + http_mocker.get( # this first request has both gte and lte before 30 days even though we know there should not be records returned + _a_request().with_created_gte(start_date).with_created_lte(slice_datetime).with_limit(100).build(), + _a_response().build(), + ) + http_mocker.get( + _a_request().with_created_gte(slice_datetime + _SECOND_REQUEST).with_created_lte(slice_datetime + slice_range + _SECOND_REQUEST).with_limit(100).build(), + _a_response().build(), + ) + http_mocker.get( + _a_request().with_created_gte(slice_datetime + slice_range + _THIRD_REQUEST).with_created_lte(_NOW).with_limit(100).build(), + _a_response().build(), + ) + + self._read(_config().with_start_date(start_date).with_slice_range_in_days(slice_range.days)) + + # request matched http_mocker + + @HttpMocker() + def test_given_lookback_window_when_read_then_request_before_start_date(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=30) + lookback_window = timedelta(days=10) + http_mocker.get( + _a_request().with_created_gte(start_date - lookback_window).with_created_lte(_NOW).with_limit(100).build(), + _a_response().build(), + ) + + self._read(_config().with_start_date(start_date).with_lookback_window_in_days(lookback_window.days)) + + # request matched http_mocker + + @HttpMocker() + def test_given_slice_range_when_read_then_perform_multiple_requests(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=30) + slice_range = timedelta(days=20) + slice_datetime = start_date + slice_range + http_mocker.get( + _a_request().with_created_gte(start_date).with_created_lte(slice_datetime).with_limit(100).build(), + _a_response().build(), + ) + http_mocker.get( + _a_request().with_created_gte(slice_datetime + _SECOND_REQUEST).with_created_lte(_NOW).with_limit(100).build(), + _a_response().build(), + ) + + self._read(_config().with_start_date(start_date).with_slice_range_in_days(slice_range.days)) + + @HttpMocker() + def test_given_http_status_400_when_read_then_stream_is_ignored(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _a_request().with_any_query_params().build(), + a_response_with_status(400), + ) + output = self._read(_config()) + assert len(output.get_stream_statuses(_STREAM_NAME)) == 0 + + @HttpMocker() + def test_given_http_status_401_when_read_then_stream_is_incomplete(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _a_request().with_any_query_params().build(), + a_response_with_status(401), + ) + output = self._read(_config().with_start_date(_A_START_DATE), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_rate_limited_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _a_request().with_any_query_params().build(), + [ + a_response_with_status(429), + _a_response().with_record(_a_record()).build(), + ], + ) + output = self._read(_config().with_start_date(_A_START_DATE)) + assert len(output.records) == 1 + + @HttpMocker() + def test_given_http_status_500_once_before_200_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _a_request().with_any_query_params().build(), + [a_response_with_status(500), _a_response().with_record(_a_record()).build()], + ) + output = self._read(_config()) + 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: + http_mocker.get( + _a_request().with_any_query_params().build(), + a_response_with_status(500), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_when_read_then_validate_availability_for_full_refresh_and_incremental(self, http_mocker: HttpMocker) -> None: + request = _a_request().with_any_query_params().build() + http_mocker.get( + request, + _a_response().build(), + ) + self._read(_config().with_start_date(_A_START_DATE)) + http_mocker.assert_number_of_calls(request, 3) # one call for full_refresh availability, one call for incremental availability and one call for the actual read + + def _read(self, config: ConfigBuilder, expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.full_refresh, expecting_exception=expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class IncrementalTest(TestCase): + + @HttpMocker() + def test_given_no_initial_state_when_read_then_return_state_based_on_cursor_field(self, http_mocker: HttpMocker) -> None: + cursor_value = int(_A_START_DATE.timestamp()) + 1 + http_mocker.get( + _a_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _a_response().with_record(_a_record().with_cursor(cursor_value)).build(), + ) + output = self._read(_config().with_start_date(_A_START_DATE), _NO_STATE) + assert output.most_recent_state == {"events": {"created": cursor_value}} + + @HttpMocker() + def test_given_state_when_read_then_use_state_for_query_params(self, http_mocker: HttpMocker) -> None: + state_value = _A_START_DATE + timedelta(seconds=1) + availability_check_requests = _a_request().with_any_query_params().build() + http_mocker.get( + availability_check_requests, + _a_response().with_record(_a_record()).build(), + ) + http_mocker.get( + _a_request().with_created_gte(state_value + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).build(), + _a_response().with_record(_a_record()).build(), + ) + + self._read( + _config().with_start_date(_A_START_DATE), + StateBuilder().with_stream_state("events", {"created": int(state_value.timestamp())}).build() + ) + + # request matched http_mocker + + @HttpMocker() + def test_given_state_more_recent_than_cursor_when_read_then_return_state_based_on_cursor_field(self, http_mocker: HttpMocker) -> None: + cursor_value = int(_A_START_DATE.timestamp()) + 1 + more_recent_than_record_cursor = int(_NOW.timestamp()) - 1 + http_mocker.get( + _a_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _a_response().with_record(_a_record().with_cursor(cursor_value)).build(), + ) + + output = self._read( + _config().with_start_date(_A_START_DATE), + StateBuilder().with_stream_state("events", {"created": more_recent_than_record_cursor}).build() + ) + + assert output.most_recent_state == {"events": {"created": more_recent_than_record_cursor}} + + def _read(self, config: ConfigBuilder, state: Optional[Dict[str, Any]], expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.incremental, state, expecting_exception) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_external_account_bank_accounts.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_external_account_bank_accounts.py new file mode 100644 index 0000000000000..f1c5804173c1d --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_external_account_bank_accounts.py @@ -0,0 +1,361 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import json +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, Optional +from unittest import TestCase + +import freezegun +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +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 ConfiguredAirbyteCatalog, FailureType, SyncMode +from integration.config import ConfigBuilder +from integration.pagination import StripePaginationStrategy +from integration.request_builder import StripeRequestBuilder +from integration.response_builder import a_response_with_status +from source_stripe import SourceStripe + +_EVENT_TYPES = ["account.external_account.created", "account.external_account.updated", "account.external_account.deleted"] + +_DATA_FIELD = NestedPath(["data", "object"]) +_OBJECT = "bank_account" +_STREAM_NAME = "external_account_bank_accounts" +_ENDPOINT_TEMPLATE_NAME = "external_bank_accounts" +_NOW = datetime.now(timezone.utc) +_A_START_DATE = _NOW - timedelta(days=60) +_ACCOUNT_ID = "account_id" +_CLIENT_SECRET = "client_secret" +_NO_STATE = {} +_AVOIDING_INCLUSIVE_BOUNDARIES = timedelta(seconds=1) + + +def _external_accounts_request() -> StripeRequestBuilder: + return StripeRequestBuilder.external_accounts_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _events_request() -> StripeRequestBuilder: + return StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _config() -> ConfigBuilder: + return ConfigBuilder().with_start_date(_NOW - timedelta(days=75)).with_account_id(_ACCOUNT_ID).with_client_secret(_CLIENT_SECRET) + + +def _catalog(sync_mode: SyncMode) -> ConfiguredAirbyteCatalog: + return CatalogBuilder().with_stream(_STREAM_NAME, sync_mode).build() + + +def _source(catalog: ConfiguredAirbyteCatalog, config: Dict[str, Any]) -> SourceStripe: + return SourceStripe(catalog, config) + + +def _an_event() -> RecordBuilder: + return create_record_builder( + find_template("events", __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _events_response() -> HttpResponseBuilder: + return create_response_builder( + find_template("events", __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _an_external_bank_account() -> RecordBuilder: + return create_record_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + ) + + +def _external_bank_accounts_response() -> HttpResponseBuilder: + return create_response_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _given_external_accounts_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.external_accounts_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _external_bank_accounts_response().build() + ) + + +def _given_events_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _events_response().build() + ) + + +def _read( + config_builder: ConfigBuilder, + sync_mode: SyncMode, + state: Optional[Dict[str, Any]] = None, + expecting_exception: bool = False +) -> EntrypointOutput: + catalog = _catalog(sync_mode) + config = config_builder.build() + return read(_source(catalog, config), config, catalog, state, expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class FullRefreshTest(TestCase): + + @HttpMocker() + def test_given_one_page_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _external_accounts_request().with_object(_OBJECT).with_limit(100).build(), + _external_bank_accounts_response().with_record(_an_external_bank_account()).with_record(_an_external_bank_account()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_many_pages_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _external_accounts_request().with_object(_OBJECT).with_limit(100).build(), + _external_bank_accounts_response().with_pagination().with_record(_an_external_bank_account().with_id("last_record_id_from_first_page")).build(), + ) + http_mocker.get( + _external_accounts_request().with_starting_after("last_record_id_from_first_page").with_object(_OBJECT).with_limit(100).build(), + _external_bank_accounts_response().with_record(_an_external_bank_account()).with_record(_an_external_bank_account()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 3 + + @HttpMocker() + def test_when_read_then_add_cursor_field(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _external_accounts_request().with_object(_OBJECT).with_limit(100).build(), + _external_bank_accounts_response().with_record(_an_external_bank_account()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + assert output.records[0].record.data["updated"] == int(_NOW.timestamp()) + + @HttpMocker() + def test_given_http_status_400_when_read_then_stream_is_ignored(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _external_accounts_request().with_any_query_params().build(), + a_response_with_status(400), + ) + output = self._read(_config()) + assert len(output.get_stream_statuses(_STREAM_NAME)) == 0 + + @HttpMocker() + def test_given_http_status_401_when_read_then_system_error(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _external_accounts_request().with_any_query_params().build(), + a_response_with_status(401), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_rate_limited_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _external_accounts_request().with_any_query_params().build(), + [ + a_response_with_status(429), + _external_bank_accounts_response().with_record(_an_external_bank_account()).build(), + ], + ) + output = self._read(_config().with_start_date(_A_START_DATE)) + assert len(output.records) == 1 + + @HttpMocker() + def test_given_http_status_500_once_before_200_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _external_accounts_request().with_any_query_params().build(), + [a_response_with_status(500), _external_bank_accounts_response().with_record(_an_external_bank_account()).build()], + ) + output = self._read(_config()) + 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: + http_mocker.get( + _external_accounts_request().with_any_query_params().build(), + a_response_with_status(500), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_small_slice_range_when_read_then_availability_check_performs_too_many_queries(self, http_mocker: HttpMocker) -> None: + # see https://github.com/airbytehq/airbyte/issues/33499 + events_requests = StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build() + http_mocker.get( + events_requests, + _events_response().build() # it is important that the event response does not have a record. This is not far fetched as this is what would happend 30 days before now + ) + http_mocker.get( + _external_accounts_request().with_any_query_params().build(), + _external_bank_accounts_response().build(), + ) + + self._read(_config().with_start_date(_NOW - timedelta(days=60)).with_slice_range_in_days(1)) + + http_mocker.assert_number_of_calls(events_requests, 30) + + def _read(self, config: ConfigBuilder, expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.full_refresh, expecting_exception=expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class IncrementalTest(TestCase): + + @HttpMocker() + def test_given_no_state_when_read_then_use_external_accounts_endpoint(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _external_accounts_request().with_object(_OBJECT).with_limit(100).build(), + _external_bank_accounts_response().with_record(_an_external_bank_account()).build(), + ) + output = self._read(_config().with_start_date(_A_START_DATE), _NO_STATE) + assert output.most_recent_state == {_STREAM_NAME: {"updated": int(_NOW.timestamp())}} + + @HttpMocker() + def test_given_state_when_read_then_query_events_using_types_and_state_value_plus_1(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=40) + state_datetime = _NOW - timedelta(days=5) + cursor_value = int(state_datetime.timestamp()) + 1 + + _given_external_accounts_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record( + _an_event().with_cursor(cursor_value).with_field(_DATA_FIELD, _an_external_bank_account().build()) + ).build(), + ) + + output = self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_object_is_not_back_account_when_read_then_filter_out(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=40) + state_datetime = _NOW - timedelta(days=5) + + _given_external_accounts_availability_check(http_mocker) + http_mocker.get( + StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _events_response().with_record( + _an_event().with_field(_DATA_FIELD, {"object": "not a bank account"}) + ).build(), + ) + + output = self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 0 + + @HttpMocker() + def test_given_state_and_pagination_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_external_accounts_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + state_datetime = _NOW - timedelta(days=5) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_pagination().with_record( + _an_event().with_id("last_record_id_from_first_page").with_field(_DATA_FIELD, _an_external_bank_account().build()) + ).build(), + ) + http_mocker.get( + _events_request().with_starting_after("last_record_id_from_first_page").with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_external_account_event()).build(), + ) + + output = self._read( + _config(), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_state_and_small_slice_range_when_read_then_perform_multiple_queries(self, http_mocker: HttpMocker) -> None: + state_datetime = _NOW - timedelta(days=5) + slice_range = timedelta(days=3) + slice_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + slice_range + + _given_external_accounts_availability_check(http_mocker) + _given_events_availability_check(http_mocker) # the availability check does not consider the state so we need to define a generic availability check + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(slice_datetime).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_external_account_event()).build(), + ) + http_mocker.get( + _events_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_external_account_event()).with_record(self._an_external_account_event()).build(), + ) + + output = self._read( + _config().with_start_date(_NOW - timedelta(days=30)).with_slice_range_in_days(slice_range.days), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_state_earlier_than_30_days_when_read_then_query_events_using_types_and_event_lower_boundary(self, http_mocker: HttpMocker) -> None: + # this seems odd as we would miss some data between start_date and events_lower_boundary. In that case, we should hit the + # external_accounts endpoint + _given_external_accounts_availability_check(http_mocker) + start_date = _NOW - timedelta(days=40) + state_value = _NOW - timedelta(days=39) + events_lower_boundary = _NOW - timedelta(days=30) + http_mocker.get( + _events_request().with_created_gte(events_lower_boundary).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_external_account_event()).build(), + ) + + self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_value.timestamp())}).build(), + ) + + # request matched http_mocker + + def _an_external_account_event(self) -> RecordBuilder: + return _an_event().with_field(_DATA_FIELD, _an_external_bank_account().build()) + + def _read(self, config: ConfigBuilder, state: Optional[Dict[str, Any]], expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.incremental, state, expecting_exception) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_external_account_cards.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_external_account_cards.py new file mode 100644 index 0000000000000..705faf8530a09 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_external_account_cards.py @@ -0,0 +1,366 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import json +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, Optional +from unittest import TestCase + +import freezegun +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +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 ConfiguredAirbyteCatalog, FailureType, SyncMode +from integration.config import ConfigBuilder +from integration.pagination import StripePaginationStrategy +from integration.request_builder import StripeRequestBuilder +from integration.response_builder import a_response_with_status +from source_stripe import SourceStripe + +_EVENT_TYPES = ["account.external_account.created", "account.external_account.updated", "account.external_account.deleted"] + +_DATA_FIELD = NestedPath(["data", "object"]) +_OBJECT = "card" +_STREAM_NAME = "external_account_cards" +_ENDPOINT_TEMPLATE_NAME = "external_account_cards" +_NOW = datetime.now(timezone.utc) +_A_START_DATE = _NOW - timedelta(days=60) +_ACCOUNT_ID = "account_id" +_CLIENT_SECRET = "client_secret" +_NO_STATE = {} +_AVOIDING_INCLUSIVE_BOUNDARIES = timedelta(seconds=1) + + +def _external_accounts_request() -> StripeRequestBuilder: + return StripeRequestBuilder.external_accounts_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _events_request() -> StripeRequestBuilder: + return StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _config() -> ConfigBuilder: + return ConfigBuilder().with_start_date(_NOW - timedelta(days=75)).with_account_id(_ACCOUNT_ID).with_client_secret(_CLIENT_SECRET) + + +def _catalog(sync_mode: SyncMode) -> ConfiguredAirbyteCatalog: + return CatalogBuilder().with_stream(_STREAM_NAME, sync_mode).build() + + +def _source(catalog: ConfiguredAirbyteCatalog, config: Dict[str, Any]) -> SourceStripe: + return SourceStripe(catalog, config) + + +def _an_event() -> RecordBuilder: + return create_record_builder( + find_template("events", __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _events_response() -> HttpResponseBuilder: + return create_response_builder( + find_template("events", __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _an_external_account_card() -> RecordBuilder: + return create_record_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + ) + + +def _external_accounts_card_response() -> HttpResponseBuilder: + """ + WARNING: this response will not fully match the template as external accounts card are queried by ID and the field "url" is not updated + to match that (it is currently hardcoded to "/v1/accounts/acct_1032D82eZvKYlo2C/external_accounts"). As this has no impact on the + tests, we will leave it as is for now. + """ + return create_response_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _given_external_accounts_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.external_accounts_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _external_accounts_card_response().build() + ) + + +def _given_events_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _events_response().build() + ) + + +def _read( + config_builder: ConfigBuilder, + sync_mode: SyncMode, + state: Optional[Dict[str, Any]] = None, + expecting_exception: bool = False +) -> EntrypointOutput: + catalog = _catalog(sync_mode) + config = config_builder.build() + return read(_source(catalog, config), config, catalog, state, expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class FullRefreshTest(TestCase): + + @HttpMocker() + def test_given_one_page_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _external_accounts_request().with_object(_OBJECT).with_limit(100).build(), + _external_accounts_card_response().with_record(_an_external_account_card()).with_record(_an_external_account_card()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_many_pages_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _external_accounts_request().with_object(_OBJECT).with_limit(100).build(), + _external_accounts_card_response().with_pagination().with_record(_an_external_account_card().with_id("last_record_id_from_first_page")).build(), + ) + http_mocker.get( + _external_accounts_request().with_starting_after("last_record_id_from_first_page").with_object(_OBJECT).with_limit(100).build(), + _external_accounts_card_response().with_record(_an_external_account_card()).with_record(_an_external_account_card()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 3 + + @HttpMocker() + def test_when_read_then_add_cursor_field(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _external_accounts_request().with_object(_OBJECT).with_limit(100).build(), + _external_accounts_card_response().with_record(_an_external_account_card()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + assert output.records[0].record.data["updated"] == int(_NOW.timestamp()) + + @HttpMocker() + def test_given_http_status_400_when_read_then_stream_is_ignored(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _external_accounts_request().with_any_query_params().build(), + a_response_with_status(400), + ) + output = self._read(_config()) + assert len(output.get_stream_statuses(_STREAM_NAME)) == 0 + + @HttpMocker() + def test_given_http_status_401_when_read_then_system_error(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _external_accounts_request().with_any_query_params().build(), + a_response_with_status(401), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_rate_limited_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _external_accounts_request().with_any_query_params().build(), + [ + a_response_with_status(429), + _external_accounts_card_response().with_record(_an_external_account_card()).build(), + ], + ) + output = self._read(_config().with_start_date(_A_START_DATE)) + assert len(output.records) == 1 + + @HttpMocker() + def test_given_http_status_500_once_before_200_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _external_accounts_request().with_any_query_params().build(), + [a_response_with_status(500), _external_accounts_card_response().with_record(_an_external_account_card()).build()], + ) + output = self._read(_config()) + 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: + http_mocker.get( + _external_accounts_request().with_any_query_params().build(), + a_response_with_status(500), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_small_slice_range_when_read_then_availability_check_performs_too_many_queries(self, http_mocker: HttpMocker) -> None: + # see https://github.com/airbytehq/airbyte/issues/33499 + events_requests = StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build() + http_mocker.get( + events_requests, + _events_response().build() # it is important that the event response does not have a record. This is not far fetched as this is what would happend 30 days before now + ) + http_mocker.get( + _external_accounts_request().with_any_query_params().build(), + _external_accounts_card_response().build(), + ) + + self._read(_config().with_start_date(_NOW - timedelta(days=60)).with_slice_range_in_days(1)) + + http_mocker.assert_number_of_calls(events_requests, 30) + + def _read(self, config: ConfigBuilder, expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.full_refresh, expecting_exception=expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class IncrementalTest(TestCase): + + @HttpMocker() + def test_given_no_state_when_read_then_use_external_accounts_endpoint(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _external_accounts_request().with_object(_OBJECT).with_limit(100).build(), + _external_accounts_card_response().with_record(_an_external_account_card()).build(), + ) + output = self._read(_config().with_start_date(_A_START_DATE), _NO_STATE) + assert output.most_recent_state == {_STREAM_NAME: {"updated": int(_NOW.timestamp())}} + + @HttpMocker() + def test_given_state_when_read_then_query_events_using_types_and_state_value_plus_1(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=40) + state_datetime = _NOW - timedelta(days=5) + cursor_value = int(state_datetime.timestamp()) + 1 + + _given_external_accounts_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record( + _an_event().with_cursor(cursor_value).with_field(_DATA_FIELD, _an_external_account_card().build()) + ).build(), + ) + + output = self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_object_is_not_back_account_when_read_then_filter_out(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=40) + state_datetime = _NOW - timedelta(days=5) + + _given_external_accounts_availability_check(http_mocker) + http_mocker.get( + StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _events_response().with_record( + _an_event().with_field(_DATA_FIELD, {"object": "not a card"}) + ).build(), + ) + + output = self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 0 + + @HttpMocker() + def test_given_state_and_pagination_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_external_accounts_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + state_datetime = _NOW - timedelta(days=5) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_pagination().with_record( + _an_event().with_id("last_record_id_from_first_page").with_field(_DATA_FIELD, _an_external_account_card().build()) + ).build(), + ) + http_mocker.get( + _events_request().with_starting_after("last_record_id_from_first_page").with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_external_account_event()).build(), + ) + + output = self._read( + _config(), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_state_and_small_slice_range_when_read_then_perform_multiple_queries(self, http_mocker: HttpMocker) -> None: + state_datetime = _NOW - timedelta(days=5) + slice_range = timedelta(days=3) + slice_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + slice_range + + _given_external_accounts_availability_check(http_mocker) + _given_events_availability_check(http_mocker) # the availability check does not consider the state so we need to define a generic availability check + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(slice_datetime).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_external_account_event()).build(), + ) + http_mocker.get( + _events_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_external_account_event()).with_record(self._an_external_account_event()).build(), + ) + + output = self._read( + _config().with_start_date(_NOW - timedelta(days=30)).with_slice_range_in_days(slice_range.days), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_state_earlier_than_30_days_when_read_then_query_events_using_types_and_event_lower_boundary(self, http_mocker: HttpMocker) -> None: + # this seems odd as we would miss some data between start_date and events_lower_boundary. In that case, we should hit the + # external_accounts endpoint + _given_external_accounts_availability_check(http_mocker) + start_date = _NOW - timedelta(days=40) + state_value = _NOW - timedelta(days=39) + events_lower_boundary = _NOW - timedelta(days=30) + http_mocker.get( + _events_request().with_created_gte(events_lower_boundary).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._an_external_account_event()).build(), + ) + + self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_value.timestamp())}).build(), + ) + + # request matched http_mocker + + def _an_external_account_event(self) -> RecordBuilder: + return _an_event().with_field(_DATA_FIELD, _an_external_account_card().build()) + + def _read(self, config: ConfigBuilder, state: Optional[Dict[str, Any]], expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.incremental, state, expecting_exception) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_payment_methods.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_payment_methods.py new file mode 100644 index 0000000000000..d8e9f1450c668 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_payment_methods.py @@ -0,0 +1,347 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import json +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, Optional +from unittest import TestCase + +import freezegun +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +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 ConfiguredAirbyteCatalog, FailureType, SyncMode +from integration.config import ConfigBuilder +from integration.pagination import StripePaginationStrategy +from integration.request_builder import StripeRequestBuilder +from integration.response_builder import a_response_with_status +from source_stripe import SourceStripe + +_EVENT_TYPES = [ + "payment_method.attached", + "payment_method.automatically_updated", + "payment_method.detached", + "payment_method.updated", +] + +_DATA_FIELD = NestedPath(["data", "object"]) +_STREAM_NAME = "payment_methods" +_ENDPOINT_TEMPLATE_NAME = "payment_methods" +_NOW = datetime.now(timezone.utc) +_A_START_DATE = _NOW - timedelta(days=60) +_ACCOUNT_ID = "account_id" +_CLIENT_SECRET = "client_secret" +_NO_STATE = {} +_AVOIDING_INCLUSIVE_BOUNDARIES = timedelta(seconds=1) + + +def _payment_methods_request() -> StripeRequestBuilder: + return StripeRequestBuilder.payment_methods_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _events_request() -> StripeRequestBuilder: + return StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _config() -> ConfigBuilder: + return ConfigBuilder().with_start_date(_NOW - timedelta(days=75)).with_account_id(_ACCOUNT_ID).with_client_secret(_CLIENT_SECRET) + + +def _catalog(sync_mode: SyncMode) -> ConfiguredAirbyteCatalog: + return CatalogBuilder().with_stream(_STREAM_NAME, sync_mode).build() + + +def _source(catalog: ConfiguredAirbyteCatalog, config: Dict[str, Any]) -> SourceStripe: + return SourceStripe(catalog, config) + + +def _an_event() -> RecordBuilder: + return create_record_builder( + find_template("events", __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _events_response() -> HttpResponseBuilder: + return create_response_builder( + find_template("events", __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _a_payment_method() -> RecordBuilder: + return create_record_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _payment_methods_response() -> HttpResponseBuilder: + return create_response_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _given_payment_methods_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.payment_methods_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _payment_methods_response().build() + ) + + +def _given_events_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _events_response().build() + ) + + +def _read( + config_builder: ConfigBuilder, + sync_mode: SyncMode, + state: Optional[Dict[str, Any]] = None, + expecting_exception: bool = False +) -> EntrypointOutput: + catalog = _catalog(sync_mode) + config = config_builder.build() + return read(_source(catalog, config), config, catalog, state, expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class FullRefreshTest(TestCase): + + @HttpMocker() + def test_given_one_page_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _payment_methods_request().with_limit(100).build(), + _payment_methods_response().with_record(_a_payment_method()).with_record(_a_payment_method()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_many_pages_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _payment_methods_request().with_limit(100).build(), + _payment_methods_response().with_pagination().with_record(_a_payment_method().with_id("last_record_id_from_first_page")).build(), + ) + http_mocker.get( + _payment_methods_request().with_starting_after("last_record_id_from_first_page").with_limit(100).build(), + _payment_methods_response().with_record(_a_payment_method()).with_record(_a_payment_method()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 3 + + @HttpMocker() + def test_when_read_then_add_cursor_field(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _payment_methods_request().with_limit(100).build(), + _payment_methods_response().with_record(_a_payment_method()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + assert output.records[0].record.data["updated"] == output.records[0].record.data["created"] + + @HttpMocker() + def test_given_http_status_400_when_read_then_stream_is_ignored(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _payment_methods_request().with_any_query_params().build(), + a_response_with_status(400), + ) + output = self._read(_config()) + assert len(output.get_stream_statuses(_STREAM_NAME)) == 0 + + @HttpMocker() + def test_given_http_status_401_when_read_then_system_error(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _payment_methods_request().with_any_query_params().build(), + a_response_with_status(401), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_rate_limited_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _payment_methods_request().with_any_query_params().build(), + [ + a_response_with_status(429), + _payment_methods_response().with_record(_a_payment_method()).build(), + ], + ) + output = self._read(_config().with_start_date(_A_START_DATE)) + assert len(output.records) == 1 + + @HttpMocker() + def test_given_http_status_500_once_before_200_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _payment_methods_request().with_any_query_params().build(), + [a_response_with_status(500), _payment_methods_response().with_record(_a_payment_method()).build()], + ) + output = self._read(_config()) + 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: + http_mocker.get( + _payment_methods_request().with_any_query_params().build(), + a_response_with_status(500), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_small_slice_range_when_read_then_availability_check_performs_too_many_queries(self, http_mocker: HttpMocker) -> None: + # see https://github.com/airbytehq/airbyte/issues/33499 + events_requests = StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build() + http_mocker.get( + events_requests, + _events_response().build() # it is important that the event response does not have a record. This is not far fetched as this is what would happend 30 days before now + ) + http_mocker.get( + _payment_methods_request().with_any_query_params().build(), + _payment_methods_response().build(), + ) + + self._read(_config().with_start_date(_NOW - timedelta(days=60)).with_slice_range_in_days(1)) + + http_mocker.assert_number_of_calls(events_requests, 30) + + def _read(self, config: ConfigBuilder, expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.full_refresh, expecting_exception=expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class IncrementalTest(TestCase): + + @HttpMocker() + def test_given_no_state_when_read_then_use_payment_methods_endpoint(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + cursor_value = int(_A_START_DATE.timestamp()) + 1 + http_mocker.get( + _payment_methods_request().with_limit(100).build(), + _payment_methods_response().with_record(_a_payment_method().with_cursor(cursor_value)).build(), + ) + output = self._read(_config().with_start_date(_A_START_DATE), _NO_STATE) + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_when_read_then_query_events_using_types_and_state_value_plus_1(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=40) + state_datetime = _NOW - timedelta(days=5) + cursor_value = int(state_datetime.timestamp()) + 1 + + _given_payment_methods_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record( + _an_event().with_cursor(cursor_value).with_field(_DATA_FIELD, _a_payment_method().build()) + ).build(), + ) + + output = self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_and_pagination_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_payment_methods_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + state_datetime = _NOW - timedelta(days=5) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_pagination().with_record( + _an_event().with_id("last_record_id_from_first_page").with_field(_DATA_FIELD, _a_payment_method().build()) + ).build(), + ) + http_mocker.get( + _events_request().with_starting_after("last_record_id_from_first_page").with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_payment_method_event()).build(), + ) + + output = self._read( + _config(), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_state_and_small_slice_range_when_read_then_perform_multiple_queries(self, http_mocker: HttpMocker) -> None: + state_datetime = _NOW - timedelta(days=5) + slice_range = timedelta(days=3) + slice_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + slice_range + + _given_payment_methods_availability_check(http_mocker) + _given_events_availability_check(http_mocker) # the availability check does not consider the state so we need to define a generic availability check + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(slice_datetime).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_payment_method_event()).build(), + ) + http_mocker.get( + _events_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_payment_method_event()).with_record(self._a_payment_method_event()).build(), + ) + + output = self._read( + _config().with_start_date(_NOW - timedelta(days=30)).with_slice_range_in_days(slice_range.days), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_state_earlier_than_30_days_when_read_then_query_events_using_types_and_event_lower_boundary(self, http_mocker: HttpMocker) -> None: + # this seems odd as we would miss some data between start_date and events_lower_boundary. In that case, we should hit the + # payment_methods endpoint + _given_payment_methods_availability_check(http_mocker) + start_date = _NOW - timedelta(days=40) + state_value = _NOW - timedelta(days=39) + events_lower_boundary = _NOW - timedelta(days=30) + http_mocker.get( + _events_request().with_created_gte(events_lower_boundary).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_payment_method_event()).build(), + ) + + self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_value.timestamp())}).build(), + ) + + # request matched http_mocker + + def _a_payment_method_event(self) -> RecordBuilder: + return _an_event().with_field(_DATA_FIELD, _a_payment_method().build()) + + def _read(self, config: ConfigBuilder, state: Optional[Dict[str, Any]], expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.incremental, state, expecting_exception) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_persons.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_persons.py new file mode 100644 index 0000000000000..db000211be088 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_persons.py @@ -0,0 +1,636 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from datetime import datetime, timedelta, timezone +from typing import List +from unittest import TestCase + +import freezegun +from airbyte_cdk.models import FailureType, SyncMode +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import read +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 AirbyteStreamStatus, Level +from integration.config import ConfigBuilder +from integration.pagination import StripePaginationStrategy +from integration.request_builder import StripeRequestBuilder +from integration.response_builder import a_response_with_status +from source_stripe import SourceStripe + +_STREAM_NAME = "persons" +_ACCOUNT_ID = "acct_1G9HZLIEn49ers" +_CLIENT_SECRET = "ConfigBuilder default client secret" +_NOW = datetime.now(timezone.utc) +_CONFIG = { + "client_secret": _CLIENT_SECRET, + "account_id": _ACCOUNT_ID, +} +_AVOIDING_INCLUSIVE_BOUNDARIES = timedelta(seconds=1) + + +def _create_config() -> ConfigBuilder: + return ConfigBuilder().with_account_id(_ACCOUNT_ID).with_client_secret(_CLIENT_SECRET) + + +def _create_catalog(sync_mode: SyncMode = SyncMode.full_refresh): + return CatalogBuilder().with_stream(name="persons", sync_mode=sync_mode).build() + + +def _create_accounts_request() -> StripeRequestBuilder: + return StripeRequestBuilder.accounts_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _create_persons_request(parent_account_id: str = _ACCOUNT_ID) -> StripeRequestBuilder: + return StripeRequestBuilder.persons_endpoint(parent_account_id, _ACCOUNT_ID, _CLIENT_SECRET) + + +def _create_events_request() -> StripeRequestBuilder: + return StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _create_response() -> HttpResponseBuilder: + return create_response_builder( + response_template=find_template("accounts", __file__), + records_path=FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _create_record(resource: str) -> RecordBuilder: + return create_record_builder( + find_template(resource, __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created") + ) + + +def _create_persons_event_record(event_type: str) -> RecordBuilder: + event_record = create_record_builder( + find_template("events", __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + person_record = create_record_builder( + find_template("persons", __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created") + ) + + return event_record.with_field(NestedPath(["data", "object"]), person_record.build()).with_field(NestedPath(["type"]), event_type) + + +def emits_successful_sync_status_messages(status_messages: List[AirbyteStreamStatus]) -> bool: + return (len(status_messages) == 3 and status_messages[0] == AirbyteStreamStatus.STARTED + and status_messages[1] == AirbyteStreamStatus.RUNNING and status_messages[2] == AirbyteStreamStatus.COMPLETE) + + +@freezegun.freeze_time(_NOW.isoformat()) +class PersonsTest(TestCase): + @HttpMocker() + def test_full_refresh(self, http_mocker): + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts")).build(), + ) + + http_mocker.get( + _create_persons_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("persons")).with_record(record=_create_record("persons")).build(), + ) + + http_mocker.get( + _create_events_request().with_created_gte(_NOW - timedelta(days=30)).with_created_lte(_NOW).with_limit(100).with_types(["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_record("events")).with_record(record=_create_record("events")).build(), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog()) + actual_messages = read(source, config=_CONFIG, catalog=_create_catalog()) + + assert emits_successful_sync_status_messages(actual_messages.get_stream_statuses(_STREAM_NAME)) + assert len(actual_messages.records) == 2 + + @HttpMocker() + def test_parent_pagination(self, http_mocker): + # First parent stream accounts first page request + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts").with_id("last_page_record_id")).with_pagination().build(), + ) + + # Second parent stream accounts second page request + http_mocker.get( + _create_accounts_request().with_limit(100).with_starting_after("last_page_record_id").build(), + _create_response().with_record(record=_create_record("accounts").with_id("last_page_record_id")).build(), + ) + + # Persons stream first page request + http_mocker.get( + _create_persons_request(parent_account_id="last_page_record_id").with_limit(100).build(), + _create_response().with_record(record=_create_record("persons")).with_record(record=_create_record("persons")).build(), + ) + + # The persons stream makes a final call to events endpoint + http_mocker.get( + _create_events_request().with_created_gte(_NOW - timedelta(days=30)).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_record("events")).with_record(record=_create_record("events")).build(), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog()) + actual_messages = read(source, config=_CONFIG, catalog=_create_catalog()) + + assert emits_successful_sync_status_messages(actual_messages.get_stream_statuses(_STREAM_NAME)) + assert len(actual_messages.records) == 4 + + @HttpMocker() + def test_substream_pagination(self, http_mocker): + # First parent stream accounts first page request + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts")).build(), + ) + + # Persons stream first page request + http_mocker.get( + _create_persons_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("persons")).with_record(record=_create_record("persons").with_id("last_page_record_id")).with_pagination().build(), + ) + + # Persons stream second page request + http_mocker.get( + _create_persons_request().with_limit(100).with_starting_after("last_page_record_id").build(), + _create_response().with_record(record=_create_record("persons")).with_record( + record=_create_record("persons")).build(), + ) + + # The persons stream makes a final call to events endpoint + http_mocker.get( + _create_events_request().with_created_gte(_NOW - timedelta(days=30)).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_record("events")).with_record(record=_create_record("events")).build(), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog()) + actual_messages = read(source, config=_CONFIG, catalog=_create_catalog()) + + assert emits_successful_sync_status_messages(actual_messages.get_stream_statuses(_STREAM_NAME)) + assert len(actual_messages.records) == 4 + + @HttpMocker() + def test_accounts_400_error(self, http_mocker: HttpMocker): + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + a_response_with_status(400), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog()) + actual_messages = read(source, config=_CONFIG, catalog=_create_catalog()) + error_log_messages = [message for message in actual_messages.logs if message.log.level == Level.ERROR] + + # For Stripe, streams that get back a 400 or 403 response code are skipped over silently without throwing an error as part of + # this connector's availability strategy + assert len(actual_messages.get_stream_statuses(_STREAM_NAME)) == 0 + assert len(error_log_messages) > 0 + + @HttpMocker() + def test_persons_400_error(self, http_mocker: HttpMocker): + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts")).build(), + ) + + # Persons stream first page request + http_mocker.get( + _create_persons_request().with_limit(100).build(), + a_response_with_status(400), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog()) + actual_messages = read(source, config=_CONFIG, catalog=_create_catalog()) + error_log_messages = [message for message in actual_messages.logs if message.log.level == Level.ERROR] + + # For Stripe, streams that get back a 400 or 403 response code are skipped over silently without throwing an error as part of + # this connector's availability strategy. They are however reported in the log messages + assert len(actual_messages.get_stream_statuses(_STREAM_NAME)) == 0 + assert len(error_log_messages) > 0 + + @HttpMocker() + def test_accounts_401_error(self, http_mocker: HttpMocker): + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + a_response_with_status(401), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog()) + actual_messages = read(source, config=_CONFIG, catalog=_create_catalog(), expecting_exception=True) + + assert actual_messages.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_persons_401_error(self, http_mocker: HttpMocker): + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts")).build(), + ) + + # Persons stream first page request + http_mocker.get( + _create_persons_request().with_limit(100).build(), + a_response_with_status(401), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog()) + actual_messages = read(source, config=_CONFIG, catalog=_create_catalog(), expecting_exception=True) + + assert actual_messages.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_persons_403_error(self, http_mocker: HttpMocker): + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts")).build(), + ) + + # Persons stream first page request + http_mocker.get( + _create_persons_request().with_limit(100).build(), + a_response_with_status(403), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog()) + actual_messages = read(source, config=_CONFIG, catalog=_create_catalog(), expecting_exception=True) + error_log_messages = [message for message in actual_messages.logs if message.log.level == Level.ERROR] + + # For Stripe, streams that get back a 400 or 403 response code are skipped over silently without throwing an error as part of + # this connector's availability strategy + assert len(actual_messages.get_stream_statuses(_STREAM_NAME)) == 0 + assert len(error_log_messages) > 0 + + @HttpMocker() + def test_incremental_with_recent_state(self, http_mocker: HttpMocker): + state_datetime = _NOW - timedelta(days=5) + cursor_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts")).build(), + ) + + http_mocker.get( + _create_persons_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("persons")).with_record(record=_create_record("persons")).build(), + ) + + http_mocker.get( + _create_events_request().with_created_gte(_NOW - timedelta(days=30)).with_created_lte(_NOW).with_limit(100).with_types(["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_persons_event_record(event_type="person.created")).with_record(record=_create_persons_event_record(event_type="person.created")).build(), + ) + + http_mocker.get( + _create_events_request().with_created_gte(cursor_datetime).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_persons_event_record(event_type="person.created")).build(), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog(sync_mode=SyncMode.incremental)) + actual_messages = read( + source, + config=_CONFIG, + catalog=_create_catalog(sync_mode=SyncMode.incremental), + state=StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert emits_successful_sync_status_messages(actual_messages.get_stream_statuses(_STREAM_NAME)) + assert actual_messages.most_recent_state == {"persons": {"updated": int(state_datetime.timestamp())}} + assert len(actual_messages.records) == 1 + + @HttpMocker() + def test_incremental_with_deleted_event(self, http_mocker: HttpMocker): + state_datetime = _NOW - timedelta(days=5) + cursor_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts")).build(), + ) + + http_mocker.get( + _create_persons_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("persons")).with_record(record=_create_record("persons")).build(), + ) + + http_mocker.get( + _create_events_request().with_created_gte(_NOW - timedelta(days=30)).with_created_lte(_NOW).with_limit(100).with_types(["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_persons_event_record(event_type="person.created")).with_record(record=_create_persons_event_record(event_type="person.deleted")).build(), + ) + + http_mocker.get( + _create_events_request().with_created_gte(cursor_datetime).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_persons_event_record(event_type="person.deleted")).build(), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog(sync_mode=SyncMode.incremental)) + actual_messages = read( + source, + config=_CONFIG, + catalog=_create_catalog(sync_mode=SyncMode.incremental), + state=StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert emits_successful_sync_status_messages(actual_messages.get_stream_statuses(_STREAM_NAME)) + assert actual_messages.most_recent_state == {"persons": {"updated": int(state_datetime.timestamp())}} + assert len(actual_messages.records) == 1 + assert actual_messages.records[0].record.data.get("is_deleted") + + @HttpMocker() + def test_incremental_with_newer_start_date(self, http_mocker): + start_datetime = _NOW - timedelta(days=7) + state_datetime = _NOW - timedelta(days=15) + config = _create_config().with_start_date(start_datetime).build() + + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts")).build(), + ) + + http_mocker.get( + _create_persons_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("persons")).with_record(record=_create_record("persons")).build(), + ) + + http_mocker.get( + _create_events_request().with_created_gte(start_datetime).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_persons_event_record(event_type="person.created")).build(), + ) + + source = SourceStripe(config=config, catalog=_create_catalog(sync_mode=SyncMode.incremental)) + actual_messages = read( + source, + config=config, + catalog=_create_catalog(sync_mode=SyncMode.incremental), + state=StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert emits_successful_sync_status_messages(actual_messages.get_stream_statuses(_STREAM_NAME)) + assert actual_messages.most_recent_state == {"persons": {"updated": int(state_datetime.timestamp())}} + assert len(actual_messages.records) == 1 + + @HttpMocker() + def test_rate_limited_parent_stream_accounts(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + [ + a_response_with_status(429), + _create_response().with_record(record=_create_record("accounts")).build(), + ], + ) + + http_mocker.get( + _create_persons_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("persons")).with_record(record=_create_record("persons")).build(), + ) + + http_mocker.get( + _create_events_request().with_created_gte(_NOW - timedelta(days=30)).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_record("events")).with_record(record=_create_record("events")).build(), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog()) + actual_messages = read(source, config=_CONFIG, catalog=_create_catalog()) + + assert emits_successful_sync_status_messages(actual_messages.get_stream_statuses(_STREAM_NAME)) + assert len(actual_messages.records) == 2 + + @HttpMocker() + def test_rate_limited_substream_persons(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts")).build(), + ) + + http_mocker.get( + _create_persons_request().with_limit(100).build(), + [ + a_response_with_status(429), + _create_response().with_record(record=_create_record("persons")).with_record(record=_create_record("persons")).build(), + ] + ) + + http_mocker.get( + _create_events_request().with_created_gte(_NOW - timedelta(days=30)).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_record("events")).with_record(record=_create_record("events")).build(), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog()) + actual_messages = read(source, config=_CONFIG, catalog=_create_catalog()) + + assert emits_successful_sync_status_messages(actual_messages.get_stream_statuses(_STREAM_NAME)) + assert len(actual_messages.records) == 2 + + @HttpMocker() + def test_rate_limited_incremental_events(self, http_mocker: HttpMocker) -> None: + state_datetime = _NOW - timedelta(days=5) + cursor_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts")).build(), + ) + + http_mocker.get( + _create_persons_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("persons")).with_record(record=_create_record("persons")).build(), + ) + + # Mock when check_availability is run on the persons incremental stream + http_mocker.get( + _create_events_request().with_created_gte(_NOW - timedelta(days=30)).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_persons_event_record(event_type="person.created")).with_record( + record=_create_persons_event_record(event_type="person.created")).build(), + ) + + http_mocker.get( + _create_events_request().with_created_gte(cursor_datetime).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + [ + a_response_with_status(429), + _create_response().with_record(record=_create_persons_event_record(event_type="person.created")).build(), + ] + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog(sync_mode=SyncMode.incremental)) + actual_messages = read( + source, + config=_CONFIG, + catalog=_create_catalog(sync_mode=SyncMode.incremental), + state=StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert emits_successful_sync_status_messages(actual_messages.get_stream_statuses(_STREAM_NAME)) + assert actual_messages.most_recent_state == {"persons": {"updated": int(state_datetime.timestamp())}} + assert len(actual_messages.records) == 1 + + @HttpMocker() + def test_rate_limit_max_attempts_exceeded(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts")).build(), + ) + + http_mocker.get( + _create_persons_request().with_limit(100).build(), + [ + # Used to pass the initial check_availability before starting the sync + _create_response().with_record(record=_create_record("persons")).with_record(record=_create_record("persons")).build(), + a_response_with_status(429), # Returns 429 on all subsequent requests to test the maximum number of retries + ] + ) + + http_mocker.get( + _create_events_request().with_created_gte(_NOW - timedelta(days=30)).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_record("events")).with_record(record=_create_record("events")).build(), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog()) + actual_messages = read(source, config=_CONFIG, catalog=_create_catalog()) + + assert len(actual_messages.errors) == 1 + + @HttpMocker() + def test_incremental_rate_limit_max_attempts_exceeded(self, http_mocker: HttpMocker) -> None: + state_datetime = _NOW - timedelta(days=5) + cursor_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts")).build(), + ) + + http_mocker.get( + _create_persons_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("persons")).with_record(record=_create_record("persons")).build(), + ) + + # Mock when check_availability is run on the persons incremental stream + http_mocker.get( + _create_events_request().with_created_gte(_NOW - timedelta(days=30)).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_persons_event_record(event_type="person.created")).with_record( + record=_create_persons_event_record(event_type="person.created")).build(), + ) + + http_mocker.get( + _create_events_request().with_created_gte(cursor_datetime).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + a_response_with_status(429), # Returns 429 on all subsequent requests to test the maximum number of retries + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog(sync_mode=SyncMode.incremental)) + actual_messages = read( + source, + config=_CONFIG, + catalog=_create_catalog(sync_mode=SyncMode.incremental), + state=StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(actual_messages.errors) == 1 + + @HttpMocker() + def test_server_error_parent_stream_accounts(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + [ + a_response_with_status(500), + _create_response().with_record(record=_create_record("accounts")).build(), + ], + ) + + http_mocker.get( + _create_persons_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("persons")).with_record(record=_create_record("persons")).build(), + ) + + http_mocker.get( + _create_events_request().with_created_gte(_NOW - timedelta(days=30)).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_record("events")).with_record(record=_create_record("events")).build(), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog()) + actual_messages = read(source, config=_CONFIG, catalog=_create_catalog()) + + assert emits_successful_sync_status_messages(actual_messages.get_stream_statuses(_STREAM_NAME)) + assert len(actual_messages.records) == 2 + + @HttpMocker() + def test_server_error_substream_persons(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts")).build(), + ) + + http_mocker.get( + _create_persons_request().with_limit(100).build(), + [ + a_response_with_status(500), + _create_response().with_record(record=_create_record("persons")).with_record(record=_create_record("persons")).build(), + ] + ) + + http_mocker.get( + _create_events_request().with_created_gte(_NOW - timedelta(days=30)).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_record("events")).with_record(record=_create_record("events")).build(), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog()) + actual_messages = read(source, config=_CONFIG, catalog=_create_catalog()) + + assert emits_successful_sync_status_messages(actual_messages.get_stream_statuses(_STREAM_NAME)) + assert len(actual_messages.records) == 2 + + @HttpMocker() + def test_server_error_max_attempts_exceeded(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _create_accounts_request().with_limit(100).build(), + _create_response().with_record(record=_create_record("accounts")).build(), + ) + + http_mocker.get( + _create_persons_request().with_limit(100).build(), + [ + # Used to pass the initial check_availability before starting the sync + _create_response().with_record(record=_create_record("persons")).with_record(record=_create_record("persons")).build(), + a_response_with_status(500), # Returns 429 on all subsequent requests to test the maximum number of retries + ] + ) + + http_mocker.get( + _create_events_request().with_created_gte(_NOW - timedelta(days=30)).with_created_lte(_NOW).with_limit(100).with_types( + ["person.created", "person.updated", "person.deleted"]).build(), + _create_response().with_record(record=_create_record("events")).with_record(record=_create_record("events")).build(), + ) + + source = SourceStripe(config=_CONFIG, catalog=_create_catalog()) + actual_messages = read(source, config=_CONFIG, catalog=_create_catalog()) + + assert len(actual_messages.errors) == 1 diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_reviews.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_reviews.py new file mode 100644 index 0000000000000..45ee0219f8da8 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_reviews.py @@ -0,0 +1,374 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import json +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, Optional +from unittest import TestCase + +import freezegun +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +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 ConfiguredAirbyteCatalog, FailureType, SyncMode +from integration.config import ConfigBuilder +from integration.pagination import StripePaginationStrategy +from integration.request_builder import StripeRequestBuilder +from integration.response_builder import a_response_with_status +from source_stripe import SourceStripe + +_EVENT_TYPES = ["review.closed", "review.opened"] + +_DATA_FIELD = NestedPath(["data", "object"]) +_STREAM_NAME = "reviews" +_ENDPOINT_TEMPLATE_NAME = "reviews" +_NOW = datetime.now(timezone.utc) +_A_START_DATE = _NOW - timedelta(days=60) +_ACCOUNT_ID = "account_id" +_CLIENT_SECRET = "client_secret" +_NO_STATE = {} +_AVOIDING_INCLUSIVE_BOUNDARIES = timedelta(seconds=1) + + +def _reviews_request() -> StripeRequestBuilder: + return StripeRequestBuilder.reviews_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _events_request() -> StripeRequestBuilder: + return StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _config() -> ConfigBuilder: + return ConfigBuilder().with_start_date(_NOW - timedelta(days=75)).with_account_id(_ACCOUNT_ID).with_client_secret(_CLIENT_SECRET) + + +def _catalog(sync_mode: SyncMode) -> ConfiguredAirbyteCatalog: + return CatalogBuilder().with_stream(_STREAM_NAME, sync_mode).build() + + +def _source(catalog: ConfiguredAirbyteCatalog, config: Dict[str, Any]) -> SourceStripe: + return SourceStripe(catalog, config) + + +def _an_event() -> RecordBuilder: + return create_record_builder( + find_template("events", __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _events_response() -> HttpResponseBuilder: + return create_response_builder( + find_template("events", __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _a_review() -> RecordBuilder: + return create_record_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _reviews_response() -> HttpResponseBuilder: + return create_response_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _given_reviews_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.reviews_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _reviews_response().build() + ) + + +def _given_events_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _events_response().build() + ) + + +def _read( + config_builder: ConfigBuilder, + sync_mode: SyncMode, + state: Optional[Dict[str, Any]] = None, + expecting_exception: bool = False +) -> EntrypointOutput: + catalog = _catalog(sync_mode) + config = config_builder.build() + return read(_source(catalog, config), config, catalog, state, expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class FullRefreshTest(TestCase): + + @HttpMocker() + def test_given_one_page_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _reviews_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _reviews_response().with_record(_a_review()).with_record(_a_review()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_many_pages_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _reviews_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _reviews_response().with_pagination().with_record(_a_review().with_id("last_record_id_from_first_page")).build(), + ) + http_mocker.get( + _reviews_request().with_starting_after("last_record_id_from_first_page").with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _reviews_response().with_record(_a_review()).with_record(_a_review()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_no_state_when_read_then_return_ignore_lookback(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _reviews_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _reviews_response().with_record(_a_review()).build(), + ) + + self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + # request matched http_mocker + + @HttpMocker() + def test_when_read_then_add_cursor_field(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _reviews_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _reviews_response().with_record(_a_review()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + assert output.records[0].record.data["updated"] == output.records[0].record.data["created"] + + @HttpMocker() + def test_given_slice_range_when_read_then_perform_multiple_requests(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=30) + slice_range = timedelta(days=20) + slice_datetime = start_date + slice_range + + _given_events_availability_check(http_mocker) + http_mocker.get( + _reviews_request().with_created_gte(start_date).with_created_lte(slice_datetime).with_limit(100).build(), + _reviews_response().build(), + ) + http_mocker.get( + _reviews_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).build(), + _reviews_response().build(), + ) + + self._read(_config().with_start_date(start_date).with_slice_range_in_days(slice_range.days)) + + # request matched http_mocker + + @HttpMocker() + def test_given_http_status_400_when_read_then_stream_is_ignored(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _reviews_request().with_any_query_params().build(), + a_response_with_status(400), + ) + output = self._read(_config()) + assert len(output.get_stream_statuses(_STREAM_NAME)) == 0 + + @HttpMocker() + def test_given_http_status_401_when_read_then_system_error(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _reviews_request().with_any_query_params().build(), + a_response_with_status(401), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_rate_limited_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _reviews_request().with_any_query_params().build(), + [ + a_response_with_status(429), + _reviews_response().with_record(_a_review()).build(), + ], + ) + output = self._read(_config().with_start_date(_A_START_DATE)) + assert len(output.records) == 1 + + @HttpMocker() + def test_given_http_status_500_once_before_200_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _reviews_request().with_any_query_params().build(), + [a_response_with_status(500), _reviews_response().with_record(_a_review()).build()], + ) + output = self._read(_config()) + 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: + http_mocker.get( + _reviews_request().with_any_query_params().build(), + a_response_with_status(500), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_small_slice_range_when_read_then_availability_check_performs_too_many_queries(self, http_mocker: HttpMocker) -> None: + # see https://github.com/airbytehq/airbyte/issues/33499 + events_requests = StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build() + http_mocker.get( + events_requests, + _events_response().build() # it is important that the event response does not have a record. This is not far fetched as this is what would happend 30 days before now + ) + http_mocker.get( + _reviews_request().with_any_query_params().build(), + _reviews_response().build(), + ) + + self._read(_config().with_start_date(_NOW - timedelta(days=60)).with_slice_range_in_days(1)) + + http_mocker.assert_number_of_calls(events_requests, 30) + + def _read(self, config: ConfigBuilder, expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.full_refresh, expecting_exception=expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class IncrementalTest(TestCase): + + @HttpMocker() + def test_given_no_state_when_read_then_use_reviews_endpoint(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + cursor_value = int(_A_START_DATE.timestamp()) + 1 + http_mocker.get( + _reviews_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _reviews_response().with_record(_a_review().with_cursor(cursor_value)).build(), + ) + output = self._read(_config().with_start_date(_A_START_DATE), _NO_STATE) + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_when_read_then_query_events_using_types_and_state_value_plus_1(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=40) + state_datetime = _NOW - timedelta(days=5) + cursor_value = int(state_datetime.timestamp()) + 1 + + _given_reviews_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record( + _an_event().with_cursor(cursor_value).with_field(_DATA_FIELD, _a_review().build()) + ).build(), + ) + + output = self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_and_pagination_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_reviews_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + state_datetime = _NOW - timedelta(days=5) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_pagination().with_record( + _an_event().with_id("last_record_id_from_first_page").with_field(_DATA_FIELD, _a_review().build()) + ).build(), + ) + http_mocker.get( + _events_request().with_starting_after("last_record_id_from_first_page").with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_review_event()).build(), + ) + + output = self._read( + _config(), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_state_and_small_slice_range_when_read_then_perform_multiple_queries(self, http_mocker: HttpMocker) -> None: + state_datetime = _NOW - timedelta(days=5) + slice_range = timedelta(days=3) + slice_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + slice_range + + _given_reviews_availability_check(http_mocker) + _given_events_availability_check(http_mocker) # the availability check does not consider the state so we need to define a generic availability check + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(slice_datetime).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_review_event()).build(), + ) + http_mocker.get( + _events_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_review_event()).with_record(self._a_review_event()).build(), + ) + + output = self._read( + _config().with_start_date(_NOW - timedelta(days=30)).with_slice_range_in_days(slice_range.days), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_state_earlier_than_30_days_when_read_then_query_events_using_types_and_event_lower_boundary(self, http_mocker: HttpMocker) -> None: + # this seems odd as we would miss some data between start_date and events_lower_boundary. In that case, we should hit the + # reviews endpoint + _given_reviews_availability_check(http_mocker) + start_date = _NOW - timedelta(days=40) + state_value = _NOW - timedelta(days=39) + events_lower_boundary = _NOW - timedelta(days=30) + http_mocker.get( + _events_request().with_created_gte(events_lower_boundary).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_review_event()).build(), + ) + + self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_value.timestamp())}).build(), + ) + + # request matched http_mocker + + def _a_review_event(self) -> RecordBuilder: + return _an_event().with_field(_DATA_FIELD, _a_review().build()) + + def _read(self, config: ConfigBuilder, state: Optional[Dict[str, Any]], expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.incremental, state, expecting_exception) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_transactions.py b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_transactions.py new file mode 100644 index 0000000000000..8c4db06972235 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/integration/test_transactions.py @@ -0,0 +1,374 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import json +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, Optional +from unittest import TestCase + +import freezegun +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +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 ConfiguredAirbyteCatalog, FailureType, SyncMode +from integration.config import ConfigBuilder +from integration.pagination import StripePaginationStrategy +from integration.request_builder import StripeRequestBuilder +from integration.response_builder import a_response_with_status +from source_stripe import SourceStripe + +_EVENT_TYPES = ["issuing_transaction.created", "issuing_transaction.updated"] + +_DATA_FIELD = NestedPath(["data", "object"]) +_STREAM_NAME = "transactions" +_ENDPOINT_TEMPLATE_NAME = "issuing_transactions" +_NOW = datetime.now(timezone.utc) +_A_START_DATE = _NOW - timedelta(days=60) +_ACCOUNT_ID = "account_id" +_CLIENT_SECRET = "client_secret" +_NO_STATE = {} +_AVOIDING_INCLUSIVE_BOUNDARIES = timedelta(seconds=1) + + +def _transactions_request() -> StripeRequestBuilder: + return StripeRequestBuilder.issuing_transactions_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _events_request() -> StripeRequestBuilder: + return StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET) + + +def _config() -> ConfigBuilder: + return ConfigBuilder().with_start_date(_NOW - timedelta(days=75)).with_account_id(_ACCOUNT_ID).with_client_secret(_CLIENT_SECRET) + + +def _catalog(sync_mode: SyncMode) -> ConfiguredAirbyteCatalog: + return CatalogBuilder().with_stream(_STREAM_NAME, sync_mode).build() + + +def _source(catalog: ConfiguredAirbyteCatalog, config: Dict[str, Any]) -> SourceStripe: + return SourceStripe(catalog, config) + + +def _an_event() -> RecordBuilder: + return create_record_builder( + find_template("events", __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _events_response() -> HttpResponseBuilder: + return create_response_builder( + find_template("events", __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _a_transaction() -> RecordBuilder: + return create_record_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + record_id_path=FieldPath("id"), + record_cursor_path=FieldPath("created"), + ) + + +def _transactions_response() -> HttpResponseBuilder: + return create_response_builder( + find_template(_ENDPOINT_TEMPLATE_NAME, __file__), + FieldPath("data"), + pagination_strategy=StripePaginationStrategy() + ) + + +def _given_transactions_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.issuing_transactions_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _transactions_response().build() + ) + + +def _given_events_availability_check(http_mocker: HttpMocker) -> None: + http_mocker.get( + StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build(), + _events_response().build() + ) + + +def _read( + config_builder: ConfigBuilder, + sync_mode: SyncMode, + state: Optional[Dict[str, Any]] = None, + expecting_exception: bool = False +) -> EntrypointOutput: + catalog = _catalog(sync_mode) + config = config_builder.build() + return read(_source(catalog, config), config, catalog, state, expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class FullRefreshTest(TestCase): + + @HttpMocker() + def test_given_one_page_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _transactions_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _transactions_response().with_record(_a_transaction()).with_record(_a_transaction()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_many_pages_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _transactions_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _transactions_response().with_pagination().with_record(_a_transaction().with_id("last_record_id_from_first_page")).build(), + ) + http_mocker.get( + _transactions_request().with_starting_after("last_record_id_from_first_page").with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _transactions_response().with_record(_a_transaction()).with_record(_a_transaction()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE)) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_no_state_when_read_then_return_ignore_lookback(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _transactions_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _transactions_response().with_record(_a_transaction()).build(), + ) + + self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + # request matched http_mocker + + @HttpMocker() + def test_when_read_then_add_cursor_field(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _transactions_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _transactions_response().with_record(_a_transaction()).build(), + ) + + output = self._read(_config().with_start_date(_A_START_DATE).with_lookback_window_in_days(10)) + + assert output.records[0].record.data["updated"] == output.records[0].record.data["created"] + + @HttpMocker() + def test_given_slice_range_when_read_then_perform_multiple_requests(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=30) + slice_range = timedelta(days=20) + slice_datetime = start_date + slice_range + + _given_events_availability_check(http_mocker) + http_mocker.get( + _transactions_request().with_created_gte(start_date).with_created_lte(slice_datetime).with_limit(100).build(), + _transactions_response().build(), + ) + http_mocker.get( + _transactions_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).build(), + _transactions_response().build(), + ) + + self._read(_config().with_start_date(start_date).with_slice_range_in_days(slice_range.days)) + + # request matched http_mocker + + @HttpMocker() + def test_given_http_status_400_when_read_then_stream_is_ignored(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _transactions_request().with_any_query_params().build(), + a_response_with_status(400), + ) + output = self._read(_config()) + assert len(output.get_stream_statuses(_STREAM_NAME)) == 0 + + @HttpMocker() + def test_given_http_status_401_when_read_then_system_error(self, http_mocker: HttpMocker) -> None: + http_mocker.get( + _transactions_request().with_any_query_params().build(), + a_response_with_status(401), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_rate_limited_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _transactions_request().with_any_query_params().build(), + [ + a_response_with_status(429), + _transactions_response().with_record(_a_transaction()).build(), + ], + ) + output = self._read(_config().with_start_date(_A_START_DATE)) + assert len(output.records) == 1 + + @HttpMocker() + def test_given_http_status_500_once_before_200_when_read_then_retry_and_return_records(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + http_mocker.get( + _transactions_request().with_any_query_params().build(), + [a_response_with_status(500), _transactions_response().with_record(_a_transaction()).build()], + ) + output = self._read(_config()) + 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: + http_mocker.get( + _transactions_request().with_any_query_params().build(), + a_response_with_status(500), + ) + output = self._read(_config(), expecting_exception=True) + assert output.errors[-1].trace.error.failure_type == FailureType.system_error + + @HttpMocker() + def test_given_small_slice_range_when_read_then_availability_check_performs_too_many_queries(self, http_mocker: HttpMocker) -> None: + # see https://github.com/airbytehq/airbyte/issues/33499 + events_requests = StripeRequestBuilder.events_endpoint(_ACCOUNT_ID, _CLIENT_SECRET).with_any_query_params().build() + http_mocker.get( + events_requests, + _events_response().build() # it is important that the event response does not have a record. This is not far fetched as this is what would happend 30 days before now + ) + http_mocker.get( + _transactions_request().with_any_query_params().build(), + _transactions_response().build(), + ) + + self._read(_config().with_start_date(_NOW - timedelta(days=60)).with_slice_range_in_days(1)) + + http_mocker.assert_number_of_calls(events_requests, 30) + + def _read(self, config: ConfigBuilder, expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.full_refresh, expecting_exception=expecting_exception) + + +@freezegun.freeze_time(_NOW.isoformat()) +class IncrementalTest(TestCase): + + @HttpMocker() + def test_given_no_state_when_read_then_use_transactions_endpoint(self, http_mocker: HttpMocker) -> None: + _given_events_availability_check(http_mocker) + cursor_value = int(_A_START_DATE.timestamp()) + 1 + http_mocker.get( + _transactions_request().with_created_gte(_A_START_DATE).with_created_lte(_NOW).with_limit(100).build(), + _transactions_response().with_record(_a_transaction().with_cursor(cursor_value)).build(), + ) + output = self._read(_config().with_start_date(_A_START_DATE), _NO_STATE) + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_when_read_then_query_events_using_types_and_state_value_plus_1(self, http_mocker: HttpMocker) -> None: + start_date = _NOW - timedelta(days=40) + state_datetime = _NOW - timedelta(days=5) + cursor_value = int(state_datetime.timestamp()) + 1 + + _given_transactions_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record( + _an_event().with_cursor(cursor_value).with_field(_DATA_FIELD, _a_transaction().build()) + ).build(), + ) + + output = self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert output.most_recent_state == {_STREAM_NAME: {"updated": cursor_value}} + + @HttpMocker() + def test_given_state_and_pagination_when_read_then_return_records(self, http_mocker: HttpMocker) -> None: + _given_transactions_availability_check(http_mocker) + _given_events_availability_check(http_mocker) + state_datetime = _NOW - timedelta(days=5) + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_pagination().with_record( + _an_event().with_id("last_record_id_from_first_page").with_field(_DATA_FIELD, _a_transaction().build()) + ).build(), + ) + http_mocker.get( + _events_request().with_starting_after("last_record_id_from_first_page").with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_transaction_event()).build(), + ) + + output = self._read( + _config(), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 2 + + @HttpMocker() + def test_given_state_and_small_slice_range_when_read_then_perform_multiple_queries(self, http_mocker: HttpMocker) -> None: + state_datetime = _NOW - timedelta(days=5) + slice_range = timedelta(days=3) + slice_datetime = state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES + slice_range + + _given_transactions_availability_check(http_mocker) + _given_events_availability_check(http_mocker) # the availability check does not consider the state so we need to define a generic availability check + http_mocker.get( + _events_request().with_created_gte(state_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(slice_datetime).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_transaction_event()).build(), + ) + http_mocker.get( + _events_request().with_created_gte(slice_datetime + _AVOIDING_INCLUSIVE_BOUNDARIES).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_transaction_event()).with_record(self._a_transaction_event()).build(), + ) + + output = self._read( + _config().with_start_date(_NOW - timedelta(days=30)).with_slice_range_in_days(slice_range.days), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_datetime.timestamp())}).build(), + ) + + assert len(output.records) == 3 + + @HttpMocker() + def test_given_state_earlier_than_30_days_when_read_then_query_events_using_types_and_event_lower_boundary(self, http_mocker: HttpMocker) -> None: + # this seems odd as we would miss some data between start_date and events_lower_boundary. In that case, we should hit the + # transactions endpoint + _given_transactions_availability_check(http_mocker) + start_date = _NOW - timedelta(days=40) + state_value = _NOW - timedelta(days=39) + events_lower_boundary = _NOW - timedelta(days=30) + http_mocker.get( + _events_request().with_created_gte(events_lower_boundary).with_created_lte(_NOW).with_limit(100).with_types(_EVENT_TYPES).build(), + _events_response().with_record(self._a_transaction_event()).build(), + ) + + self._read( + _config().with_start_date(start_date), + StateBuilder().with_stream_state(_STREAM_NAME, {"updated": int(state_value.timestamp())}).build(), + ) + + # request matched http_mocker + + def _a_transaction_event(self) -> RecordBuilder: + return _an_event().with_field(_DATA_FIELD, _a_transaction().build()) + + def _read(self, config: ConfigBuilder, state: Optional[Dict[str, Any]], expecting_exception: bool = False) -> EntrypointOutput: + return _read(config, SyncMode.incremental, state, expecting_exception) diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/400.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/400.json new file mode 100644 index 0000000000000..4ded0c0a89190 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/400.json @@ -0,0 +1,7 @@ +{ + "error": { + "message": "Your account is not set up to use Issuing. Please visit https://dashboard.stripe.com/issuing/overview to get started.", + "request_log_url": "https://dashboard.stripe.com/test/logs/req_OzHOvvVQ4ALtKm?t=1702476901", + "type": "invalid_request_error" + } +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/401.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/401.json new file mode 100644 index 0000000000000..67f5dfd22e070 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/401.json @@ -0,0 +1,6 @@ +{ + "error": { + "message": "Invalid API Key provided: sk_test_*****************************************************mFeM", + "type": "invalid_request_error" + } +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/403.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/403.json new file mode 100644 index 0000000000000..9fadb9f2fe1f0 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/403.json @@ -0,0 +1,8 @@ +{ + "error": { + "code": "oauth_not_supported", + "message": "This application does not have the required permissions for this endpoint on account 'acct_1G9HZLIEn49ers'.", + "request_log_url": "https://dashboard.stripe.com/acct_1IB2IIRPz4Eoy76F/test/logs/req_yfhmzM1ChMWuhX?t=1703806215", + "type": "invalid_request_error" + } +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/429.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/429.json new file mode 100644 index 0000000000000..249f882eecc0e --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/429.json @@ -0,0 +1,8 @@ +{ + "error": { + "message": "Request rate limit exceeded. Learn more about rate limits here https://stripe.com/docs/rate-limits.", + "type": "invalid_request_error", + "code": "rate_limit", + "doc_url": "https://stripe.com/docs/error-codes/rate-limit" + } +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/500.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/500.json new file mode 100644 index 0000000000000..0077e9a45a615 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/500.json @@ -0,0 +1,3 @@ +{ + "unknown": "maxi297: I could not reproduce the issue hence this response will not look like the actual 500 status response" +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/accounts.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/accounts.json new file mode 100644 index 0000000000000..475961a4ed4f0 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/accounts.json @@ -0,0 +1,136 @@ +{ + "object": "list", + "url": "/v1/accounts", + "has_more": false, + "data": [ + { + "id": "acct_1G9HZLIEn49ers", + "object": "account", + "business_profile": { + "mcc": null, + "name": null, + "product_description": null, + "support_address": null, + "support_email": null, + "support_phone": null, + "support_url": null, + "url": null + }, + "business_type": null, + "capabilities": { + "card_payments": "inactive", + "transfers": "inactive" + }, + "charges_enabled": false, + "country": "US", + "created": 1695830751, + "default_currency": "usd", + "details_submitted": false, + "email": "john.lynch@49ers.com", + "external_accounts": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/accounts/acct_1G9HZLIEn49ers/external_accounts" + }, + "future_requirements": { + "alternatives": [], + "current_deadline": null, + "currently_due": [], + "disabled_reason": null, + "errors": [], + "eventually_due": [], + "past_due": [], + "pending_verification": [] + }, + "metadata": {}, + "payouts_enabled": false, + "requirements": { + "alternatives": [], + "current_deadline": null, + "currently_due": [ + "business_profile.mcc", + "business_profile.url", + "business_type", + "external_account", + "representative.first_name", + "representative.last_name", + "tos_acceptance.date", + "tos_acceptance.ip" + ], + "disabled_reason": "requirements.past_due", + "errors": [], + "eventually_due": [ + "business_profile.mcc", + "business_profile.url", + "business_type", + "external_account", + "representative.first_name", + "representative.last_name", + "tos_acceptance.date", + "tos_acceptance.ip" + ], + "past_due": [ + "business_profile.mcc", + "business_profile.url", + "business_type", + "external_account", + "representative.first_name", + "representative.last_name", + "tos_acceptance.date", + "tos_acceptance.ip" + ], + "pending_verification": [] + }, + "settings": { + "bacs_debit_payments": {}, + "branding": { + "icon": null, + "logo": null, + "primary_color": null, + "secondary_color": null + }, + "card_issuing": { + "tos_acceptance": { + "date": null, + "ip": null + } + }, + "card_payments": { + "decline_on": { + "avs_failure": false, + "cvc_failure": false + }, + "statement_descriptor_prefix": null, + "statement_descriptor_prefix_kana": null, + "statement_descriptor_prefix_kanji": null + }, + "dashboard": { + "display_name": null, + "timezone": "Etc/UTC" + }, + "payments": { + "statement_descriptor": null, + "statement_descriptor_kana": null, + "statement_descriptor_kanji": null + }, + "payouts": { + "debit_negative_balances": false, + "schedule": { + "delay_days": 2, + "interval": "daily" + }, + "statement_descriptor": null + }, + "sepa_debit_payments": {} + }, + "tos_acceptance": { + "date": null, + "ip": null, + "user_agent": null + }, + "type": "custom" + } + ] +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/application_fees.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/application_fees.json new file mode 100644 index 0000000000000..97bb806e6bbea --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/application_fees.json @@ -0,0 +1,138 @@ +{ + "object": "list", + "url": "/v1/application_fees", + "has_more": false, + "data": [ + { + "id": "fee_1B73DOKbnvuxQXGuhY8Aw0TN", + "object": "application_fee", + "account": "acct_164wxjKbnvuxQXGu", + "amount": 105, + "amount_refunded": 105, + "application": "ca_32D88BD1qLklliziD7gYQvctJIhWBSQ7", + "balance_transaction": "txn_1032HU2eZvKYlo2CEPtcnUvl", + "charge": "ch_1B73DOKbnvuxQXGurbwPqzsu", + "created": 1506609734, + "currency": "gbp", + "livemode": false, + "originating_transaction": null, + "refunded": true, + "refunds": { + "object": "list", + "data": [ + { + "id": "fr_1MBoV6KbnvuxQXGucP0PaPPO", + "object": "fee_refund", + "amount": 0, + "balance_transaction": null, + "created": 1670284508, + "currency": "usd", + "fee": "fee_1B73DOKbnvuxQXGuhY8Aw0TN", + "metadata": {} + }, + { + "id": "fr_1MBoU0KbnvuxQXGu2wCCz4Bb", + "object": "fee_refund", + "amount": 0, + "balance_transaction": null, + "created": 1670284441, + "currency": "usd", + "fee": "fee_1B73DOKbnvuxQXGuhY8Aw0TN", + "metadata": {} + }, + { + "id": "fr_1MBoRzKbnvuxQXGuvKkBKkSR", + "object": "fee_refund", + "amount": 0, + "balance_transaction": null, + "created": 1670284315, + "currency": "usd", + "fee": "fee_1B73DOKbnvuxQXGuhY8Aw0TN", + "metadata": {} + }, + { + "id": "fr_1MBoPOKbnvuxQXGueOBnke22", + "object": "fee_refund", + "amount": 0, + "balance_transaction": null, + "created": 1670284154, + "currency": "usd", + "fee": "fee_1B73DOKbnvuxQXGuhY8Aw0TN", + "metadata": {} + }, + { + "id": "fr_1MBoOGKbnvuxQXGu6EPQI2Zp", + "object": "fee_refund", + "amount": 0, + "balance_transaction": null, + "created": 1670284084, + "currency": "usd", + "fee": "fee_1B73DOKbnvuxQXGuhY8Aw0TN", + "metadata": {} + }, + { + "id": "fr_1MBoMUKbnvuxQXGu8Y0Peaoy", + "object": "fee_refund", + "amount": 0, + "balance_transaction": null, + "created": 1670283974, + "currency": "usd", + "fee": "fee_1B73DOKbnvuxQXGuhY8Aw0TN", + "metadata": {} + }, + { + "id": "fr_1MAgZBKbnvuxQXGuLTUrgGeq", + "object": "fee_refund", + "amount": 0, + "balance_transaction": null, + "created": 1670015681, + "currency": "usd", + "fee": "fee_1B73DOKbnvuxQXGuhY8Aw0TN", + "metadata": {} + }, + { + "id": "fr_1JAu9EKbnvuxQXGuRdZYkxVW", + "object": "fee_refund", + "amount": 0, + "balance_transaction": null, + "created": 1625738880, + "currency": "usd", + "fee": "fee_1B73DOKbnvuxQXGuhY8Aw0TN", + "metadata": { + "order_id": "6735" + } + }, + { + "id": "fr_1HZK0UKbnvuxQXGuS428gH0W", + "object": "fee_refund", + "amount": 0, + "balance_transaction": null, + "created": 1602005482, + "currency": "usd", + "fee": "fee_1B73DOKbnvuxQXGuhY8Aw0TN", + "metadata": {} + }, + { + "id": "fr_D0s7fGBKB40Twy", + "object": "fee_refund", + "amount": 138, + "balance_transaction": "txn_1CaqNg2eZvKYlo2C75cA3Euk", + "created": 1528486576, + "currency": "usd", + "fee": "fee_1B73DOKbnvuxQXGuhY8Aw0TN", + "metadata": {} + } + ], + "has_more": false, + "url": "/v1/application_fees/fee_1B73DOKbnvuxQXGuhY8Aw0TN/refunds" + }, + "source": { + "fee_type": "charge_application_fee", + "resource": { + "charge": "ch_1B73DOKbnvuxQXGurbwPqzsu", + "type": "charge" + } + } + } + ] +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/application_fees_refunds.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/application_fees_refunds.json new file mode 100644 index 0000000000000..47eacf5fad9fa --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/application_fees_refunds.json @@ -0,0 +1,17 @@ +{ + "object": "list", + "url": "/v1/application_fees/fr_1MtJRpKbnvuxQXGuM6Ww0D24/refunds", + "has_more": false, + "data": [ + { + "id": "fr_1MtJRpKbnvuxQXGuM6Ww0D24", + "object": "fee_refund", + "amount": 100, + "balance_transaction": null, + "created": 1680651573, + "currency": "usd", + "fee": "fee_1B73DOKbnvuxQXGuhY8Aw0TN", + "metadata": {} + } + ] +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/bank_accounts.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/bank_accounts.json new file mode 100644 index 0000000000000..bad75c218964e --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/bank_accounts.json @@ -0,0 +1,23 @@ +{ + "object": "list", + "url": "/v1/customers/cus_9s6XI9OFIdpjIg/bank_accounts", + "has_more": false, + "data": [ + { + "id": "ba_1MvoIJ2eZvKYlo2CO9f0MabO", + "object": "bank_account", + "account_holder_name": "Jane Austen", + "account_holder_type": "company", + "account_type": null, + "bank_name": "STRIPE TEST BANK", + "country": "US", + "currency": "usd", + "customer": "cus_9s6XI9OFIdpjIg", + "fingerprint": "1JWtPxqbdX5Gamtc", + "last4": "6789", + "metadata": {}, + "routing_number": "110000000", + "status": "new" + } + ] +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/customers_expand_data_source.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/customers_expand_data_source.json new file mode 100644 index 0000000000000..182aa0e44e9f3 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/customers_expand_data_source.json @@ -0,0 +1,43 @@ +{ + "object": "list", + "url": "/v1/customers", + "has_more": false, + "data": [ + { + "id": "cus_NffrFeUfNV2Hib", + "object": "customer", + "address": null, + "balance": 0, + "created": 1680893993, + "currency": null, + "default_source": null, + "delinquent": false, + "description": null, + "discount": null, + "email": "jennyrosen@example.com", + "invoice_prefix": "0759376C", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": null, + "footer": null, + "rendering_options": null + }, + "livemode": false, + "metadata": {}, + "name": "Jenny Rosen", + "next_invoice_sequence": 1, + "phone": null, + "preferred_locales": [], + "shipping": null, + "sources": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/customers/cus_NffrFeUfNV2Hib/sources" + }, + "tax_exempt": "none", + "test_clock": null + } + ] +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/events.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/events.json new file mode 100644 index 0000000000000..7f62598ea161b --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/events.json @@ -0,0 +1,58 @@ +{ + "object": "list", + "data": [ + { + "id": "evt_1OEiWvEcXtiJtvvhLaQOew6V", + "object": "event", + "api_version": "2020-08-27", + "created": 1700529213, + "data": { + "object": { + "object": "balance", + "available": [ + { + "amount": 518686, + "currency": "usd", + "source_types": { + "card": 518686 + } + } + ], + "connect_reserved": [ + { + "amount": 0, + "currency": "usd" + } + ], + "issuing": { + "available": [ + { + "amount": 150000, + "currency": "usd" + } + ] + }, + "livemode": false, + "pending": [ + { + "amount": 0, + "currency": "usd", + "source_types": { + "card": 0 + } + } + ] + } + }, + "livemode": false, + "pending_webhooks": 0, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "balance.available" + } + ], + "has_more": false, + "url": "/v1/events" +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/external_account_cards.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/external_account_cards.json new file mode 100644 index 0000000000000..c26bc36461cdb --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/external_account_cards.json @@ -0,0 +1,34 @@ +{ + "object": "list", + "url": "/v1/accounts/acct_1032D82eZvKYlo2C/external_accounts", + "has_more": false, + "data": [ + { + "id": "card_1NAz2x2eZvKYlo2C75wJ1YUs", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 8, + "exp_year": 2024, + "fingerprint": "Xt5EWLLDS7FJjR1c", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "redaction": null, + "tokenization_method": null, + "wallet": null, + "account": "acct_1032D82eZvKYlo2C" + } + ] +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/external_bank_accounts.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/external_bank_accounts.json new file mode 100644 index 0000000000000..a8704270de2f3 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/external_bank_accounts.json @@ -0,0 +1,23 @@ +{ + "object": "list", + "url": "/v1/accounts/acct_1032D82eZvKYlo2C/external_accounts", + "has_more": false, + "data": [ + { + "id": "ba_1NB1IV2eZvKYlo2CByiLrMWv", + "object": "bank_account", + "account_holder_name": "Jane Austen", + "account_holder_type": "company", + "account_type": null, + "bank_name": "STRIPE TEST BANK", + "country": "US", + "currency": "usd", + "fingerprint": "1JWtPxqbdX5Gamtc", + "last4": "6789", + "metadata": {}, + "routing_number": "110000000", + "status": "new", + "account": "acct_1032D82eZvKYlo2C" + } + ] +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/issuing_authorizations.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/issuing_authorizations.json new file mode 100644 index 0000000000000..cdf281ee2cd7a --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/issuing_authorizations.json @@ -0,0 +1,142 @@ +{ + "object": "list", + "url": "/v1/issuing/authorizations", + "has_more": false, + "data": [ + { + "id": "iauth_1JVXl82eZvKYlo2CPIiWlzrn", + "object": "issuing.authorization", + "amount": 382, + "amount_details": { + "atm_fee": null + }, + "approved": false, + "authorization_method": "online", + "balance_transactions": [], + "card": { + "id": "ic_1JDmgz2eZvKYlo2CRXlTsXj6", + "object": "issuing.card", + "brand": "Visa", + "cancellation_reason": null, + "cardholder": { + "id": "ich_1JDmfb2eZvKYlo2CwHUgaAxU", + "object": "issuing.cardholder", + "billing": { + "address": { + "city": "San Francisco", + "country": "US", + "line1": "123 Main Street", + "line2": null, + "postal_code": "94111", + "state": "CA" + } + }, + "company": null, + "created": 1626425119, + "email": "jenny.rosen@example.com", + "individual": null, + "livemode": false, + "metadata": {}, + "name": "Jenny Rosen", + "phone_number": "+18008675309", + "redaction": null, + "requirements": { + "disabled_reason": null, + "past_due": [] + }, + "spending_controls": { + "allowed_categories": [], + "blocked_categories": [], + "spending_limits": [], + "spending_limits_currency": null + }, + "status": "active", + "type": "individual" + }, + "created": 1626425206, + "currency": "usd", + "exp_month": 6, + "exp_year": 2024, + "last4": "8693", + "livemode": false, + "metadata": {}, + "redaction": null, + "replaced_by": null, + "replacement_for": null, + "replacement_reason": null, + "shipping": null, + "spending_controls": { + "allowed_categories": null, + "blocked_categories": null, + "spending_limits": [ + { + "amount": 50000, + "categories": [], + "interval": "daily" + } + ], + "spending_limits_currency": "usd" + }, + "status": "active", + "type": "virtual", + "wallets": { + "apple_pay": { + "eligible": true, + "ineligible_reason": null + }, + "google_pay": { + "eligible": true, + "ineligible_reason": null + }, + "primary_account_identifier": null + } + }, + "cardholder": "ich_1JDmfb2eZvKYlo2CwHUgaAxU", + "created": 1630657706, + "currency": "usd", + "livemode": false, + "merchant_amount": 382, + "merchant_currency": "usd", + "merchant_data": { + "category": "computer_software_stores", + "category_code": "5734", + "city": "SAN FRANCISCO", + "country": "US", + "name": "STRIPE", + "network_id": "1234567890", + "postal_code": "94103", + "state": "CA" + }, + "metadata": { + "order_id": "6735" + }, + "network_data": null, + "pending_request": null, + "redaction": null, + "request_history": [ + { + "amount": 382, + "amount_details": { + "atm_fee": null + }, + "approved": false, + "created": 1630657706, + "currency": "usd", + "merchant_amount": 382, + "merchant_currency": "usd", + "reason": "verification_failed", + "reason_message": null + } + ], + "status": "closed", + "transactions": [], + "verification_data": { + "address_line1_check": "not_provided", + "address_postal_code_check": "not_provided", + "cvc_check": "mismatch", + "expiry_check": "match" + }, + "wallet": null + } + ] +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/issuing_cards.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/issuing_cards.json new file mode 100644 index 0000000000000..1d5027df5457d --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/issuing_cards.json @@ -0,0 +1,82 @@ +{ + "object": "list", + "url": "/v1/issuing/cards", + "has_more": false, + "data": [ + { + "id": "ic_1MvSieLkdIwHu7ixn6uuO0Xu", + "object": "issuing.card", + "brand": "Visa", + "cancellation_reason": null, + "cardholder": { + "id": "ich_1MsKAB2eZvKYlo2C3eZ2BdvK", + "object": "issuing.cardholder", + "billing": { + "address": { + "city": "Anytown", + "country": "US", + "line1": "123 Main Street", + "line2": null, + "postal_code": "12345", + "state": "CA" + } + }, + "company": null, + "created": 1680415995, + "email": null, + "individual": null, + "livemode": false, + "metadata": {}, + "name": "John Doe", + "phone_number": null, + "requirements": { + "disabled_reason": "requirements.past_due", + "past_due": [ + "individual.card_issuing.user_terms_acceptance.ip", + "individual.card_issuing.user_terms_acceptance.date", + "individual.first_name", + "individual.last_name" + ] + }, + "spending_controls": { + "allowed_categories": [], + "blocked_categories": [], + "spending_limits": [], + "spending_limits_currency": null + }, + "status": "active", + "type": "individual" + }, + "created": 1681163868, + "currency": "usd", + "exp_month": 8, + "exp_year": 2024, + "last4": "4242", + "livemode": false, + "metadata": {}, + "replaced_by": null, + "replacement_for": null, + "replacement_reason": null, + "shipping": null, + "spending_controls": { + "allowed_categories": null, + "blocked_categories": null, + "spending_limits": [], + "spending_limits_currency": null + }, + "status": "active", + "type": "virtual", + "wallets": { + "apple_pay": { + "eligible": false, + "ineligible_reason": "missing_cardholder_contact" + }, + "google_pay": { + "eligible": false, + "ineligible_reason": "missing_cardholder_contact" + }, + "primary_account_identifier": null + } + } + ] +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/issuing_transactions.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/issuing_transactions.json new file mode 100644 index 0000000000000..bbd790f318eb3 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/issuing_transactions.json @@ -0,0 +1,38 @@ +{ + "object": "list", + "url": "/v1/issuing/transactions", + "has_more": false, + "data": [ + { + "id": "ipi_1MzFN1K8F4fqH0lBmFq8CjbU", + "object": "issuing.transaction", + "amount": -100, + "amount_details": { + "atm_fee": null + }, + "authorization": "iauth_1MzFMzK8F4fqH0lBc9VdaZUp", + "balance_transaction": "txn_1MzFN1K8F4fqH0lBQPtqUmJN", + "card": "ic_1MzFMxK8F4fqH0lBjIUITRYi", + "cardholder": "ich_1MzFMxK8F4fqH0lBXnFW0ROG", + "created": 1682065867, + "currency": "usd", + "dispute": null, + "livemode": false, + "merchant_amount": -100, + "merchant_currency": "usd", + "merchant_data": { + "category": "computer_software_stores", + "category_code": "5734", + "city": "SAN FRANCISCO", + "country": "US", + "name": "WWWW.BROWSEBUG.BIZ", + "network_id": "1234567890", + "postal_code": "94103", + "state": "CA" + }, + "metadata": {}, + "type": "capture", + "wallet": null + } + ] +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/payment_methods.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/payment_methods.json new file mode 100644 index 0000000000000..59ced3939e62b --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/payment_methods.json @@ -0,0 +1,52 @@ +{ + "object": "list", + "url": "/v1/payment_methods", + "has_more": false, + "data": [ + { + "id": "pm_1NO6mA2eZvKYlo2CEydeHsKT", + "object": "payment_method", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "unchecked" + }, + "country": "US", + "exp_month": 8, + "exp_year": 2024, + "fingerprint": "Xt5EWLLDS7FJjR1c", + "funding": "credit", + "generated_from": null, + "last4": "4242", + "networks": { + "available": ["visa"], + "preferred": null + }, + "three_d_secure_usage": { + "supported": true + }, + "wallet": null + }, + "created": 1687991030, + "customer": "cus_9s6XKzkNRiz8i3", + "livemode": false, + "metadata": {}, + "type": "card" + } + ] +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/persons.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/persons.json new file mode 100644 index 0000000000000..f7ede1e42817e --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/persons.json @@ -0,0 +1,65 @@ +{ + "object": "list", + "url": "/v1/accounts/acct_1G9HZLIEn49ers/persons", + "has_more": false, + "data": [ + { + "id": "person_1MqjB62eZvKYlo2CaeEJzK13", + "person": "person_1MqjB62eZvKYlo2CaeEJzK13", + "object": "person", + "account": "acct_1G9HZLIEn49ers", + "created": 1680035496, + "dob": { + "day": null, + "month": null, + "year": null + }, + "first_name": "Brock", + "future_requirements": { + "alternatives": [], + "currently_due": [], + "errors": [], + "eventually_due": [], + "past_due": [], + "pending_verification": [] + }, + "id_number_provided": false, + "last_name": "Purdy", + "metadata": {}, + "relationship": { + "director": false, + "executive": true, + "owner": false, + "percent_ownership": null, + "representative": false, + "title": null + }, + "requirements": { + "alternatives": [], + "currently_due": [], + "errors": [], + "eventually_due": [], + "past_due": [], + "pending_verification": [] + }, + "ssn_last_4_provided": false, + "verification": { + "additional_document": { + "back": null, + "details": null, + "details_code": null, + "front": null + }, + "details": null, + "details_code": null, + "document": { + "back": null, + "details": null, + "details_code": null, + "front": null + }, + "status": "verified" + } + } + ] +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/radar_early_fraud_warnings.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/radar_early_fraud_warnings.json new file mode 100644 index 0000000000000..8da264f5b4751 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/radar_early_fraud_warnings.json @@ -0,0 +1,16 @@ +{ + "object": "list", + "url": "/v1/radar/early_fraud_warnings", + "has_more": false, + "data": [ + { + "id": "issfr_1NnrwHBw2dPENLoi9lnhV3RQ", + "object": "radar.early_fraud_warning", + "actionable": true, + "charge": "ch_1234", + "created": 123456789, + "fraud_type": "misc", + "livemode": false + } + ] +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/refunds.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/refunds.json new file mode 100644 index 0000000000000..af20ee7480d30 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/refunds.json @@ -0,0 +1,32 @@ +{ + "object": "list", + "url": "/v1/refunds", + "has_more": false, + "data": [ + { + "id": "re_1Nispe2eZvKYlo2Cd31jOCgZ", + "object": "refund", + "amount": 1000, + "balance_transaction": "txn_1Nispe2eZvKYlo2CYezqFhEx", + "charge": "ch_1NirD82eZvKYlo2CIvbtLWuY", + "created": 1692942318, + "currency": "usd", + "destination_details": { + "card": { + "reference": "123456789012", + "reference_status": "available", + "reference_type": "acquirer_reference_number", + "type": "refund" + }, + "type": "card" + }, + "metadata": {}, + "payment_intent": "pi_1GszsK2eZvKYlo2CfhZyoZLp", + "reason": null, + "receipt_number": null, + "source_transfer_reversal": null, + "status": "succeeded", + "transfer_reversal": null + } + ] +} diff --git a/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/reviews.json b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/reviews.json new file mode 100644 index 0000000000000..0e41d57d3bb11 --- /dev/null +++ b/airbyte-integrations/connectors/source-stripe/unit_tests/resource/http/response/reviews.json @@ -0,0 +1,23 @@ +{ + "object": "list", + "url": "/v1/reviews", + "has_more": false, + "data": [ + { + "id": "prv_1NVyFt2eZvKYlo2CjubqF1xm", + "object": "review", + "billing_zip": null, + "charge": null, + "closed_reason": null, + "created": 1689864901, + "ip_address": null, + "ip_address_location": null, + "livemode": false, + "open": true, + "opened_reason": "rule", + "payment_intent": "pi_3NVy8c2eZvKYlo2C055h7pkd", + "reason": "rule", + "session": null + } + ] +} diff --git a/docs/integrations/sources/stripe.md b/docs/integrations/sources/stripe.md index 2e517830bdf7b..1b3f61baf99f8 100644 --- a/docs/integrations/sources/stripe.md +++ b/docs/integrations/sources/stripe.md @@ -216,93 +216,94 @@ Each record is marked with `is_deleted` flag when the appropriate event happens | Version | Date | Pull Request | Subject | |:--------|:-----------|:-------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 5.1.2 | 2024-01-04 | [33414](https://github.com/airbytehq/airbyte/pull/33414) | Prepare for airbyte-lib | +| 5.1.3 | 2023-12-18 | [33306](https://github.com/airbytehq/airbyte/pull/33306/) | Adding integration tests | +| 5.1.2 | 2024-01-04 | [33414](https://github.com/airbytehq/airbyte/pull/33414) | Prepare for airbyte-lib | | 5.1.1 | 2024-01-04 | [33926](https://github.com/airbytehq/airbyte/pull/33926/) | Update endpoint for `bank_accounts` stream | | 5.1.0 | 2023-12-11 | [32908](https://github.com/airbytehq/airbyte/pull/32908/) | Read full refresh streams concurrently | | 5.0.2 | 2023-12-01 | [33038](https://github.com/airbytehq/airbyte/pull/33038) | Add stream slice logging for SubStream | -| 5.0.1 | 2023-11-17 | [32638](https://github.com/airbytehq/airbyte/pull/32638/) | Availability stretegy: check availability of both endpoints (if applicable) - common API + events API | -| 5.0.0 | 2023-11-16 | [32286](https://github.com/airbytehq/airbyte/pull/32286/) | Fix multiple issues regarding usage of the incremental sync mode for the `Refunds`, `CheckoutSessions`, `CheckoutSessionsLineItems` streams. Fix schemas for the streams: `Invoices`, `Subscriptions`, `SubscriptionSchedule` | -| 4.5.4 | 2023-11-16 | [32284](https://github.com/airbytehq/airbyte/pull/32284/) | Enable client-side rate limiting | -| 4.5.3 | 2023-11-14 | [32473](https://github.com/airbytehq/airbyte/pull/32473/) | Have all full_refresh stream syncs be concurrent | -| 4.5.2 | 2023-11-03 | [32146](https://github.com/airbytehq/airbyte/pull/32146/) | Fix multiple BankAccount issues | -| 4.5.1 | 2023-11-01 | [32056](https://github.com/airbytehq/airbyte/pull/32056/) | Use CDK version 0.52.8 | -| 4.5.0 | 2023-10-25 | [31327](https://github.com/airbytehq/airbyte/pull/31327/) | Use concurrent CDK when running in full-refresh | -| 4.4.2 | 2023-10-24 | [31764](https://github.com/airbytehq/airbyte/pull/31764) | Base image migration: remove Dockerfile and use the python-connector-base image | -| 4.4.1 | 2023-10-18 | [31553](https://github.com/airbytehq/airbyte/pull/31553) | Adjusted `Setup Attempts` and extended `Checkout Sessions` stream schemas | -| 4.4.0 | 2023-10-04 | [31046](https://github.com/airbytehq/airbyte/pull/31046) | Added margins field to invoice_line_items stream. | -| 4.3.1 | 2023-09-27 | [30800](https://github.com/airbytehq/airbyte/pull/30800) | Handle permission issues a non breaking | -| 4.3.0 | 2023-09-26 | [30752](https://github.com/airbytehq/airbyte/pull/30752) | Do not sync upcoming invoices, extend stream schemas | -| 4.2.0 | 2023-09-21 | [30660](https://github.com/airbytehq/airbyte/pull/30660) | Fix updated state for the incremental syncs | -| 4.1.1 | 2023-09-15 | [30494](https://github.com/airbytehq/airbyte/pull/30494) | Fix datatype of invoices.lines property | -| 4.1.0 | 2023-08-29 | [29950](https://github.com/airbytehq/airbyte/pull/29950) | Implement incremental deletes, add suggested streams | -| 4.0.1 | 2023-09-07 | [30254](https://github.com/airbytehq/airbyte/pull/30254) | Fix cursorless incremental streams | -| 4.0.0 | 2023-08-15 | [29330](https://github.com/airbytehq/airbyte/pull/29330) | Implement incremental syncs based on date of update | -| 3.17.4 | 2023-08-15 | [29425](https://github.com/airbytehq/airbyte/pull/29425) | Revert 3.17.3 | -| 3.17.3 | 2023-08-01 | [28911](https://github.com/airbytehq/airbyte/pull/28911) | Revert 3.17.2 and fix atm_fee property | -| 3.17.2 | 2023-08-01 | [28911](https://github.com/airbytehq/airbyte/pull/28911) | Fix stream schemas, remove custom 403 error handling | -| 3.17.1 | 2023-08-01 | [28887](https://github.com/airbytehq/airbyte/pull/28887) | Fix `Invoices` schema | -| 3.17.0 | 2023-07-28 | [26127](https://github.com/airbytehq/airbyte/pull/26127) | Add `Prices` stream | -| 3.16.0 | 2023-07-27 | [28776](https://github.com/airbytehq/airbyte/pull/28776) | Add new fields to stream schemas | -| 3.15.0 | 2023-07-09 | [28709](https://github.com/airbytehq/airbyte/pull/28709) | Remove duplicate streams | -| 3.14.0 | 2023-07-09 | [27217](https://github.com/airbytehq/airbyte/pull/27217) | Add `ShippingRates` stream | -| 3.13.0 | 2023-07-18 | [28466](https://github.com/airbytehq/airbyte/pull/28466) | Pin source API version | -| 3.12.0 | 2023-05-20 | [26208](https://github.com/airbytehq/airbyte/pull/26208) | Add new stream `Persons` | -| 3.11.0 | 2023-06-26 | [27734](https://github.com/airbytehq/airbyte/pull/27734) | License Update: Elv2 stream | -| 3.10.0 | 2023-06-22 | [27132](https://github.com/airbytehq/airbyte/pull/27132) | Add `CreditNotes` stream | -| 3.9.1 | 2023-06-20 | [27522](https://github.com/airbytehq/airbyte/pull/27522) | Fix formatting | -| 3.9.0 | 2023-06-19 | [27362](https://github.com/airbytehq/airbyte/pull/27362) | Add new Streams: Transfer Reversals, Setup Attempts, Usage Records, Transactions | -| 3.8.0 | 2023-06-12 | [27238](https://github.com/airbytehq/airbyte/pull/27238) | Add `Topups` stream; Add `Files` stream; Add `FileLinks` stream | -| 3.7.0 | 2023-06-06 | [27083](https://github.com/airbytehq/airbyte/pull/27083) | Add new Streams: Authorizations, Cardholders, Cards, Payment Methods, Reviews | -| 3.6.0 | 2023-05-24 | [25893](https://github.com/airbytehq/airbyte/pull/25893) | Add `ApplicationFeesRefunds` stream with parent `ApplicationFees` | -| 3.5.0 | 2023-05-20 | [22859](https://github.com/airbytehq/airbyte/pull/22859) | Add stream `Early Fraud Warnings` | -| 3.4.3 | 2023-05-10 | [25965](https://github.com/airbytehq/airbyte/pull/25965) | Fix Airbyte date-time data-types | -| 3.4.2 | 2023-05-04 | [25795](https://github.com/airbytehq/airbyte/pull/25795) | Added `CDK TypeTransformer` to guarantee declared JSON Schema data-types | -| 3.4.1 | 2023-04-24 | [23389](https://github.com/airbytehq/airbyte/pull/23389) | Add `customer_tax_ids` to `Invoices` | -| 3.4.0 | 2023-03-20 | [23963](https://github.com/airbytehq/airbyte/pull/23963) | Add `SetupIntents` stream | -| 3.3.0 | 2023-04-12 | [25136](https://github.com/airbytehq/airbyte/pull/25136) | Add stream `Accounts` | -| 3.2.0 | 2023-04-10 | [23624](https://github.com/airbytehq/airbyte/pull/23624) | Add new stream `Subscription Schedule` | -| 3.1.0 | 2023-03-10 | [19906](https://github.com/airbytehq/airbyte/pull/19906) | Expand `tiers` when syncing `Plans` streams | -| 3.0.5 | 2023-03-25 | [22866](https://github.com/airbytehq/airbyte/pull/22866) | Specified date formatting in specification | -| 3.0.4 | 2023-03-24 | [24471](https://github.com/airbytehq/airbyte/pull/24471) | Fix stream slices for single sliced streams | -| 3.0.3 | 2023-03-17 | [24179](https://github.com/airbytehq/airbyte/pull/24179) | Get customer's attributes safely | -| 3.0.2 | 2023-03-13 | [24051](https://github.com/airbytehq/airbyte/pull/24051) | Cache `customers` stream; Do not request transactions of customers with zero balance. | -| 3.0.1 | 2023-02-22 | [22898](https://github.com/airbytehq/airbyte/pull/22898) | Add missing column to Subscriptions stream | -| 3.0.0 | 2023-02-21 | [23295](https://github.com/airbytehq/airbyte/pull/23295) | Fix invoice schema | -| 2.0.0 | 2023-02-14 | [22312](https://github.com/airbytehq/airbyte/pull/22312) | Another fix of `Invoices` stream schema + Remove http urls from openapi_spec.json | -| 1.0.2 | 2023-02-09 | [22659](https://github.com/airbytehq/airbyte/pull/22659) | Set `AvailabilityStrategy` for all streams | -| 1.0.1 | 2023-01-27 | [22042](https://github.com/airbytehq/airbyte/pull/22042) | Set `AvailabilityStrategy` for streams explicitly to `None` | -| 1.0.0 | 2023-01-25 | [21858](https://github.com/airbytehq/airbyte/pull/21858) | Update the `Subscriptions` and `Invoices` stream schemas | -| 0.1.40 | 2022-10-20 | [18228](https://github.com/airbytehq/airbyte/pull/18228) | Update the `PaymentIntents` stream schema | -| 0.1.39 | 2022-09-28 | [17304](https://github.com/airbytehq/airbyte/pull/17304) | Migrate to per-stream states. | -| 0.1.38 | 2022-09-09 | [16537](https://github.com/airbytehq/airbyte/pull/16537) | Fix `redeem_by` field type for `customers` stream | -| 0.1.37 | 2022-08-16 | [15686](https://github.com/airbytehq/airbyte/pull/15686) | Fix the bug when the stream couldn't be fetched due to limited permission set, if so - it should be skipped | -| 0.1.36 | 2022-08-04 | [15292](https://github.com/airbytehq/airbyte/pull/15292) | Implement slicing | -| 0.1.35 | 2022-07-21 | [14924](https://github.com/airbytehq/airbyte/pull/14924) | Remove `additionalProperties` field from spec and schema | -| 0.1.34 | 2022-07-01 | [14357](https://github.com/airbytehq/airbyte/pull/14357) | Add external account streams - | -| 0.1.33 | 2022-06-06 | [13449](https://github.com/airbytehq/airbyte/pull/13449) | Add semi-incremental support for CheckoutSessions and CheckoutSessionsLineItems streams, fixed big in StripeSubStream, added unittests, updated docs | -| 0.1.32 | 2022-04-30 | [12500](https://github.com/airbytehq/airbyte/pull/12500) | Improve input configuration copy | -| 0.1.31 | 2022-04-20 | [12230](https://github.com/airbytehq/airbyte/pull/12230) | Update connector to use a `spec.yaml` | -| 0.1.30 | 2022-03-21 | [11286](https://github.com/airbytehq/airbyte/pull/11286) | Minor corrections to documentation and connector specification | -| 0.1.29 | 2022-03-08 | [10359](https://github.com/airbytehq/airbyte/pull/10359) | Improved performance for streams with substreams: invoice_line_items, subscription_items, bank_accounts | -| 0.1.28 | 2022-02-08 | [10165](https://github.com/airbytehq/airbyte/pull/10165) | Improve 404 handling for `CheckoutSessionsLineItems` stream | -| 0.1.27 | 2021-12-28 | [9148](https://github.com/airbytehq/airbyte/pull/9148) | Fix `date`, `arrival\_date` fields | -| 0.1.26 | 2021-12-21 | [8992](https://github.com/airbytehq/airbyte/pull/8992) | Fix type `events.request` in schema | -| 0.1.25 | 2021-11-25 | [8250](https://github.com/airbytehq/airbyte/pull/8250) | Rearrange setup fields | -| 0.1.24 | 2021-11-08 | [7729](https://github.com/airbytehq/airbyte/pull/7729) | Include tax data in `checkout_sessions_line_items` stream | -| 0.1.23 | 2021-11-08 | [7729](https://github.com/airbytehq/airbyte/pull/7729) | Correct `payment_intents` schema | -| 0.1.22 | 2021-11-05 | [7345](https://github.com/airbytehq/airbyte/pull/7345) | Add 3 new streams | -| 0.1.21 | 2021-10-07 | [6841](https://github.com/airbytehq/airbyte/pull/6841) | Fix missing `start_date` argument + update json files for SAT | -| 0.1.20 | 2021-09-30 | [6017](https://github.com/airbytehq/airbyte/pull/6017) | Add lookback_window_days parameter | -| 0.1.19 | 2021-09-27 | [6466](https://github.com/airbytehq/airbyte/pull/6466) | Use `start_date` parameter in incremental streams | -| 0.1.18 | 2021-09-14 | [6004](https://github.com/airbytehq/airbyte/pull/6004) | Fix coupons and subscriptions stream schemas by removing incorrect timestamp formatting | -| 0.1.17 | 2021-09-14 | [6004](https://github.com/airbytehq/airbyte/pull/6004) | Add `PaymentIntents` stream | -| 0.1.16 | 2021-07-28 | [4980](https://github.com/airbytehq/airbyte/pull/4980) | Remove Updated field from schemas | -| 0.1.15 | 2021-07-21 | [4878](https://github.com/airbytehq/airbyte/pull/4878) | Fix incorrect percent_off and discounts data filed types | -| 0.1.14 | 2021-07-09 | [4669](https://github.com/airbytehq/airbyte/pull/4669) | Subscriptions Stream now returns all kinds of subscriptions \(including expired and canceled\) | -| 0.1.13 | 2021-07-03 | [4528](https://github.com/airbytehq/airbyte/pull/4528) | Remove regex for acc validation | -| 0.1.12 | 2021-06-08 | [3973](https://github.com/airbytehq/airbyte/pull/3973) | Add `AIRBYTE_ENTRYPOINT` for Kubernetes support | -| 0.1.11 | 2021-05-30 | [3744](https://github.com/airbytehq/airbyte/pull/3744) | Fix types in schema | -| 0.1.10 | 2021-05-28 | [3728](https://github.com/airbytehq/airbyte/pull/3728) | Update data types to be number instead of int | -| 0.1.9 | 2021-05-13 | [3367](https://github.com/airbytehq/airbyte/pull/3367) | Add acceptance tests for connected accounts | -| 0.1.8 | 2021-05-11 | [3566](https://github.com/airbytehq/airbyte/pull/3368) | Bump CDK connectors | +| 5.0.1 | 2023-11-17 | [32638](https://github.com/airbytehq/airbyte/pull/32638/) | Availability stretegy: check availability of both endpoints (if applicable) - common API + events API | +| 5.0.0 | 2023-11-16 | [32286](https://github.com/airbytehq/airbyte/pull/32286/) | Fix multiple issues regarding usage of the incremental sync mode for the `Refunds`, `CheckoutSessions`, `CheckoutSessionsLineItems` streams. Fix schemas for the streams: `Invoices`, `Subscriptions`, `SubscriptionSchedule` | +| 4.5.4 | 2023-11-16 | [32284](https://github.com/airbytehq/airbyte/pull/32284/) | Enable client-side rate limiting | +| 4.5.3 | 2023-11-14 | [32473](https://github.com/airbytehq/airbyte/pull/32473/) | Have all full_refresh stream syncs be concurrent | +| 4.5.2 | 2023-11-03 | [32146](https://github.com/airbytehq/airbyte/pull/32146/) | Fix multiple BankAccount issues | +| 4.5.1 | 2023-11-01 | [32056](https://github.com/airbytehq/airbyte/pull/32056/) | Use CDK version 0.52.8 | +| 4.5.0 | 2023-10-25 | [31327](https://github.com/airbytehq/airbyte/pull/31327/) | Use concurrent CDK when running in full-refresh | +| 4.4.2 | 2023-10-24 | [31764](https://github.com/airbytehq/airbyte/pull/31764) | Base image migration: remove Dockerfile and use the python-connector-base image | +| 4.4.1 | 2023-10-18 | [31553](https://github.com/airbytehq/airbyte/pull/31553) | Adjusted `Setup Attempts` and extended `Checkout Sessions` stream schemas | +| 4.4.0 | 2023-10-04 | [31046](https://github.com/airbytehq/airbyte/pull/31046) | Added margins field to invoice_line_items stream. | +| 4.3.1 | 2023-09-27 | [30800](https://github.com/airbytehq/airbyte/pull/30800) | Handle permission issues a non breaking | +| 4.3.0 | 2023-09-26 | [30752](https://github.com/airbytehq/airbyte/pull/30752) | Do not sync upcoming invoices, extend stream schemas | +| 4.2.0 | 2023-09-21 | [30660](https://github.com/airbytehq/airbyte/pull/30660) | Fix updated state for the incremental syncs | +| 4.1.1 | 2023-09-15 | [30494](https://github.com/airbytehq/airbyte/pull/30494) | Fix datatype of invoices.lines property | +| 4.1.0 | 2023-08-29 | [29950](https://github.com/airbytehq/airbyte/pull/29950) | Implement incremental deletes, add suggested streams | +| 4.0.1 | 2023-09-07 | [30254](https://github.com/airbytehq/airbyte/pull/30254) | Fix cursorless incremental streams | +| 4.0.0 | 2023-08-15 | [29330](https://github.com/airbytehq/airbyte/pull/29330) | Implement incremental syncs based on date of update | +| 3.17.4 | 2023-08-15 | [29425](https://github.com/airbytehq/airbyte/pull/29425) | Revert 3.17.3 | +| 3.17.3 | 2023-08-01 | [28911](https://github.com/airbytehq/airbyte/pull/28911) | Revert 3.17.2 and fix atm_fee property | +| 3.17.2 | 2023-08-01 | [28911](https://github.com/airbytehq/airbyte/pull/28911) | Fix stream schemas, remove custom 403 error handling | +| 3.17.1 | 2023-08-01 | [28887](https://github.com/airbytehq/airbyte/pull/28887) | Fix `Invoices` schema | +| 3.17.0 | 2023-07-28 | [26127](https://github.com/airbytehq/airbyte/pull/26127) | Add `Prices` stream | +| 3.16.0 | 2023-07-27 | [28776](https://github.com/airbytehq/airbyte/pull/28776) | Add new fields to stream schemas | +| 3.15.0 | 2023-07-09 | [28709](https://github.com/airbytehq/airbyte/pull/28709) | Remove duplicate streams | +| 3.14.0 | 2023-07-09 | [27217](https://github.com/airbytehq/airbyte/pull/27217) | Add `ShippingRates` stream | +| 3.13.0 | 2023-07-18 | [28466](https://github.com/airbytehq/airbyte/pull/28466) | Pin source API version | +| 3.12.0 | 2023-05-20 | [26208](https://github.com/airbytehq/airbyte/pull/26208) | Add new stream `Persons` | +| 3.11.0 | 2023-06-26 | [27734](https://github.com/airbytehq/airbyte/pull/27734) | License Update: Elv2 stream | +| 3.10.0 | 2023-06-22 | [27132](https://github.com/airbytehq/airbyte/pull/27132) | Add `CreditNotes` stream | +| 3.9.1 | 2023-06-20 | [27522](https://github.com/airbytehq/airbyte/pull/27522) | Fix formatting | +| 3.9.0 | 2023-06-19 | [27362](https://github.com/airbytehq/airbyte/pull/27362) | Add new Streams: Transfer Reversals, Setup Attempts, Usage Records, Transactions | +| 3.8.0 | 2023-06-12 | [27238](https://github.com/airbytehq/airbyte/pull/27238) | Add `Topups` stream; Add `Files` stream; Add `FileLinks` stream | +| 3.7.0 | 2023-06-06 | [27083](https://github.com/airbytehq/airbyte/pull/27083) | Add new Streams: Authorizations, Cardholders, Cards, Payment Methods, Reviews | +| 3.6.0 | 2023-05-24 | [25893](https://github.com/airbytehq/airbyte/pull/25893) | Add `ApplicationFeesRefunds` stream with parent `ApplicationFees` | +| 3.5.0 | 2023-05-20 | [22859](https://github.com/airbytehq/airbyte/pull/22859) | Add stream `Early Fraud Warnings` | +| 3.4.3 | 2023-05-10 | [25965](https://github.com/airbytehq/airbyte/pull/25965) | Fix Airbyte date-time data-types | +| 3.4.2 | 2023-05-04 | [25795](https://github.com/airbytehq/airbyte/pull/25795) | Added `CDK TypeTransformer` to guarantee declared JSON Schema data-types | +| 3.4.1 | 2023-04-24 | [23389](https://github.com/airbytehq/airbyte/pull/23389) | Add `customer_tax_ids` to `Invoices` | +| 3.4.0 | 2023-03-20 | [23963](https://github.com/airbytehq/airbyte/pull/23963) | Add `SetupIntents` stream | +| 3.3.0 | 2023-04-12 | [25136](https://github.com/airbytehq/airbyte/pull/25136) | Add stream `Accounts` | +| 3.2.0 | 2023-04-10 | [23624](https://github.com/airbytehq/airbyte/pull/23624) | Add new stream `Subscription Schedule` | +| 3.1.0 | 2023-03-10 | [19906](https://github.com/airbytehq/airbyte/pull/19906) | Expand `tiers` when syncing `Plans` streams | +| 3.0.5 | 2023-03-25 | [22866](https://github.com/airbytehq/airbyte/pull/22866) | Specified date formatting in specification | +| 3.0.4 | 2023-03-24 | [24471](https://github.com/airbytehq/airbyte/pull/24471) | Fix stream slices for single sliced streams | +| 3.0.3 | 2023-03-17 | [24179](https://github.com/airbytehq/airbyte/pull/24179) | Get customer's attributes safely | +| 3.0.2 | 2023-03-13 | [24051](https://github.com/airbytehq/airbyte/pull/24051) | Cache `customers` stream; Do not request transactions of customers with zero balance. | +| 3.0.1 | 2023-02-22 | [22898](https://github.com/airbytehq/airbyte/pull/22898) | Add missing column to Subscriptions stream | +| 3.0.0 | 2023-02-21 | [23295](https://github.com/airbytehq/airbyte/pull/23295) | Fix invoice schema | +| 2.0.0 | 2023-02-14 | [22312](https://github.com/airbytehq/airbyte/pull/22312) | Another fix of `Invoices` stream schema + Remove http urls from openapi_spec.json | +| 1.0.2 | 2023-02-09 | [22659](https://github.com/airbytehq/airbyte/pull/22659) | Set `AvailabilityStrategy` for all streams | +| 1.0.1 | 2023-01-27 | [22042](https://github.com/airbytehq/airbyte/pull/22042) | Set `AvailabilityStrategy` for streams explicitly to `None` | +| 1.0.0 | 2023-01-25 | [21858](https://github.com/airbytehq/airbyte/pull/21858) | Update the `Subscriptions` and `Invoices` stream schemas | +| 0.1.40 | 2022-10-20 | [18228](https://github.com/airbytehq/airbyte/pull/18228) | Update the `PaymentIntents` stream schema | +| 0.1.39 | 2022-09-28 | [17304](https://github.com/airbytehq/airbyte/pull/17304) | Migrate to per-stream states. | +| 0.1.38 | 2022-09-09 | [16537](https://github.com/airbytehq/airbyte/pull/16537) | Fix `redeem_by` field type for `customers` stream | +| 0.1.37 | 2022-08-16 | [15686](https://github.com/airbytehq/airbyte/pull/15686) | Fix the bug when the stream couldn't be fetched due to limited permission set, if so - it should be skipped | +| 0.1.36 | 2022-08-04 | [15292](https://github.com/airbytehq/airbyte/pull/15292) | Implement slicing | +| 0.1.35 | 2022-07-21 | [14924](https://github.com/airbytehq/airbyte/pull/14924) | Remove `additionalProperties` field from spec and schema | +| 0.1.34 | 2022-07-01 | [14357](https://github.com/airbytehq/airbyte/pull/14357) | Add external account streams - | +| 0.1.33 | 2022-06-06 | [13449](https://github.com/airbytehq/airbyte/pull/13449) | Add semi-incremental support for CheckoutSessions and CheckoutSessionsLineItems streams, fixed big in StripeSubStream, added unittests, updated docs | +| 0.1.32 | 2022-04-30 | [12500](https://github.com/airbytehq/airbyte/pull/12500) | Improve input configuration copy | +| 0.1.31 | 2022-04-20 | [12230](https://github.com/airbytehq/airbyte/pull/12230) | Update connector to use a `spec.yaml` | +| 0.1.30 | 2022-03-21 | [11286](https://github.com/airbytehq/airbyte/pull/11286) | Minor corrections to documentation and connector specification | +| 0.1.29 | 2022-03-08 | [10359](https://github.com/airbytehq/airbyte/pull/10359) | Improved performance for streams with substreams: invoice_line_items, subscription_items, bank_accounts | +| 0.1.28 | 2022-02-08 | [10165](https://github.com/airbytehq/airbyte/pull/10165) | Improve 404 handling for `CheckoutSessionsLineItems` stream | +| 0.1.27 | 2021-12-28 | [9148](https://github.com/airbytehq/airbyte/pull/9148) | Fix `date`, `arrival\_date` fields | +| 0.1.26 | 2021-12-21 | [8992](https://github.com/airbytehq/airbyte/pull/8992) | Fix type `events.request` in schema | +| 0.1.25 | 2021-11-25 | [8250](https://github.com/airbytehq/airbyte/pull/8250) | Rearrange setup fields | +| 0.1.24 | 2021-11-08 | [7729](https://github.com/airbytehq/airbyte/pull/7729) | Include tax data in `checkout_sessions_line_items` stream | +| 0.1.23 | 2021-11-08 | [7729](https://github.com/airbytehq/airbyte/pull/7729) | Correct `payment_intents` schema | +| 0.1.22 | 2021-11-05 | [7345](https://github.com/airbytehq/airbyte/pull/7345) | Add 3 new streams | +| 0.1.21 | 2021-10-07 | [6841](https://github.com/airbytehq/airbyte/pull/6841) | Fix missing `start_date` argument + update json files for SAT | +| 0.1.20 | 2021-09-30 | [6017](https://github.com/airbytehq/airbyte/pull/6017) | Add lookback_window_days parameter | +| 0.1.19 | 2021-09-27 | [6466](https://github.com/airbytehq/airbyte/pull/6466) | Use `start_date` parameter in incremental streams | +| 0.1.18 | 2021-09-14 | [6004](https://github.com/airbytehq/airbyte/pull/6004) | Fix coupons and subscriptions stream schemas by removing incorrect timestamp formatting | +| 0.1.17 | 2021-09-14 | [6004](https://github.com/airbytehq/airbyte/pull/6004) | Add `PaymentIntents` stream | +| 0.1.16 | 2021-07-28 | [4980](https://github.com/airbytehq/airbyte/pull/4980) | Remove Updated field from schemas | +| 0.1.15 | 2021-07-21 | [4878](https://github.com/airbytehq/airbyte/pull/4878) | Fix incorrect percent_off and discounts data filed types | +| 0.1.14 | 2021-07-09 | [4669](https://github.com/airbytehq/airbyte/pull/4669) | Subscriptions Stream now returns all kinds of subscriptions \(including expired and canceled\) | +| 0.1.13 | 2021-07-03 | [4528](https://github.com/airbytehq/airbyte/pull/4528) | Remove regex for acc validation | +| 0.1.12 | 2021-06-08 | [3973](https://github.com/airbytehq/airbyte/pull/3973) | Add `AIRBYTE_ENTRYPOINT` for Kubernetes support | +| 0.1.11 | 2021-05-30 | [3744](https://github.com/airbytehq/airbyte/pull/3744) | Fix types in schema | +| 0.1.10 | 2021-05-28 | [3728](https://github.com/airbytehq/airbyte/pull/3728) | Update data types to be number instead of int | +| 0.1.9 | 2021-05-13 | [3367](https://github.com/airbytehq/airbyte/pull/3367) | Add acceptance tests for connected accounts | +| 0.1.8 | 2021-05-11 | [3566](https://github.com/airbytehq/airbyte/pull/3368) | Bump CDK connectors | From 6c27626a3a2a8dab98581f84c91a545c54342979 Mon Sep 17 00:00:00 2001 From: Patrick Nilan Date: Thu, 11 Jan 2024 09:19:01 -0800 Subject: [PATCH 061/574] Zendesk Talk Source: Updated QL to 200 (#32385) --- .../connectors/source-zendesk-talk/metadata.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-integrations/connectors/source-zendesk-talk/metadata.yaml b/airbyte-integrations/connectors/source-zendesk-talk/metadata.yaml index e01ac6a9dbe06..451f6e4dcbc9a 100644 --- a/airbyte-integrations/connectors/source-zendesk-talk/metadata.yaml +++ b/airbyte-integrations/connectors/source-zendesk-talk/metadata.yaml @@ -1,6 +1,6 @@ data: ab_internal: - ql: 400 + ql: 200 sl: 200 allowedHosts: hosts: From 7aae02abf975dd2b6c0e3e93a16ed9f870b35459 Mon Sep 17 00:00:00 2001 From: Patrick Nilan Date: Thu, 11 Jan 2024 09:24:20 -0800 Subject: [PATCH 062/574] Zendesk Chat: Updated QL to 200 (#32383) --- .../connectors/source-zendesk-chat/metadata.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-integrations/connectors/source-zendesk-chat/metadata.yaml b/airbyte-integrations/connectors/source-zendesk-chat/metadata.yaml index 7620e39f8aebf..5b8c27ff5ce95 100644 --- a/airbyte-integrations/connectors/source-zendesk-chat/metadata.yaml +++ b/airbyte-integrations/connectors/source-zendesk-chat/metadata.yaml @@ -1,6 +1,6 @@ data: ab_internal: - ql: 400 + ql: 200 sl: 200 allowedHosts: hosts: From 7d9f636142488ebacde0b557b0a748d77b946386 Mon Sep 17 00:00:00 2001 From: Subodh Kant Chaturvedi Date: Thu, 11 Jan 2024 23:00:17 +0530 Subject: [PATCH 063/574] snowflake-destination: upgrade cdk version to start emitting destination stats as part of state (#34083) --- .../connectors/destination-snowflake/build.gradle | 2 +- .../connectors/destination-snowflake/metadata.yaml | 2 +- docs/integrations/destinations/snowflake.md | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/airbyte-integrations/connectors/destination-snowflake/build.gradle b/airbyte-integrations/connectors/destination-snowflake/build.gradle index 94a30ef8a0928..2a6e6a00f2a06 100644 --- a/airbyte-integrations/connectors/destination-snowflake/build.gradle +++ b/airbyte-integrations/connectors/destination-snowflake/build.gradle @@ -4,7 +4,7 @@ plugins { } airbyteJavaConnector { - cdkVersionRequired = '0.11.0' + cdkVersionRequired = '0.11.2' features = ['db-destinations', 's3-destinations'] useLocalCdk = false } diff --git a/airbyte-integrations/connectors/destination-snowflake/metadata.yaml b/airbyte-integrations/connectors/destination-snowflake/metadata.yaml index db96739abf1a9..fa5975df1ccfa 100644 --- a/airbyte-integrations/connectors/destination-snowflake/metadata.yaml +++ b/airbyte-integrations/connectors/destination-snowflake/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: database connectorType: destination definitionId: 424892c4-daac-4491-b35d-c6688ba547ba - dockerImageTag: 3.4.20 + dockerImageTag: 3.4.21 dockerRepository: airbyte/destination-snowflake documentationUrl: https://docs.airbyte.com/integrations/destinations/snowflake githubIssueLabel: destination-snowflake diff --git a/docs/integrations/destinations/snowflake.md b/docs/integrations/destinations/snowflake.md index 504f6f218a300..5ccaf068410a7 100644 --- a/docs/integrations/destinations/snowflake.md +++ b/docs/integrations/destinations/snowflake.md @@ -246,6 +246,7 @@ Otherwise, make sure to grant the role the required permissions in the desired n | Version | Date | Pull Request | Subject | |:----------------|:-----------|:-----------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 3.4.21 | 2024-01-10 | [\#34083](https://github.com/airbytehq/airbte/pull/34083) | Emit destination stats as part of the state message | | 3.4.20 | 2024-01-05 | [\#33948](https://github.com/airbytehq/airbyte/pull/33948) | Skip retrieving initial table state when setup fails | | 3.4.19 | 2024-01-04 | [\#33730](https://github.com/airbytehq/airbyte/pull/33730) | Internal code structure changes | | 3.4.18 | 2024-01-02 | [\#33728](https://github.com/airbytehq/airbyte/pull/33728) | Add option to only type and dedupe at the end of the sync | From 42b54bbdf2238cbffae06087d6a5ef6b5c02a46d Mon Sep 17 00:00:00 2001 From: Dylan Seidt Date: Thu, 11 Jan 2024 11:34:39 -0600 Subject: [PATCH 064/574] =?UTF-8?q?=E2=9C=A8=20Source=20Iterable:=20add=20?= =?UTF-8?q?userId=20to=20applicable=20streams=20(#30931)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: marcosmarxm --- airbyte-integrations/connectors/source-iterable/metadata.yaml | 2 +- .../source-iterable/source_iterable/schemas/email_bounce.json | 3 +++ .../source-iterable/source_iterable/schemas/email_click.json | 3 +++ .../source_iterable/schemas/email_complaint.json | 3 +++ .../source-iterable/source_iterable/schemas/email_open.json | 3 +++ .../source-iterable/source_iterable/schemas/email_send.json | 3 +++ .../source_iterable/schemas/email_send_skip.json | 3 +++ .../source_iterable/schemas/email_subscribe.json | 3 +++ .../source_iterable/schemas/email_unsubscribe.json | 3 +++ .../source-iterable/source_iterable/schemas/events.json | 3 +++ docs/integrations/sources/iterable.md | 3 ++- 11 files changed, 30 insertions(+), 2 deletions(-) diff --git a/airbyte-integrations/connectors/source-iterable/metadata.yaml b/airbyte-integrations/connectors/source-iterable/metadata.yaml index 6362a4567f8d5..d62ca667d12f6 100644 --- a/airbyte-integrations/connectors/source-iterable/metadata.yaml +++ b/airbyte-integrations/connectors/source-iterable/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: 2e875208-0c0b-4ee4-9e92-1cb3156ea799 - dockerImageTag: 0.1.31 + dockerImageTag: 0.2.0 dockerRepository: airbyte/source-iterable documentationUrl: https://docs.airbyte.com/integrations/sources/iterable githubIssueLabel: source-iterable diff --git a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_bounce.json b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_bounce.json index fd74f6a40f9fc..14cc02a90c994 100644 --- a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_bounce.json +++ b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_bounce.json @@ -29,6 +29,9 @@ "email": { "type": ["null", "string"] }, + "userId": { + "type": ["null", "string"] + }, "recipientState": { "type": ["null", "string"] } diff --git a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_click.json b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_click.json index 5a0fecaf34478..f8439312858cb 100644 --- a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_click.json +++ b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_click.json @@ -55,6 +55,9 @@ }, "email": { "type": ["null", "string"] + }, + "userId": { + "type": ["null", "string"] } }, "type": ["null", "object"] diff --git a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_complaint.json b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_complaint.json index fd74f6a40f9fc..14cc02a90c994 100644 --- a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_complaint.json +++ b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_complaint.json @@ -29,6 +29,9 @@ "email": { "type": ["null", "string"] }, + "userId": { + "type": ["null", "string"] + }, "recipientState": { "type": ["null", "string"] } diff --git a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_open.json b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_open.json index 2e085dceeff0d..36064e7ab3c9c 100644 --- a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_open.json +++ b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_open.json @@ -46,6 +46,9 @@ }, "email": { "type": ["null", "string"] + }, + "userId": { + "type": ["null", "string"] } }, "type": ["null", "object"] diff --git a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_send.json b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_send.json index e2614d971b183..1f328b78436b5 100644 --- a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_send.json +++ b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_send.json @@ -122,6 +122,9 @@ "email": { "type": ["null", "string"] }, + "userId": { + "type": ["null", "string"] + }, "channelId": { "type": ["null", "integer"] } diff --git a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_send_skip.json b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_send_skip.json index 374a9671f9983..a96ce2d53e7a3 100644 --- a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_send_skip.json +++ b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_send_skip.json @@ -122,6 +122,9 @@ "email": { "type": ["null", "string"] }, + "userId": { + "type": ["null", "string"] + }, "channelId": { "type": ["null", "integer"] } diff --git a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_subscribe.json b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_subscribe.json index 3ac82b5cecbac..8839d6d76ea2f 100644 --- a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_subscribe.json +++ b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_subscribe.json @@ -30,6 +30,9 @@ "email": { "type": ["null", "string"] }, + "userId": { + "type": ["null", "string"] + }, "profileUpdatedAt": { "type": ["null", "string"], "format": "date-time" diff --git a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_unsubscribe.json b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_unsubscribe.json index 03b00577f7ba9..c69cfa5bcb316 100644 --- a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_unsubscribe.json +++ b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/email_unsubscribe.json @@ -46,6 +46,9 @@ "email": { "type": ["null", "string"] }, + "userId": { + "type": ["null", "string"] + }, "channelId": { "type": ["null", "integer"] } diff --git a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/events.json b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/events.json index 028d32c788544..3c88b02b1ab9c 100644 --- a/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/events.json +++ b/airbyte-integrations/connectors/source-iterable/source_iterable/schemas/events.json @@ -23,6 +23,9 @@ "email": { "type": ["null", "string"] }, + "userId": { + "type": ["null", "string"] + }, "data": { "type": ["null", "object"] } diff --git a/docs/integrations/sources/iterable.md b/docs/integrations/sources/iterable.md index e6f0a1cc1e36b..f58adccd0b172 100644 --- a/docs/integrations/sources/iterable.md +++ b/docs/integrations/sources/iterable.md @@ -80,6 +80,7 @@ The Iterable source connector supports the following [sync modes](https://docs.a | Version | Date | Pull Request | Subject | | :------ | :--------- | :------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------- | +| 0.2.0 | 2023-09-29 | [28457](https://github.com/airbytehq/airbyte/pull/30931) | Added `userId` to `email_bounce`, `email_click`, `email_complaint`, `email_open`, `email_send` `email_send_skip`, `email_subscribe`, `email_unsubscribe`, `events` streams | | 0.1.31 | 2023-12-06 | [33106](https://github.com/airbytehq/airbyte/pull/33106) | Base image migration: remove Dockerfile and use the python-connector-base image | | 0.1.30 | 2023-07-19 | [28457](https://github.com/airbytehq/airbyte/pull/28457) | Fixed TypeError for StreamSlice in debug mode | | 0.1.29 | 2023-05-24 | [26459](https://github.com/airbytehq/airbyte/pull/26459) | Added requests reading timeout 300 seconds | @@ -104,4 +105,4 @@ The Iterable source connector supports the following [sync modes](https://docs.a | 0.1.10 | 2021-11-03 | [7591](https://github.com/airbytehq/airbyte/pull/7591) | Optimize export streams memory consumption for large requests | | 0.1.9 | 2021-10-06 | [5915](https://github.com/airbytehq/airbyte/pull/5915) | Enable campaign_metrics stream | | 0.1.8 | 2021-09-20 | [5915](https://github.com/airbytehq/airbyte/pull/5915) | Add new streams: campaign_metrics, events | -| 0.1.7 | 2021-09-20 | [6242](https://github.com/airbytehq/airbyte/pull/6242) | Updated schema for: campaigns, lists, templates, metadata | \ No newline at end of file +| 0.1.7 | 2021-09-20 | [6242](https://github.com/airbytehq/airbyte/pull/6242) | Updated schema for: campaigns, lists, templates, metadata | From dd5d23bd5c1896152a0ccc349ca68f2c2f1f58aa Mon Sep 17 00:00:00 2001 From: Sitaram Shelke Date: Thu, 11 Jan 2024 23:32:18 +0530 Subject: [PATCH 065/574] docs: Mention that DBT Worker not supported on kubernetes (#34087) Co-authored-by: Marcos Marx --- docs/deploying-airbyte/on-kubernetes-via-helm.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/deploying-airbyte/on-kubernetes-via-helm.md b/docs/deploying-airbyte/on-kubernetes-via-helm.md index 818dec3f78f56..375a59b8576d8 100644 --- a/docs/deploying-airbyte/on-kubernetes-via-helm.md +++ b/docs/deploying-airbyte/on-kubernetes-via-helm.md @@ -10,6 +10,10 @@ If you don't want to configure your own Kubernetes cluster and Airbyte instance, Alternatively, you can deploy Airbyte on [Restack](https://www.restack.io) to provision your Kubernetes cluster on AWS. Follow [this guide](on-restack.md) to get started. +:::note +Airbyte running on Self-Hosted Kubernetes doesn't support DBT Transformations. Please refer to [#5901](https://github.com/airbytehq/airbyte/issues/5091) +::: + ## Getting Started ### Cluster Setup From 800236f7b75043f5135c54a04cc3422056ab08c8 Mon Sep 17 00:00:00 2001 From: Cynthia Yin Date: Thu, 11 Jan 2024 11:05:51 -0800 Subject: [PATCH 066/574] =?UTF-8?q?=F0=9F=A7=B9=20Destination=20Redshift:?= =?UTF-8?q?=20clean=20up=20DAT=20classes=20(#34134)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../destination-redshift/build.gradle | 2 +- .../redshift/RedshiftConnectionTest.java | 56 +++++++++++ ...=> RedshiftDestinationAcceptanceTest.java} | 94 +------------------ .../redshift/RedshiftFileBufferTest.java | 41 ++++++++ ...dshiftInsertDestinationAcceptanceTest.java | 2 +- ...tagingInsertDestinationAcceptanceTest.java | 6 +- 6 files changed, 107 insertions(+), 94 deletions(-) create mode 100644 airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftConnectionTest.java rename airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/{RedshiftStagingS3DestinationAcceptanceTest.java => RedshiftDestinationAcceptanceTest.java} (68%) create mode 100644 airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftFileBufferTest.java diff --git a/airbyte-integrations/connectors/destination-redshift/build.gradle b/airbyte-integrations/connectors/destination-redshift/build.gradle index aff9a6f52400f..dfe01f34dc6d6 100644 --- a/airbyte-integrations/connectors/destination-redshift/build.gradle +++ b/airbyte-integrations/connectors/destination-redshift/build.gradle @@ -6,7 +6,7 @@ plugins { airbyteJavaConnector { cdkVersionRequired = '0.11.1' features = ['db-destinations', 's3-destinations'] - useLocalCdk = false + useLocalCdk = true } //remove once upgrading the CDK version to 0.4.x or later diff --git a/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftConnectionTest.java b/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftConnectionTest.java new file mode 100644 index 0000000000000..dfefbf0c0f100 --- /dev/null +++ b/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftConnectionTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.integrations.destination.redshift; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.airbyte.commons.io.IOs; +import io.airbyte.commons.json.Jsons; +import io.airbyte.protocol.models.v0.AirbyteConnectionStatus; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; + +public class RedshiftConnectionTest { + + private final JsonNode config = Jsons.deserialize(IOs.readFile(Path.of("secrets/config.json"))); + private final RedshiftDestination destination = new RedshiftDestination(); + private AirbyteConnectionStatus status; + + @Test + void testCheckIncorrectPasswordFailure() throws Exception { + ((ObjectNode) config).put("password", "fake"); + status = destination.check(config); + assertEquals(AirbyteConnectionStatus.Status.FAILED, status.getStatus()); + assertTrue(status.getMessage().contains("State code: 28000;")); + } + + @Test + public void testCheckIncorrectUsernameFailure() throws Exception { + ((ObjectNode) config).put("username", ""); + status = destination.check(config); + assertEquals(AirbyteConnectionStatus.Status.FAILED, status.getStatus()); + assertTrue(status.getMessage().contains("State code: 28000;")); + } + + @Test + public void testCheckIncorrectHostFailure() throws Exception { + ((ObjectNode) config).put("host", "localhost2"); + status = destination.check(config); + assertEquals(AirbyteConnectionStatus.Status.FAILED, status.getStatus()); + assertTrue(status.getMessage().contains("State code: 08001;")); + } + + @Test + public void testCheckIncorrectDataBaseFailure() throws Exception { + ((ObjectNode) config).put("database", "wrongdatabase"); + status = destination.check(config); + assertEquals(AirbyteConnectionStatus.Status.FAILED, status.getStatus()); + assertTrue(status.getMessage().contains("State code: 3D000;")); + } + +} diff --git a/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftStagingS3DestinationAcceptanceTest.java b/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftDestinationAcceptanceTest.java similarity index 68% rename from airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftStagingS3DestinationAcceptanceTest.java rename to airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftDestinationAcceptanceTest.java index 47b6926a05170..b398c0b9e5973 100644 --- a/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftStagingS3DestinationAcceptanceTest.java +++ b/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftDestinationAcceptanceTest.java @@ -4,29 +4,21 @@ package io.airbyte.integrations.destination.redshift; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - import com.amazon.redshift.util.RedshiftTimestamp; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import io.airbyte.cdk.db.Database; import io.airbyte.cdk.db.factory.ConnectionFactory; import io.airbyte.cdk.db.factory.DatabaseDriver; import io.airbyte.cdk.db.jdbc.JdbcUtils; import io.airbyte.cdk.integrations.base.JavaBaseConstants; -import io.airbyte.cdk.integrations.destination.record_buffer.FileBuffer; import io.airbyte.cdk.integrations.standardtest.destination.JdbcDestinationAcceptanceTest; import io.airbyte.cdk.integrations.standardtest.destination.TestingNamespaces; import io.airbyte.cdk.integrations.standardtest.destination.comparator.TestDataComparator; -import io.airbyte.commons.io.IOs; import io.airbyte.commons.json.Jsons; import io.airbyte.commons.string.Strings; import io.airbyte.integrations.destination.redshift.operations.RedshiftSqlOperations; -import io.airbyte.protocol.models.v0.AirbyteConnectionStatus; import java.io.IOException; -import java.nio.file.Path; import java.sql.Connection; import java.sql.SQLException; import java.time.ZoneOffset; @@ -38,21 +30,16 @@ import java.util.Optional; import java.util.stream.Collectors; import org.jooq.impl.DSL; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Integration test testing {@link RedshiftStagingS3Destination}. The default Redshift integration - * test credentials contain S3 credentials - this automatically causes COPY to be selected. - */ // these tests are not yet thread-safe, unlike the DV2 tests. @Execution(ExecutionMode.SAME_THREAD) -public abstract class RedshiftStagingS3DestinationAcceptanceTest extends JdbcDestinationAcceptanceTest { +public abstract class RedshiftDestinationAcceptanceTest extends JdbcDestinationAcceptanceTest { - private static final Logger LOGGER = LoggerFactory.getLogger(RedshiftStagingS3DestinationAcceptanceTest.class); + private static final Logger LOGGER = LoggerFactory.getLogger(RedshiftDestinationAcceptanceTest.class); // config from which to create / delete schemas. private JsonNode baseConfig; @@ -65,8 +52,6 @@ public abstract class RedshiftStagingS3DestinationAcceptanceTest extends JdbcDes private Connection connection; protected TestDestinationEnv testDestinationEnv; - private final ObjectMapper mapper = new ObjectMapper(); - @Override protected String getImageName() { return "airbyte/destination-redshift:dev"; @@ -77,9 +62,7 @@ protected JsonNode getConfig() { return config; } - public JsonNode getStaticConfig() throws IOException { - return Jsons.deserialize(IOs.readFile(Path.of("secrets/config_staging.json"))); - } + public abstract JsonNode getStaticConfig() throws IOException; @Override protected JsonNode getFailCheckConfig() { @@ -88,73 +71,6 @@ protected JsonNode getFailCheckConfig() { return invalidConfig; } - @Test - void testCheckIncorrectPasswordFailure() throws Exception { - final JsonNode invalidConfig = Jsons.clone(config); - ((ObjectNode) invalidConfig).put("password", "fake"); - final RedshiftDestination destination = new RedshiftDestination(); - final AirbyteConnectionStatus status = destination.check(invalidConfig); - assertEquals(AirbyteConnectionStatus.Status.FAILED, status.getStatus()); - assertTrue(status.getMessage().contains("State code: 28000;")); - } - - @Test - public void testCheckIncorrectUsernameFailure() throws Exception { - final JsonNode invalidConfig = Jsons.clone(config); - ((ObjectNode) invalidConfig).put("username", ""); - final RedshiftDestination destination = new RedshiftDestination(); - final AirbyteConnectionStatus status = destination.check(invalidConfig); - assertEquals(AirbyteConnectionStatus.Status.FAILED, status.getStatus()); - assertTrue(status.getMessage().contains("State code: 28000;")); - } - - @Test - public void testCheckIncorrectHostFailure() throws Exception { - final JsonNode invalidConfig = Jsons.clone(config); - ((ObjectNode) invalidConfig).put("host", "localhost2"); - final RedshiftDestination destination = new RedshiftDestination(); - final AirbyteConnectionStatus status = destination.check(invalidConfig); - assertEquals(AirbyteConnectionStatus.Status.FAILED, status.getStatus()); - assertTrue(status.getMessage().contains("State code: 08001;")); - } - - @Test - public void testCheckIncorrectDataBaseFailure() throws Exception { - final JsonNode invalidConfig = Jsons.clone(config); - ((ObjectNode) invalidConfig).put("database", "wrongdatabase"); - final RedshiftDestination destination = new RedshiftDestination(); - final AirbyteConnectionStatus status = destination.check(invalidConfig); - assertEquals(AirbyteConnectionStatus.Status.FAILED, status.getStatus()); - assertTrue(status.getMessage().contains("State code: 3D000;")); - } - - /* - * FileBuffer Default Tests - */ - @Test - public void testGetFileBufferDefault() { - final RedshiftStagingS3Destination destination = new RedshiftStagingS3Destination(); - assertEquals(destination.getNumberOfFileBuffers(config), FileBuffer.DEFAULT_MAX_CONCURRENT_STREAM_IN_BUFFER); - } - - @Test - public void testGetFileBufferMaxLimited() { - final JsonNode defaultConfig = Jsons.clone(config); - ((ObjectNode) defaultConfig).put(FileBuffer.FILE_BUFFER_COUNT_KEY, 100); - final RedshiftStagingS3Destination destination = new RedshiftStagingS3Destination(); - assertEquals(destination.getNumberOfFileBuffers(defaultConfig), FileBuffer.MAX_CONCURRENT_STREAM_IN_BUFFER); - } - - @Test - public void testGetMinimumFileBufferCount() { - final JsonNode defaultConfig = Jsons.clone(config); - ((ObjectNode) defaultConfig).put(FileBuffer.FILE_BUFFER_COUNT_KEY, 1); - final RedshiftStagingS3Destination destination = new RedshiftStagingS3Destination(); - // User cannot set number of file counts below the default file buffer count, which is existing - // behavior - assertEquals(destination.getNumberOfFileBuffers(defaultConfig), FileBuffer.DEFAULT_MAX_CONCURRENT_STREAM_IN_BUFFER); - } - @Override protected TestDataComparator getTestDataComparator() { return new RedshiftTestDataComparator(); @@ -320,10 +236,6 @@ protected Database getDatabase() { return database; } - public RedshiftSQLNameTransformer getNamingResolver() { - return namingResolver; - } - @Override protected int getMaxRecordValueLimit() { return RedshiftSqlOperations.REDSHIFT_VARCHAR_MAX_BYTE_SIZE; diff --git a/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftFileBufferTest.java b/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftFileBufferTest.java new file mode 100644 index 0000000000000..bbeab71e6be0c --- /dev/null +++ b/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftFileBufferTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.integrations.destination.redshift; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.airbyte.cdk.integrations.destination.record_buffer.FileBuffer; +import io.airbyte.commons.io.IOs; +import io.airbyte.commons.json.Jsons; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; + +public class RedshiftFileBufferTest { + + private final JsonNode config = Jsons.deserialize(IOs.readFile(Path.of("secrets/config_staging.json"))); + private final RedshiftStagingS3Destination destination = new RedshiftStagingS3Destination(); + + @Test + public void testGetFileBufferDefault() { + assertEquals(destination.getNumberOfFileBuffers(config), FileBuffer.DEFAULT_MAX_CONCURRENT_STREAM_IN_BUFFER); + } + + @Test + public void testGetFileBufferMaxLimited() { + ((ObjectNode) config).put(FileBuffer.FILE_BUFFER_COUNT_KEY, 100); + assertEquals(destination.getNumberOfFileBuffers(config), FileBuffer.MAX_CONCURRENT_STREAM_IN_BUFFER); + } + + @Test + public void testGetMinimumFileBufferCount() { + ((ObjectNode) config).put(FileBuffer.FILE_BUFFER_COUNT_KEY, 1); + // User cannot set number of file counts below the default file buffer count, which is existing + // behavior + assertEquals(destination.getNumberOfFileBuffers(config), FileBuffer.DEFAULT_MAX_CONCURRENT_STREAM_IN_BUFFER); + } + +} diff --git a/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftInsertDestinationAcceptanceTest.java b/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftInsertDestinationAcceptanceTest.java index 57f61b4f39f2c..6ca0a17fce3ce 100644 --- a/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftInsertDestinationAcceptanceTest.java +++ b/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftInsertDestinationAcceptanceTest.java @@ -13,7 +13,7 @@ /** * Integration test testing the {@link RedshiftInsertDestination}. */ -public class RedshiftInsertDestinationAcceptanceTest extends RedshiftStagingS3DestinationAcceptanceTest { +public class RedshiftInsertDestinationAcceptanceTest extends RedshiftDestinationAcceptanceTest { public JsonNode getStaticConfig() throws IOException { return Jsons.deserialize(Files.readString(Path.of("secrets/config.json"))); diff --git a/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftS3StagingInsertDestinationAcceptanceTest.java b/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftS3StagingInsertDestinationAcceptanceTest.java index 52cd07ce17484..fc054be512326 100644 --- a/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftS3StagingInsertDestinationAcceptanceTest.java +++ b/airbyte-integrations/connectors/destination-redshift/src/test-integration/java/io/airbyte/integrations/destination/redshift/RedshiftS3StagingInsertDestinationAcceptanceTest.java @@ -9,7 +9,11 @@ import io.airbyte.commons.json.Jsons; import java.nio.file.Path; -public class RedshiftS3StagingInsertDestinationAcceptanceTest extends RedshiftStagingS3DestinationAcceptanceTest { +/** + * Integration test testing {@link RedshiftStagingS3Destination}. The default Redshift integration + * test credentials contain S3 credentials - this automatically causes COPY to be selected. + */ +public class RedshiftS3StagingInsertDestinationAcceptanceTest extends RedshiftDestinationAcceptanceTest { public JsonNode getStaticConfig() { return Jsons.deserialize(IOs.readFile(Path.of("secrets/config_staging.json"))); From cf7f700bbb201fdf253225842e4532a8b26d8147 Mon Sep 17 00:00:00 2001 From: Baz Date: Thu, 11 Jan 2024 21:26:23 +0200 Subject: [PATCH 067/574] =?UTF-8?q?=F0=9F=8E=89=20Airbyte=20CDK=20(File-ba?= =?UTF-8?q?sed=20CDK):=20Stop=20the=20sync=20if=20the=20record=20could=20n?= =?UTF-8?q?ot=20be=20parsed=20=20(#32589)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airbyte-cdk/python/.bumpversion.cfg | 2 +- airbyte-cdk/python/CHANGELOG.md | 3 + airbyte-cdk/python/Dockerfile | 2 +- .../sources/file_based/config/avro_format.py | 1 + .../file_based/config/parquet_format.py | 1 + .../sources/file_based/exceptions.py | 27 +++- .../sources/file_based/file_based_source.py | 6 +- .../file_based/file_types/avro_parser.py | 24 ++-- .../file_based/file_types/csv_parser.py | 30 +++-- .../file_based/file_types/jsonl_parser.py | 2 +- .../file_based/file_types/parquet_parser.py | 36 +++--- .../stream/abstract_file_based_stream.py | 4 +- .../stream/default_file_based_stream.py | 14 ++- airbyte-cdk/python/setup.py | 2 +- .../file_based/scenarios/csv_scenarios.py | 119 ++++++++++++++++-- .../scenarios/unstructured_scenarios.py | 4 + .../scenarios/validation_policy_scenarios.py | 47 +++---- .../stream/test_default_file_based_stream.py | 97 ++++++++++++-- .../file_based/test_file_based_scenarios.py | 2 + 19 files changed, 326 insertions(+), 97 deletions(-) diff --git a/airbyte-cdk/python/.bumpversion.cfg b/airbyte-cdk/python/.bumpversion.cfg index daaecf3ee2f13..8c0dfbd0ff1a2 100644 --- a/airbyte-cdk/python/.bumpversion.cfg +++ b/airbyte-cdk/python/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.58.5 +current_version = 0.58.6 commit = False [bumpversion:file:setup.py] diff --git a/airbyte-cdk/python/CHANGELOG.md b/airbyte-cdk/python/CHANGELOG.md index e23dd1840b8c1..58937e371e008 100644 --- a/airbyte-cdk/python/CHANGELOG.md +++ b/airbyte-cdk/python/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 0.58.6 +File CDK: Added logic to emit logged `RecordParseError` errors and raise the single `AirbyteTracebackException` in the end of the sync, instead of silent skipping the parsing errors. PR: https://github.com/airbytehq/airbyte/pull/32589 + ## 0.58.5 Handle private network exception as config error diff --git a/airbyte-cdk/python/Dockerfile b/airbyte-cdk/python/Dockerfile index 96c5069998e35..cb35d42e9f458 100644 --- a/airbyte-cdk/python/Dockerfile +++ b/airbyte-cdk/python/Dockerfile @@ -32,5 +32,5 @@ ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] # needs to be the same as CDK -LABEL io.airbyte.version=0.58.5 +LABEL io.airbyte.version=0.58.6 LABEL io.airbyte.name=airbyte/source-declarative-manifest diff --git a/airbyte-cdk/python/airbyte_cdk/sources/file_based/config/avro_format.py b/airbyte-cdk/python/airbyte_cdk/sources/file_based/config/avro_format.py index 9a8d05c5255b6..a5bef76f61764 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/file_based/config/avro_format.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/file_based/config/avro_format.py @@ -2,6 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # + from airbyte_cdk.utils.oneof_option_config import OneOfOptionConfig from pydantic import BaseModel, Field diff --git a/airbyte-cdk/python/airbyte_cdk/sources/file_based/config/parquet_format.py b/airbyte-cdk/python/airbyte_cdk/sources/file_based/config/parquet_format.py index 2462df3d14cb8..b462e78bba030 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/file_based/config/parquet_format.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/file_based/config/parquet_format.py @@ -2,6 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # + from airbyte_cdk.utils.oneof_option_config import OneOfOptionConfig from pydantic import BaseModel, Field diff --git a/airbyte-cdk/python/airbyte_cdk/sources/file_based/exceptions.py b/airbyte-cdk/python/airbyte_cdk/sources/file_based/exceptions.py index 61951fc214722..18073be07b266 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/file_based/exceptions.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/file_based/exceptions.py @@ -3,8 +3,9 @@ # from enum import Enum -from typing import Union +from typing import Any, List, Union +from airbyte_cdk.models import AirbyteMessage, FailureType from airbyte_cdk.utils import AirbyteTracedException @@ -40,6 +41,30 @@ class FileBasedSourceError(Enum): UNDEFINED_VALIDATION_POLICY = "The validation policy defined in the config does not exist for the source." +class FileBasedErrorsCollector: + """ + The placeholder for all errors collected. + """ + + errors: List[AirbyteMessage] = [] + + def yield_and_raise_collected(self) -> Any: + if self.errors: + # emit collected logged messages + yield from self.errors + # clean the collector + self.errors.clear() + # raising the single exception + raise AirbyteTracedException( + internal_message="Please check the logged errors for more information.", + message="Some errors occured while reading from the source.", + failure_type=FailureType.config_error, + ) + + def collect(self, logged_error: AirbyteMessage) -> None: + self.errors.append(logged_error) + + class BaseFileBasedSourceError(Exception): def __init__(self, error: Union[FileBasedSourceError, str], **kwargs): # type: ignore # noqa if isinstance(error, FileBasedSourceError): diff --git a/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_based_source.py b/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_based_source.py index 01a8e7d0bbce1..9904e4a8be97b 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_based_source.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_based_source.py @@ -14,7 +14,7 @@ from airbyte_cdk.sources.file_based.config.abstract_file_based_spec import AbstractFileBasedSpec from airbyte_cdk.sources.file_based.config.file_based_stream_config import FileBasedStreamConfig, ValidationPolicy from airbyte_cdk.sources.file_based.discovery_policy import AbstractDiscoveryPolicy, DefaultDiscoveryPolicy -from airbyte_cdk.sources.file_based.exceptions import ConfigValidationError, FileBasedSourceError +from airbyte_cdk.sources.file_based.exceptions import ConfigValidationError, FileBasedErrorsCollector, FileBasedSourceError from airbyte_cdk.sources.file_based.file_based_stream_reader import AbstractFileBasedStreamReader from airbyte_cdk.sources.file_based.file_types import default_parsers from airbyte_cdk.sources.file_based.file_types.file_type_parser import FileTypeParser @@ -49,6 +49,7 @@ def __init__( self.stream_schemas = {s.stream.name: s.stream.json_schema for s in catalog.streams} if catalog else {} self.cursor_cls = cursor_cls self.logger = logging.getLogger(f"airbyte.{self.name}") + self.errors_collector: FileBasedErrorsCollector = FileBasedErrorsCollector() def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> Tuple[bool, Optional[Any]]: """ @@ -106,6 +107,7 @@ def streams(self, config: Mapping[str, Any]) -> List[Stream]: parsers=self.parsers, validation_policy=self._validate_and_get_validation_policy(stream_config), cursor=self.cursor_cls(stream_config), + errors_collector=self.errors_collector, ) ) return streams @@ -121,6 +123,8 @@ def read( state: Optional[Union[List[AirbyteStateMessage], MutableMapping[str, Any]]] = None, ) -> Iterator[AirbyteMessage]: yield from super().read(logger, config, catalog, state) + # emit all the errors collected + yield from self.errors_collector.yield_and_raise_collected() # count streams using a certain parser parsed_config = self._get_parsed_config(config) for parser, count in Counter(stream.format.filetype for stream in parsed_config.streams).items(): diff --git a/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/avro_parser.py b/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/avro_parser.py index 366e7429ba1db..25267b9a5c23e 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/avro_parser.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/avro_parser.py @@ -8,6 +8,7 @@ import fastavro from airbyte_cdk.sources.file_based.config.avro_format import AvroFormat from airbyte_cdk.sources.file_based.config.file_based_stream_config import FileBasedStreamConfig +from airbyte_cdk.sources.file_based.exceptions import FileBasedSourceError, RecordParseError from airbyte_cdk.sources.file_based.file_based_stream_reader import AbstractFileBasedStreamReader, FileReadMode from airbyte_cdk.sources.file_based.file_types.file_type_parser import FileTypeParser from airbyte_cdk.sources.file_based.remote_file import RemoteFile @@ -144,15 +145,20 @@ def parse_records( if not isinstance(avro_format, AvroFormat): raise ValueError(f"Expected ParquetFormat, got {avro_format}") - with stream_reader.open_file(file, self.file_read_mode, self.ENCODING, logger) as fp: - avro_reader = fastavro.reader(fp) - schema = avro_reader.writer_schema - schema_field_name_to_type = {field["name"]: field["type"] for field in schema["fields"]} - for record in avro_reader: - yield { - record_field: self._to_output_value(avro_format, schema_field_name_to_type[record_field], record[record_field]) - for record_field, record_value in schema_field_name_to_type.items() - } + line_no = 0 + try: + with stream_reader.open_file(file, self.file_read_mode, self.ENCODING, logger) as fp: + avro_reader = fastavro.reader(fp) + schema = avro_reader.writer_schema + schema_field_name_to_type = {field["name"]: field["type"] for field in schema["fields"]} + for record in avro_reader: + line_no += 1 + yield { + record_field: self._to_output_value(avro_format, schema_field_name_to_type[record_field], record[record_field]) + for record_field, record_value in schema_field_name_to_type.items() + } + except Exception as exc: + raise RecordParseError(FileBasedSourceError.ERROR_PARSING_RECORD, filename=file.uri, lineno=line_no) from exc @property def file_read_mode(self) -> FileReadMode: diff --git a/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/csv_parser.py b/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/csv_parser.py index e687be07f0929..b67aebcd723e7 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/csv_parser.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/csv_parser.py @@ -178,17 +178,25 @@ def parse_records( logger: logging.Logger, discovered_schema: Optional[Mapping[str, SchemaType]], ) -> Iterable[Dict[str, Any]]: - config_format = _extract_format(config) - if discovered_schema: - property_types = {col: prop["type"] for col, prop in discovered_schema["properties"].items()} # type: ignore # discovered_schema["properties"] is known to be a mapping - deduped_property_types = CsvParser._pre_propcess_property_types(property_types) - else: - deduped_property_types = {} - cast_fn = CsvParser._get_cast_function(deduped_property_types, config_format, logger, config.schemaless) - data_generator = self._csv_reader.read_data(config, file, stream_reader, logger, self.file_read_mode) - for row in data_generator: - yield CsvParser._to_nullable(cast_fn(row), deduped_property_types, config_format.null_values, config_format.strings_can_be_null) - data_generator.close() + line_no = 0 + try: + config_format = _extract_format(config) + if discovered_schema: + property_types = {col: prop["type"] for col, prop in discovered_schema["properties"].items()} # type: ignore # discovered_schema["properties"] is known to be a mapping + deduped_property_types = CsvParser._pre_propcess_property_types(property_types) + else: + deduped_property_types = {} + cast_fn = CsvParser._get_cast_function(deduped_property_types, config_format, logger, config.schemaless) + data_generator = self._csv_reader.read_data(config, file, stream_reader, logger, self.file_read_mode) + for row in data_generator: + line_no += 1 + yield CsvParser._to_nullable( + cast_fn(row), deduped_property_types, config_format.null_values, config_format.strings_can_be_null + ) + except RecordParseError as parse_err: + raise RecordParseError(FileBasedSourceError.ERROR_PARSING_RECORD, filename=file.uri, lineno=line_no) from parse_err + finally: + data_generator.close() @property def file_read_mode(self) -> FileReadMode: diff --git a/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/jsonl_parser.py b/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/jsonl_parser.py index e543e1a4f257f..122103c5739de 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/jsonl_parser.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/jsonl_parser.py @@ -119,7 +119,7 @@ def _parse_jsonl_entries( break if had_json_parsing_error and not yielded_at_least_once: - raise RecordParseError(FileBasedSourceError.ERROR_PARSING_RECORD) + raise RecordParseError(FileBasedSourceError.ERROR_PARSING_RECORD, filename=file.uri, lineno=line) @staticmethod def _instantiate_accumulator(line: Union[bytes, str]) -> Union[bytes, str]: diff --git a/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/parquet_parser.py b/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/parquet_parser.py index 06072a40cf10e..00b78c489801b 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/parquet_parser.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/file_based/file_types/parquet_parser.py @@ -11,7 +11,7 @@ import pyarrow as pa import pyarrow.parquet as pq from airbyte_cdk.sources.file_based.config.file_based_stream_config import FileBasedStreamConfig, ParquetFormat -from airbyte_cdk.sources.file_based.exceptions import ConfigValidationError, FileBasedSourceError +from airbyte_cdk.sources.file_based.exceptions import ConfigValidationError, FileBasedSourceError, RecordParseError from airbyte_cdk.sources.file_based.file_based_stream_reader import AbstractFileBasedStreamReader, FileReadMode from airbyte_cdk.sources.file_based.file_types.file_type_parser import FileTypeParser from airbyte_cdk.sources.file_based.remote_file import RemoteFile @@ -64,19 +64,27 @@ def parse_records( if not isinstance(parquet_format, ParquetFormat): logger.info(f"Expected ParquetFormat, got {parquet_format}") raise ConfigValidationError(FileBasedSourceError.CONFIG_VALIDATION_ERROR) - with stream_reader.open_file(file, self.file_read_mode, self.ENCODING, logger) as fp: - reader = pq.ParquetFile(fp) - partition_columns = {x.split("=")[0]: x.split("=")[1] for x in self._extract_partitions(file.uri)} - for row_group in range(reader.num_row_groups): - batch = reader.read_row_group(row_group) - for row in range(batch.num_rows): - yield { - **{ - column: ParquetParser._to_output_value(batch.column(column)[row], parquet_format) - for column in batch.column_names - }, - **partition_columns, - } + + line_no = 0 + try: + with stream_reader.open_file(file, self.file_read_mode, self.ENCODING, logger) as fp: + reader = pq.ParquetFile(fp) + partition_columns = {x.split("=")[0]: x.split("=")[1] for x in self._extract_partitions(file.uri)} + for row_group in range(reader.num_row_groups): + batch = reader.read_row_group(row_group) + for row in range(batch.num_rows): + line_no += 1 + yield { + **{ + column: ParquetParser._to_output_value(batch.column(column)[row], parquet_format) + for column in batch.column_names + }, + **partition_columns, + } + except Exception as exc: + raise RecordParseError( + FileBasedSourceError.ERROR_PARSING_RECORD, filename=file.uri, lineno=f"{row_group=}, {line_no=}" + ) from exc @staticmethod def _extract_partitions(filepath: str) -> List[str]: diff --git a/airbyte-cdk/python/airbyte_cdk/sources/file_based/stream/abstract_file_based_stream.py b/airbyte-cdk/python/airbyte_cdk/sources/file_based/stream/abstract_file_based_stream.py index 474a271a48d7b..420cf7ef69882 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/file_based/stream/abstract_file_based_stream.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/file_based/stream/abstract_file_based_stream.py @@ -10,7 +10,7 @@ from airbyte_cdk.sources.file_based.availability_strategy import AbstractFileBasedAvailabilityStrategy from airbyte_cdk.sources.file_based.config.file_based_stream_config import FileBasedStreamConfig, PrimaryKeyType from airbyte_cdk.sources.file_based.discovery_policy import AbstractDiscoveryPolicy -from airbyte_cdk.sources.file_based.exceptions import FileBasedSourceError, RecordParseError, UndefinedParserError +from airbyte_cdk.sources.file_based.exceptions import FileBasedErrorsCollector, FileBasedSourceError, RecordParseError, UndefinedParserError from airbyte_cdk.sources.file_based.file_based_stream_reader import AbstractFileBasedStreamReader from airbyte_cdk.sources.file_based.file_types.file_type_parser import FileTypeParser from airbyte_cdk.sources.file_based.remote_file import RemoteFile @@ -44,6 +44,7 @@ def __init__( discovery_policy: AbstractDiscoveryPolicy, parsers: Dict[Type[Any], FileTypeParser], validation_policy: AbstractSchemaValidationPolicy, + errors_collector: FileBasedErrorsCollector, ): super().__init__() self.config = config @@ -53,6 +54,7 @@ def __init__( self._discovery_policy = discovery_policy self._availability_strategy = availability_strategy self._parsers = parsers + self.errors_collector = errors_collector @property @abstractmethod diff --git a/airbyte-cdk/python/airbyte_cdk/sources/file_based/stream/default_file_based_stream.py b/airbyte-cdk/python/airbyte_cdk/sources/file_based/stream/default_file_based_stream.py index 86888236b466c..f6e0ac8e0fe72 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/file_based/stream/default_file_based_stream.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/file_based/stream/default_file_based_stream.py @@ -112,12 +112,14 @@ def read_records_from_slice(self, stream_slice: StreamSlice) -> Iterable[Airbyte except RecordParseError: # Increment line_no because the exception was raised before we could increment it line_no += 1 - yield AirbyteMessage( - type=MessageType.LOG, - log=AirbyteLogMessage( - level=Level.ERROR, - message=f"{FileBasedSourceError.ERROR_PARSING_RECORD.value} stream={self.name} file={file.uri} line_no={line_no} n_skipped={n_skipped}", - stack_trace=traceback.format_exc(), + self.errors_collector.collect( + AirbyteMessage( + type=MessageType.LOG, + log=AirbyteLogMessage( + level=Level.ERROR, + message=f"{FileBasedSourceError.ERROR_PARSING_RECORD.value} stream={self.name} file={file.uri} line_no={line_no} n_skipped={n_skipped}", + stack_trace=traceback.format_exc(), + ), ), ) diff --git a/airbyte-cdk/python/setup.py b/airbyte-cdk/python/setup.py index 553069c338407..0cee360df7134 100644 --- a/airbyte-cdk/python/setup.py +++ b/airbyte-cdk/python/setup.py @@ -36,7 +36,7 @@ name="airbyte-cdk", # The version of the airbyte-cdk package is used at runtime to validate manifests. That validation must be # updated if our semver format changes such as using release candidate versions. - version="0.58.5", + version="0.58.6", description="A framework for writing Airbyte Connectors.", long_description=README, long_description_content_type="text/markdown", diff --git a/airbyte-cdk/python/unit_tests/sources/file_based/scenarios/csv_scenarios.py b/airbyte-cdk/python/unit_tests/sources/file_based/scenarios/csv_scenarios.py index e6c5824b4e195..77164c83d8d87 100644 --- a/airbyte-cdk/python/unit_tests/sources/file_based/scenarios/csv_scenarios.py +++ b/airbyte-cdk/python/unit_tests/sources/file_based/scenarios/csv_scenarios.py @@ -852,6 +852,109 @@ ] } ) + .set_expected_read_error( + AirbyteTracedException, + "Please check the logged errors for more information.", + ) +).build() + +invalid_csv_multi_scenario: TestScenario[InMemoryFilesSource] = ( + TestScenarioBuilder[InMemoryFilesSource]() + .set_name("invalid_csv_multi_scenario") # too many values for the number of headers + .set_config( + { + "streams": [ + { + "name": "stream1", + "format": {"filetype": "csv"}, + "globs": ["*"], + "validation_policy": "Emit Record", + }, + { + "name": "stream2", + "format": {"filetype": "csv"}, + "globs": ["b.csv"], + "validation_policy": "Emit Record", + }, + ] + } + ) + .set_source_builder( + FileBasedSourceBuilder() + .set_files( + { + "a.csv": { + "contents": [ + ("col1",), + ("val11", "val12"), + ("val21", "val22"), + ], + "last_modified": "2023-06-05T03:54:07.000Z", + }, + "b.csv": { + "contents": [ + ("col3",), + ("val13b", "val14b"), + ("val23b", "val24b"), + ], + "last_modified": "2023-06-05T03:54:07.000Z", + }, + } + ) + .set_file_type("csv") + ) + .set_expected_catalog( + { + "streams": [ + { + "default_cursor_field": ["_ab_source_file_last_modified"], + "json_schema": { + "type": "object", + "properties": { + "col1": {"type": ["null", "string"]}, + "col2": {"type": ["null", "string"]}, + "_ab_source_file_last_modified": {"type": "string"}, + "_ab_source_file_url": {"type": "string"}, + }, + }, + "name": "stream1", + "source_defined_cursor": True, + "supported_sync_modes": ["full_refresh", "incremental"], + }, + { + "json_schema": { + "type": "object", + "properties": { + "col3": {"type": ["null", "string"]}, + "_ab_source_file_last_modified": {"type": "string"}, + "_ab_source_file_url": {"type": "string"}, + }, + }, + "name": "stream2", + "source_defined_cursor": True, + "default_cursor_field": ["_ab_source_file_last_modified"], + "supported_sync_modes": ["full_refresh", "incremental"], + }, + ] + } + ) + .set_expected_records([]) + .set_expected_discover_error(AirbyteTracedException, FileBasedSourceError.SCHEMA_INFERENCE_ERROR.value) + .set_expected_logs( + { + "read": [ + { + "level": "ERROR", + "message": f"{FileBasedSourceError.ERROR_PARSING_RECORD.value} stream=stream1 file=a.csv line_no=1 n_skipped=0", + }, + { + "level": "ERROR", + "message": f"{FileBasedSourceError.ERROR_PARSING_RECORD.value} stream=stream2 file=b.csv line_no=1 n_skipped=0", + }, + ] + } + ) + .set_expected_read_error(AirbyteTracedException, "Please check the logged errors for more information.") ).build() csv_single_stream_scenario: TestScenario[InMemoryFilesSource] = ( @@ -2172,17 +2275,15 @@ }, ] ) - .set_expected_logs( - { - "read": [ - { - "level": "ERROR", - "message": "Error parsing record. This could be due to a mismatch between the config's file type and the actual file type, or because the file or record is not parseable. stream=stream1 file=a.csv line_no=2 n_skipped=0", - } - ] - } + .set_expected_read_error( + AirbyteTracedException, + f"{FileBasedSourceError.ERROR_PARSING_RECORD.value} stream=stream1 file=a.csv line_no=2 n_skipped=0", ) .set_expected_discover_error(AirbyteTracedException, FileBasedSourceError.SCHEMA_INFERENCE_ERROR.value) + .set_expected_read_error( + AirbyteTracedException, + "Please check the logged errors for more information.", + ) ).build() csv_escape_char_is_set_scenario: TestScenario[InMemoryFilesSource] = ( diff --git a/airbyte-cdk/python/unit_tests/sources/file_based/scenarios/unstructured_scenarios.py b/airbyte-cdk/python/unit_tests/sources/file_based/scenarios/unstructured_scenarios.py index f052c4530e4ac..dc0824512a437 100644 --- a/airbyte-cdk/python/unit_tests/sources/file_based/scenarios/unstructured_scenarios.py +++ b/airbyte-cdk/python/unit_tests/sources/file_based/scenarios/unstructured_scenarios.py @@ -231,6 +231,10 @@ ) .set_expected_records([]) .set_expected_discover_error(AirbyteTracedException, "Error inferring schema from files") + .set_expected_read_error( + AirbyteTracedException, + "Please check the logged errors for more information.", + ) ).build() # If skip unprocessable file types is set to true, then discover will succeed even if there are non-matching file types diff --git a/airbyte-cdk/python/unit_tests/sources/file_based/scenarios/validation_policy_scenarios.py b/airbyte-cdk/python/unit_tests/sources/file_based/scenarios/validation_policy_scenarios.py index af1318dba6476..9ac880b11fe55 100644 --- a/airbyte-cdk/python/unit_tests/sources/file_based/scenarios/validation_policy_scenarios.py +++ b/airbyte-cdk/python/unit_tests/sources/file_based/scenarios/validation_policy_scenarios.py @@ -2,7 +2,8 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # -from airbyte_cdk.sources.file_based.exceptions import FileBasedSourceError + +from airbyte_cdk.utils.traced_exception import AirbyteTracedException from unit_tests.sources.file_based.scenarios.file_based_source_builder import FileBasedSourceBuilder from unit_tests.sources.file_based.scenarios.scenario_builder import TestScenarioBuilder @@ -272,6 +273,10 @@ ] } ) + .set_expected_read_error( + AirbyteTracedException, + "Please check the logged errors for more information.", + ) ).build() @@ -416,6 +421,10 @@ ] } ) + .set_expected_read_error( + AirbyteTracedException, + "Please check the logged errors for more information.", + ) ).build() @@ -492,19 +501,9 @@ }, ] ) - .set_expected_logs( - { - "read": [ - { - "level": "ERROR", - "message": f"{FileBasedSourceError.ERROR_PARSING_RECORD.value} stream=stream1 file=c.csv line_no=2 n_skipped=0", - }, - { - "level": "WARN", - "message": "Could not cast the value to the expected type.: col2: value=this is text that will trigger validation policy,expected_type=integer", - }, - ] - } + .set_expected_read_error( + AirbyteTracedException, + "Please check the logged errors for more information.", ) ).build() @@ -640,23 +639,9 @@ }, ] ) - .set_expected_logs( - { - "read": [ - { - "level": "ERROR", - "message": f"{FileBasedSourceError.ERROR_PARSING_RECORD.value} stream=stream1 file=a/a3.csv line_no=2 n_skipped=0", - }, - { - "level": "WARN", - "message": "Could not cast the value to the expected type.: col2: value=this is text that will trigger validation policy,expected_type=integer", - }, - { - "level": "WARN", - "message": "Could not cast the value to the expected type.: col2: value=this is text that will trigger validation policy,expected_type=integer", - }, - ] - } + .set_expected_read_error( + AirbyteTracedException, + "Please check the logged errors for more information.", ) ).build() diff --git a/airbyte-cdk/python/unit_tests/sources/file_based/stream/test_default_file_based_stream.py b/airbyte-cdk/python/unit_tests/sources/file_based/stream/test_default_file_based_stream.py index e0c5f59623f53..be36413f271bc 100644 --- a/airbyte-cdk/python/unit_tests/sources/file_based/stream/test_default_file_based_stream.py +++ b/airbyte-cdk/python/unit_tests/sources/file_based/stream/test_default_file_based_stream.py @@ -2,15 +2,18 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # +import traceback import unittest from datetime import datetime, timezone from typing import Any, Iterable, Iterator, Mapping from unittest.mock import Mock import pytest -from airbyte_cdk.models import Level +from airbyte_cdk.models import AirbyteLogMessage, AirbyteMessage, Level +from airbyte_cdk.models import Type as MessageType from airbyte_cdk.sources.file_based.availability_strategy import AbstractFileBasedAvailabilityStrategy from airbyte_cdk.sources.file_based.discovery_policy import AbstractDiscoveryPolicy +from airbyte_cdk.sources.file_based.exceptions import FileBasedErrorsCollector, FileBasedSourceError from airbyte_cdk.sources.file_based.file_based_stream_reader import AbstractFileBasedStreamReader from airbyte_cdk.sources.file_based.file_types.file_type_parser import FileTypeParser from airbyte_cdk.sources.file_based.remote_file import RemoteFile @@ -55,12 +58,17 @@ class MockFormat: ), pytest.param( {"type": "object", "properties": {"prop": {"type": "string"}}}, - {"type": ["null", "object"], "properties": {"prop": {"type": ["null", "string"]}}}, + { + "type": ["null", "object"], + "properties": {"prop": {"type": ["null", "string"]}}, + }, id="deeply-nested-schema", ), ], ) -def test_fill_nulls(input_schema: Mapping[str, Any], expected_output: Mapping[str, Any]) -> None: +def test_fill_nulls( + input_schema: Mapping[str, Any], expected_output: Mapping[str, Any] +) -> None: assert DefaultFileBasedStream._fill_nulls(input_schema) == expected_output @@ -90,21 +98,33 @@ def setUp(self) -> None: parsers={MockFormat: self._parser}, validation_policy=self._validation_policy, cursor=self._cursor, + errors_collector=FileBasedErrorsCollector(), ) def test_when_read_records_from_slice_then_return_records(self) -> None: self._parser.parse_records.return_value = [self._A_RECORD] - messages = list(self._stream.read_records_from_slice({"files": [RemoteFile(uri="uri", last_modified=self._NOW)]})) - assert list(map(lambda message: message.record.data["data"], messages)) == [self._A_RECORD] + messages = list( + self._stream.read_records_from_slice( + {"files": [RemoteFile(uri="uri", last_modified=self._NOW)]} + ) + ) + assert list(map(lambda message: message.record.data["data"], messages)) == [ + self._A_RECORD + ] - def test_given_exception_when_read_records_from_slice_then_do_process_other_files(self) -> None: + def test_given_exception_when_read_records_from_slice_then_do_process_other_files( + self, + ) -> None: """ The current behavior for source-s3 v3 does not fail sync on some errors and hence, we will keep this behaviour for now. One example we can easily reproduce this is by having a file with gzip extension that is not actually a gzip file. The reader will fail to open the file but the sync won't fail. Ticket: https://github.com/airbytehq/airbyte/issues/29680 """ - self._parser.parse_records.side_effect = [ValueError("An error"), [self._A_RECORD]] + self._parser.parse_records.side_effect = [ + ValueError("An error"), + [self._A_RECORD], + ] messages = list( self._stream.read_records_from_slice( @@ -120,7 +140,9 @@ def test_given_exception_when_read_records_from_slice_then_do_process_other_file assert messages[0].log.level == Level.ERROR assert messages[1].record.data["data"] == self._A_RECORD - def test_given_traced_exception_when_read_records_from_slice_then_fail(self) -> None: + def test_given_traced_exception_when_read_records_from_slice_then_fail( + self, + ) -> None: """ When a traced exception is raised, the stream shouldn't try to handle but pass it on to the caller. """ @@ -138,10 +160,14 @@ def test_given_traced_exception_when_read_records_from_slice_then_fail(self) -> ) ) - def test_given_exception_after_skipping_records_when_read_records_from_slice_then_send_warning(self) -> None: + def test_given_exception_after_skipping_records_when_read_records_from_slice_then_send_warning( + self, + ) -> None: self._stream_config.schemaless = False self._validation_policy.record_passes_validation_policy.return_value = False - self._parser.parse_records.side_effect = [self._iter([self._A_RECORD, ValueError("An error")])] + self._parser.parse_records.side_effect = [ + self._iter([self._A_RECORD, ValueError("An error")]) + ] messages = list( self._stream.read_records_from_slice( @@ -183,3 +209,54 @@ def _iter(self, x: Iterable[Any]) -> Iterator[Any]: if isinstance(item, Exception): raise item yield item + + +class TestFileBasedErrorCollector: + test_error_collector: FileBasedErrorsCollector = FileBasedErrorsCollector() + + @pytest.mark.parametrize( + "stream, file, line_no, n_skipped, collector_expected_len", + ( + ("stream_1", "test.csv", 1, 1, 1), + ("stream_2", "test2.csv", 2, 2, 2), + ), + ids=[ + "Single error", + "Multiple errors", + ], + ) + def test_collect_parsing_error( + self, stream, file, line_no, n_skipped, collector_expected_len + ) -> None: + test_error_pattern = "Error parsing record." + # format the error body + test_error = ( + AirbyteMessage( + type=MessageType.LOG, + log=AirbyteLogMessage( + level=Level.ERROR, + message=f"{FileBasedSourceError.ERROR_PARSING_RECORD.value} stream={stream} file={file} line_no={line_no} n_skipped={n_skipped}", + stack_trace=traceback.format_exc(), + ), + ), + ) + # collecting the error + self.test_error_collector.collect(test_error) + # check the error has been collected + assert len(self.test_error_collector.errors) == collector_expected_len + # check for the patern presence for the collected errors + for error in self.test_error_collector.errors: + assert test_error_pattern in error[0].log.message + + def test_yield_and_raise_collected(self) -> None: + # we expect the following method will raise the AirbyteTracedException + with pytest.raises(AirbyteTracedException) as parse_error: + list(self.test_error_collector.yield_and_raise_collected()) + assert ( + parse_error.value.message + == "Some errors occured while reading from the source." + ) + assert ( + parse_error.value.internal_message + == "Please check the logged errors for more information." + ) diff --git a/airbyte-cdk/python/unit_tests/sources/file_based/test_file_based_scenarios.py b/airbyte-cdk/python/unit_tests/sources/file_based/test_file_based_scenarios.py index 6bc58b8edf96f..e39c4b02c5a88 100644 --- a/airbyte-cdk/python/unit_tests/sources/file_based/test_file_based_scenarios.py +++ b/airbyte-cdk/python/unit_tests/sources/file_based/test_file_based_scenarios.py @@ -48,6 +48,7 @@ csv_strings_can_be_null_not_quoted_scenario, earlier_csv_scenario, empty_schema_inference_scenario, + invalid_csv_multi_scenario, invalid_csv_scenario, multi_csv_scenario, multi_csv_stream_n_file_exceeds_limit_for_inference, @@ -132,6 +133,7 @@ csv_multi_stream_scenario, csv_single_stream_scenario, invalid_csv_scenario, + invalid_csv_multi_scenario, single_csv_scenario, multi_csv_scenario, multi_csv_stream_n_file_exceeds_limit_for_inference, From 37cc90517d4435b3607bea11aabc4240226c1e30 Mon Sep 17 00:00:00 2001 From: bazarnov Date: Thu, 11 Jan 2024 19:34:51 +0000 Subject: [PATCH 068/574] =?UTF-8?q?=F0=9F=A4=96=20Bump=20patch=20version?= =?UTF-8?q?=20of=20Python=20CDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airbyte-cdk/python/.bumpversion.cfg | 2 +- airbyte-cdk/python/CHANGELOG.md | 3 +++ airbyte-cdk/python/Dockerfile | 2 +- airbyte-cdk/python/setup.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/airbyte-cdk/python/.bumpversion.cfg b/airbyte-cdk/python/.bumpversion.cfg index 8c0dfbd0ff1a2..a43a0aa292bf0 100644 --- a/airbyte-cdk/python/.bumpversion.cfg +++ b/airbyte-cdk/python/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.58.6 +current_version = 0.58.7 commit = False [bumpversion:file:setup.py] diff --git a/airbyte-cdk/python/CHANGELOG.md b/airbyte-cdk/python/CHANGELOG.md index 58937e371e008..3baf17d7480b3 100644 --- a/airbyte-cdk/python/CHANGELOG.md +++ b/airbyte-cdk/python/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 0.58.7 + + ## 0.58.6 File CDK: Added logic to emit logged `RecordParseError` errors and raise the single `AirbyteTracebackException` in the end of the sync, instead of silent skipping the parsing errors. PR: https://github.com/airbytehq/airbyte/pull/32589 diff --git a/airbyte-cdk/python/Dockerfile b/airbyte-cdk/python/Dockerfile index cb35d42e9f458..14c33a7d72a01 100644 --- a/airbyte-cdk/python/Dockerfile +++ b/airbyte-cdk/python/Dockerfile @@ -32,5 +32,5 @@ ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] # needs to be the same as CDK -LABEL io.airbyte.version=0.58.6 +LABEL io.airbyte.version=0.58.7 LABEL io.airbyte.name=airbyte/source-declarative-manifest diff --git a/airbyte-cdk/python/setup.py b/airbyte-cdk/python/setup.py index 0cee360df7134..129f675e6fc0f 100644 --- a/airbyte-cdk/python/setup.py +++ b/airbyte-cdk/python/setup.py @@ -36,7 +36,7 @@ name="airbyte-cdk", # The version of the airbyte-cdk package is used at runtime to validate manifests. That validation must be # updated if our semver format changes such as using release candidate versions. - version="0.58.6", + version="0.58.7", description="A framework for writing Airbyte Connectors.", long_description=README, long_description_content_type="text/markdown", From c7c6a27dec69d32fe75c7f4f61f052869d81a823 Mon Sep 17 00:00:00 2001 From: kekiss Date: Thu, 11 Jan 2024 11:43:27 -0800 Subject: [PATCH 069/574] Docs: Updated grammar and formatting for clarity and consistency (#34080) --- docs/integrations/sources/amazon-ads.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/integrations/sources/amazon-ads.md b/docs/integrations/sources/amazon-ads.md index 2305b656afebe..ecef322972b84 100644 --- a/docs/integrations/sources/amazon-ads.md +++ b/docs/integrations/sources/amazon-ads.md @@ -13,11 +13,11 @@ This page contains the setup guide and reference information for the Amazon Ads ## Setup guide ### Step 1: Set up Amazon Ads -Create an [Amazon user](https://www.amazon.com) with access to [Amazon Ads account](https://advertising.amazon.com). +Create an [Amazon user](https://www.amazon.com) with access to an [Amazon Ads account](https://advertising.amazon.com). **For Airbyte Open Source:** -To use the [Amazon Ads API](https://advertising.amazon.com/API/docs/en-us), you must first complete the [onboarding process](https://advertising.amazon.com/API/docs/en-us/setting-up/overview). The onboarding process has several steps and may take several days to complete. After completing all steps you will have to get Amazon client application `Client ID`, `Client Secret` and `Refresh Token`. +To use the [Amazon Ads API](https://advertising.amazon.com/API/docs/en-us), you must first complete the [onboarding process](https://advertising.amazon.com/API/docs/en-us/setting-up/overview). The onboarding process has several steps and may take several days to complete. After completing all steps you will have to get the Amazon client application's `Client ID`, `Client Secret` and `Refresh Token`. ### Step 2: Set up the Amazon Ads connector in Airbyte @@ -28,13 +28,13 @@ To use the [Amazon Ads API](https://advertising.amazon.com/API/docs/en-us), you 1. [Log into your Airbyte Cloud](https://cloud.airbyte.com/workspaces) account. 2. In the left navigation bar, click **Sources**. In the top-right corner, click **+ new source**. 3. On the source setup page, select **Amazon Ads** from the Source type dropdown and enter a name for this connector. -4. Click `Authenticate your Amazon Ads account`. +4. Click **Authenticate your Amazon Ads account**. 5. Log in and Authorize to the Amazon account. 6. Select **Region** to pull data from **North America (NA)**, **Europe (EU)**, **Far East (FE)**. See [docs](https://advertising.amazon.com/API/docs/en-us/info/api-overview#api-endpoints) for more details. -7. **Start Date (Optional)** is used for generating reports starting from the specified start date. Should be in YYYY-MM-DD format and not more than 60 days in the past. If not specified today's date is used. The date is treated in the timezone of the processed profile. +7. **Start Date (Optional)** is used for generating reports starting from the specified start date. This should be in YYYY-MM-DD format and not more than 60 days in the past. If a date is not specified, today's date is used. The date is treated in the timezone of the processed profile. 8. **Profile IDs (Optional)** you want to fetch data for. See [docs](https://advertising.amazon.com/API/docs/en-us/concepts/authorization/profiles) for more details. 9. **Marketplace IDs (Optional)** you want to fetch data for. _Note: If Profile IDs are also selected, profiles will be selected if they match the Profile ID **OR** the Marketplace ID._ -10. Click `Set up source`. +10. Click **Set up source**. @@ -44,7 +44,7 @@ To use the [Amazon Ads API](https://advertising.amazon.com/API/docs/en-us), you 2. **Client Secret** of your Amazon Ads developer application. See [onboarding process](https://advertising.amazon.com/API/docs/en-us/setting-up/overview) for more details. 3. **Refresh Token**. See [onboarding process](https://advertising.amazon.com/API/docs/en-us/setting-up/overview) for more details. 4. Select **Region** to pull data from **North America (NA)**, **Europe (EU)**, **Far East (FE)**. See [docs](https://advertising.amazon.com/API/docs/en-us/info/api-overview#api-endpoints) for more details. -5. **Start Date (Optional)** is used for generating reports starting from the specified start date. Should be in YYYY-MM-DD format and not more than 60 days in the past. If not specified today's date is used. The date is treated in the timezone of the processed profile. +5. **Start Date (Optional)** is used for generating reports starting from the specified start date. This should be in YYYY-MM-DD format and not more than 60 days in the past. If a date is not specified, today's date is used. The date is treated in the timezone of the processed profile. 6. **Profile IDs (Optional)** you want to fetch data for. See [docs](https://advertising.amazon.com/API/docs/en-us/concepts/authorization/profiles) for more details. 7. **Marketplace IDs (Optional)** you want to fetch data for. _Note: If Profile IDs are also selected, profiles will be selected if they match the Profile ID **OR** the Marketplace ID._ @@ -85,15 +85,15 @@ This source is capable of syncing the following streams: ## Connector-specific features and highlights -All the reports are generated relative to the target profile' timezone. +All the reports are generated relative to the target profile's timezone. -Campaign reports may sometimes have no data or not presenting in records. This can occur when there are no clicks or views associated with the campaigns on the requested day - [details](https://advertising.amazon.com/API/docs/en-us/guides/reporting/v2/faq#why-is-my-report-empty). +Campaign reports may sometimes have no data or may not be presenting in records. This can occur when there are no clicks or views associated with the campaigns on the requested day - [details](https://advertising.amazon.com/API/docs/en-us/guides/reporting/v2/faq#why-is-my-report-empty). -Report data synchronization only cover the last 60 days - [details](https://advertising.amazon.com/API/docs/en-us/reference/1/reports#parameters). +Report data synchronization only covers the last 60 days - [details](https://advertising.amazon.com/API/docs/en-us/reference/1/reports#parameters). ## Performance considerations -Information about expected report generation waiting time you may find [here](https://advertising.amazon.com/API/docs/en-us/get-started/developer-notes). +Information about expected report generation waiting time can be found [here](https://advertising.amazon.com/API/docs/en-us/get-started/developer-notes). ### Data type mapping From c1574b800f38fd96d9704d142e8f7d90c1b38940 Mon Sep 17 00:00:00 2001 From: Anatolii Yatsuk <35109939+tolik0@users.noreply.github.com> Date: Thu, 11 Jan 2024 21:44:59 +0200 Subject: [PATCH 070/574] =?UTF-8?q?=E2=9C=A8Source=20Google=20Ads:=20Add?= =?UTF-8?q?=20possibility=20to=20sync=20all=20connected=20accounts=20(#337?= =?UTF-8?q?07)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../source-google-ads/metadata.yaml | 2 +- .../source_google_ads/google_ads.py | 42 +++- .../source_google_ads/models.py | 25 +- .../schemas/customer_client.json | 24 ++ .../source_google_ads/source.py | 70 ++++-- .../source_google_ads/spec.json | 31 +-- .../source_google_ads/streams.py | 93 +++++++- .../source-google-ads/unit_tests/common.py | 5 +- .../source-google-ads/unit_tests/conftest.py | 2 + .../unit_tests/test_errors.py | 20 +- .../unit_tests/test_google_ads.py | 2 +- .../test_incremental_events_streams.py | 59 ++++- .../unit_tests/test_models.py | 13 +- .../unit_tests/test_source.py | 97 +++++++- .../unit_tests/test_streams.py | 38 +-- docs/integrations/sources/google-ads.md | 225 +++++++++--------- 16 files changed, 538 insertions(+), 210 deletions(-) create mode 100644 airbyte-integrations/connectors/source-google-ads/source_google_ads/schemas/customer_client.json diff --git a/airbyte-integrations/connectors/source-google-ads/metadata.yaml b/airbyte-integrations/connectors/source-google-ads/metadata.yaml index f77549067f968..50a62c4282e3f 100644 --- a/airbyte-integrations/connectors/source-google-ads/metadata.yaml +++ b/airbyte-integrations/connectors/source-google-ads/metadata.yaml @@ -11,7 +11,7 @@ data: connectorSubtype: api connectorType: source definitionId: 253487c0-2246-43ba-a21f-5116b20a2c50 - dockerImageTag: 3.1.0 + dockerImageTag: 3.2.0 dockerRepository: airbyte/source-google-ads documentationUrl: https://docs.airbyte.com/integrations/sources/google-ads githubIssueLabel: source-google-ads diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/google_ads.py b/airbyte-integrations/connectors/source-google-ads/source_google_ads/google_ads.py index 05e8ef37a64fe..c34833154ce67 100644 --- a/airbyte-integrations/connectors/source-google-ads/source_google_ads/google_ads.py +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/google_ads.py @@ -27,8 +27,28 @@ def __init__(self, credentials: MutableMapping[str, Any]): # `google-ads` library version `14.0.0` and higher requires an additional required parameter `use_proto_plus`. # More details can be found here: https://developers.google.com/google-ads/api/docs/client-libs/python/protobuf-messages credentials["use_proto_plus"] = True - self.client = self.get_google_ads_client(credentials) - self.ga_service = self.client.get_service("GoogleAdsService") + self.clients = {} + self.ga_services = {} + self.credentials = credentials + + self.clients["default"] = self.get_google_ads_client(credentials) + self.ga_services["default"] = self.clients["default"].get_service("GoogleAdsService") + + self.customer_service = self.clients["default"].get_service("CustomerService") + + def get_client(self, login_customer_id="default"): + if login_customer_id in self.clients: + return self.clients[login_customer_id] + new_creds = self.credentials.copy() + new_creds["login_customer_id"] = login_customer_id + self.clients[login_customer_id] = self.get_google_ads_client(new_creds) + return self.clients[login_customer_id] + + def ga_service(self, login_customer_id="default"): + if login_customer_id in self.ga_services: + return self.ga_services[login_customer_id] + self.ga_services[login_customer_id] = self.clients[login_customer_id].get_service("GoogleAdsService") + return self.ga_services[login_customer_id] @staticmethod def get_google_ads_client(credentials) -> GoogleAdsClient: @@ -38,6 +58,14 @@ def get_google_ads_client(credentials) -> GoogleAdsClient: message = "The authentication to Google Ads has expired. Re-authenticate to restore access to Google Ads." raise AirbyteTracedException(message=message, failure_type=FailureType.config_error) from e + def get_accessible_accounts(self): + customer_resource_names = self.customer_service.list_accessible_customers().resource_names + logger.info(f"Found {len(customer_resource_names)} accessible accounts: {customer_resource_names}") + + for customer_resource_name in customer_resource_names: + customer_id = self.ga_service().parse_customer_path(customer_resource_name)["customer_id"] + yield customer_id + @backoff.on_exception( backoff.expo, (InternalServerError, ServerError, TooManyRequests), @@ -46,13 +74,13 @@ def get_google_ads_client(credentials) -> GoogleAdsClient: ), max_tries=5, ) - def send_request(self, query: str, customer_id: str) -> Iterator[SearchGoogleAdsResponse]: - client = self.client + def send_request(self, query: str, customer_id: str, login_customer_id: str = "default") -> Iterator[SearchGoogleAdsResponse]: + client = self.get_client(login_customer_id) search_request = client.get_type("SearchGoogleAdsRequest") search_request.query = query search_request.page_size = self.DEFAULT_PAGE_SIZE search_request.customer_id = customer_id - return [self.ga_service.search(search_request)] + return [self.ga_service(login_customer_id).search(search_request)] def get_fields_metadata(self, fields: List[str]) -> Mapping[str, Any]: """ @@ -61,8 +89,8 @@ def get_fields_metadata(self, fields: List[str]) -> Mapping[str, Any]: :return dict of fields type info. """ - ga_field_service = self.client.get_service("GoogleAdsFieldService") - request = self.client.get_type("SearchGoogleAdsFieldsRequest") + ga_field_service = self.get_client().get_service("GoogleAdsFieldService") + request = self.get_client().get_type("SearchGoogleAdsFieldsRequest") request.page_size = len(fields) fields_sql = ",".join([f"'{field}'" for field in fields]) request.query = f""" diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/models.py b/airbyte-integrations/connectors/source-google-ads/source_google_ads/models.py index c11ffaf0c57c4..7da4ed7c2b9c9 100644 --- a/airbyte-integrations/connectors/source-google-ads/source_google_ads/models.py +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/models.py @@ -4,27 +4,32 @@ from dataclasses import dataclass -from typing import Any, Iterable, Mapping, Union +from typing import Any, Iterable, Mapping -from pendulum import timezone +from pendulum import local_timezone, timezone from pendulum.tz.timezone import Timezone @dataclass class CustomerModel: id: str - time_zone: Union[timezone, str] = "local" + time_zone: timezone = local_timezone() is_manager_account: bool = False + login_customer_id: str = None @classmethod - def from_accounts(cls, accounts: Iterable[Iterable[Mapping[str, Any]]]): + def from_accounts(cls, accounts: Iterable[Mapping[str, Any]]) -> Iterable["CustomerModel"]: data_objects = [] - for account_list in accounts: - for account in account_list: - time_zone_name = account.get("customer.time_zone") - tz = Timezone(time_zone_name) if time_zone_name else "local" + for account in accounts: + time_zone_name = account.get("customer_client.time_zone") + tz = Timezone(time_zone_name) if time_zone_name else local_timezone() - data_objects.append( - cls(id=str(account["customer.id"]), time_zone=tz, is_manager_account=bool(account.get("customer.manager"))) + data_objects.append( + cls( + id=str(account["customer_client.id"]), + time_zone=tz, + is_manager_account=bool(account.get("customer_client.manager")), + login_customer_id=account.get("login_customer_id"), ) + ) return data_objects diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/schemas/customer_client.json b/airbyte-integrations/connectors/source-google-ads/source_google_ads/schemas/customer_client.json new file mode 100644 index 0000000000000..efb4bfd93f78c --- /dev/null +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/schemas/customer_client.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "customer_client.client_customer": { + "type": ["null", "boolean"] + }, + "customer_client.level": { + "type": ["null", "string"] + }, + "customer_client.id": { + "type": ["null", "integer"] + }, + "customer_client.manager": { + "type": ["null", "boolean"] + }, + "customer_client.time_zone": { + "type": ["null", "number"] + }, + "customer_client.status": { + "type": ["null", "string"] + } + } +} diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/source.py b/airbyte-integrations/connectors/source-google-ads/source_google_ads/source.py index 1e23f20fe5129..2402cd18adbed 100644 --- a/airbyte-integrations/connectors/source-google-ads/source_google_ads/source.py +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/source.py @@ -34,6 +34,7 @@ CampaignLabel, ClickView, Customer, + CustomerClient, CustomerLabel, DisplayKeywordView, GeographicView, @@ -47,6 +48,8 @@ ) from .utils import GAQL +logger = logging.getLogger("airbyte") + class SourceGoogleAds(AbstractSource): # Skip exceptions on missing streams @@ -65,6 +68,11 @@ def _validate_and_transform(config: Mapping[str, Any]): "https://developers.google.com/google-ads/api/fields/v15/query_validator" ) raise AirbyteTracedException(message=message, failure_type=FailureType.config_error) + + if "customer_id" in config: + config["customer_ids"] = config["customer_id"].split(",") + config.pop("customer_id") + return config @staticmethod @@ -73,10 +81,6 @@ def get_credentials(config: Mapping[str, Any]) -> MutableMapping[str, Any]: # use_proto_plus is set to True, because setting to False returned wrong value types, which breaks the backward compatibility. # For more info read the related PR's description: https://github.com/airbytehq/airbyte/pull/9996 credentials.update(use_proto_plus=True) - - # https://developers.google.com/google-ads/api/docs/concepts/call-structure#cid - if "login_customer_id" in config and config["login_customer_id"].strip(): - credentials["login_customer_id"] = config["login_customer_id"] return credentials @staticmethod @@ -98,12 +102,45 @@ def get_incremental_stream_config(google_api: GoogleAds, config: Mapping[str, An ) return incremental_stream_config - @staticmethod - def get_account_info(google_api: GoogleAds, config: Mapping[str, Any]) -> Iterable[Iterable[Mapping[str, Any]]]: - dummy_customers = [CustomerModel(id=_id) for _id in config["customer_id"].split(",")] - accounts_stream = ServiceAccounts(google_api, customers=dummy_customers) - for slice_ in accounts_stream.stream_slices(): - yield accounts_stream.read_records(sync_mode=SyncMode.full_refresh, stream_slice=slice_) + def get_all_accounts(self, google_api: GoogleAds, customers: List[CustomerModel], customer_status_filter: List[str]) -> List[str]: + customer_clients_stream = CustomerClient(api=google_api, customers=customers, customer_status_filter=customer_status_filter) + for slice in customer_clients_stream.stream_slices(): + for record in customer_clients_stream.read_records(sync_mode=SyncMode.full_refresh, stream_slice=slice): + yield record + + def _get_all_connected_accounts( + self, google_api: GoogleAds, customer_status_filter: List[str] + ) -> Iterable[Iterable[Mapping[str, Any]]]: + customer_ids = [customer_id for customer_id in google_api.get_accessible_accounts()] + dummy_customers = [CustomerModel(id=_id, login_customer_id=_id) for _id in customer_ids] + + yield from self.get_all_accounts(google_api, dummy_customers, customer_status_filter) + + def get_customers(self, google_api: GoogleAds, config: Mapping[str, Any]) -> List[CustomerModel]: + customer_status_filter = config.get("customer_status_filter", []) + accounts = self._get_all_connected_accounts(google_api, customer_status_filter) + customers = CustomerModel.from_accounts(accounts) + + # filter duplicates as one customer can be accessible from mutiple connected accounts + unique_customers = [] + seen_ids = set() + for customer in customers: + if customer.id in seen_ids: + continue + seen_ids.add(customer.id) + unique_customers.append(customer) + customers = unique_customers + customers_dict = {customer.id: customer for customer in customers} + + # filter only selected accounts + if config.get("customer_ids"): + customers = [] + for customer_id in config["customer_ids"]: + if customer_id not in customers_dict: + logging.warning(f"Customer with id {customer_id} is not accessible. Skipping it.") + else: + customers.append(customers_dict[customer_id]) + return customers @staticmethod def is_metrics_in_custom_query(query: GAQL) -> bool: @@ -149,8 +186,9 @@ def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> logger.info("Checking the config") google_api = GoogleAds(credentials=self.get_credentials(config)) - accounts = self.get_account_info(google_api, config) - customers = CustomerModel.from_accounts(accounts) + customers = self.get_customers(google_api, config) + logger.info(f"Found {len(customers)} customers: {[customer.id for customer in customers]}") + # Check custom query request validity by sending metric request with non-existent time window for customer in customers: for query in config.get("custom_queries_array", []): @@ -168,7 +206,7 @@ def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> query = IncrementalCustomQuery.insert_segments_date_expr(query, "1980-01-01", "1980-01-01") query = query.set_limit(1) - response = google_api.send_request(str(query), customer_id=customer.id) + response = google_api.send_request(str(query), customer_id=customer.id, login_customer_id=customer.login_customer_id) # iterate over the response otherwise exceptions will not be raised! for _ in response: pass @@ -177,8 +215,10 @@ def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> def streams(self, config: Mapping[str, Any]) -> List[Stream]: config = self._validate_and_transform(config) google_api = GoogleAds(credentials=self.get_credentials(config)) - accounts = self.get_account_info(google_api, config) - customers = CustomerModel.from_accounts(accounts) + + customers = self.get_customers(google_api, config) + logger.info(f"Found {len(customers)} customers: {[customer.id for customer in customers]}") + non_manager_accounts = [customer for customer in customers if not customer.is_manager_account] default_config = dict(api=google_api, customers=customers) incremental_config = self.get_incremental_stream_config(google_api, config, customers) diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/spec.json b/airbyte-integrations/connectors/source-google-ads/source_google_ads/spec.json index b875b6d419d90..2b84f6bc1beb3 100644 --- a/airbyte-integrations/connectors/source-google-ads/source_google_ads/spec.json +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/spec.json @@ -4,7 +4,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Google Ads Spec", "type": "object", - "required": ["credentials", "customer_id"], + "required": ["credentials"], "additionalProperties": true, "properties": { "credentials": { @@ -64,6 +64,18 @@ "examples": ["6783948572,5839201945"], "order": 1 }, + "customer_status_filter": { + "title": "Customer Statuses Filter", + "description": "A list of customer statuses to filter on. For detailed info about what each status mean refer to Google Ads documentation.", + "default": [], + "order": 2, + "type": "array", + "items": { + "title": "CustomerStatus", + "description": "An enumeration.", + "enum": ["UNKNOWN", "ENABLED", "CANCELED", "SUSPENDED", "CLOSED"] + } + }, "start_date": { "type": "string", "title": "Start Date", @@ -71,7 +83,7 @@ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", "pattern_descriptor": "YYYY-MM-DD", "examples": ["2017-01-25"], - "order": 2, + "order": 3, "format": "date" }, "end_date": { @@ -81,14 +93,14 @@ "pattern": "^$|^[0-9]{4}-[0-9]{2}-[0-9]{2}$", "pattern_descriptor": "YYYY-MM-DD", "examples": ["2017-01-30"], - "order": 6, + "order": 4, "format": "date" }, "custom_queries_array": { "type": "array", "title": "Custom GAQL Queries", "description": "", - "order": 3, + "order": 5, "items": { "type": "object", "required": ["query", "table_name"], @@ -110,15 +122,6 @@ } } }, - "login_customer_id": { - "type": "string", - "title": "Login Customer ID for Managed Accounts", - "description": "If your access to the customer account is through a manager account, this field is required, and must be set to the 10-digit customer ID of the manager account. For more information about this field, refer to Google's documentation.", - "pattern_descriptor": ": 10 digits, with no dashes.", - "pattern": "^([0-9]{10})?$", - "examples": ["7349206847"], - "order": 4 - }, "conversion_window_days": { "title": "Conversion Window", "type": "integer", @@ -127,7 +130,7 @@ "maximum": 1095, "default": 14, "examples": [14], - "order": 5 + "order": 6 } } }, diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py b/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py index 695ad285c8a64..d843771b82e2d 100644 --- a/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py @@ -42,7 +42,7 @@ def parse_response(self, response: SearchPager, stream_slice: Optional[Mapping[s def stream_slices(self, stream_state: Mapping[str, Any] = None, **kwargs) -> Iterable[Optional[Mapping[str, any]]]: for customer in self.customers: - yield {"customer_id": customer.id} + yield {"customer_id": customer.id, "login_customer_id": customer.login_customer_id} @generator_backoff( wait_gen=backoff.constant, @@ -54,8 +54,8 @@ def stream_slices(self, stream_state: Mapping[str, Any] = None, **kwargs) -> Ite interval=1, ) @detached(timeout_minutes=5) - def request_records_job(self, customer_id, query, stream_slice): - response_records = self.google_ads_client.send_request(query=query, customer_id=customer_id) + def request_records_job(self, customer_id, login_customer_id, query, stream_slice): + response_records = self.google_ads_client.send_request(query=query, customer_id=customer_id, login_customer_id=login_customer_id) yield from self.parse_records_with_backoff(response_records, stream_slice) def read_records(self, sync_mode, stream_slice: Optional[Mapping[str, Any]] = None, **kwargs) -> Iterable[Mapping[str, Any]]: @@ -63,8 +63,10 @@ def read_records(self, sync_mode, stream_slice: Optional[Mapping[str, Any]] = No return [] customer_id = stream_slice["customer_id"] + login_customer_id = stream_slice["login_customer_id"] + try: - yield from self.request_records_job(customer_id, self.get_query(stream_slice), stream_slice) + yield from self.request_records_job(customer_id, login_customer_id, self.get_query(stream_slice), stream_slice) except (GoogleAdsException, Unauthenticated) as exception: traced_exception(exception, customer_id, self.CATCH_CUSTOMER_NOT_ENABLED_ERROR) except TimeoutError as exception: @@ -149,6 +151,7 @@ def stream_slices(self, stream_state: Mapping[str, Any] = None, **kwargs) -> Ite ): if chunk: chunk["customer_id"] = customer.id + chunk["login_customer_id"] = customer.login_customer_id yield chunk def _update_state(self, customer_id: str, record: MutableMapping[str, Any]): @@ -228,6 +231,63 @@ def parse_response(self, response: SearchPager, stream_slice: Optional[Mapping[s yield record +class CustomerClient(GoogleAdsStream): + """ + Customer Client stream: https://developers.google.com/google-ads/api/fields/v15/customer_client + """ + + CATCH_CUSTOMER_NOT_ENABLED_ERROR = False + primary_key = ["customer_client.id"] + + def __init__(self, customer_status_filter: List[str], **kwargs): + self.customer_status_filter = customer_status_filter + super().__init__(**kwargs) + + def get_query(self, stream_slice: Mapping[str, Any] = None) -> str: + fields = GoogleAds.get_fields_from_schema(self.get_json_schema()) + table_name = get_resource_name(self.name) + + active_customers_condition = [] + if self.customer_status_filter: + customer_status_filter = ", ".join([f"'{status}'" for status in self.customer_status_filter]) + active_customers_condition = [f"customer_client.status in ({customer_status_filter})"] + + query = GoogleAds.convert_schema_into_query(fields=fields, table_name=table_name, conditions=active_customers_condition) + return query + + def read_records(self, sync_mode, stream_slice: Optional[Mapping[str, Any]] = None, **kwargs) -> Iterable[Mapping[str, Any]]: + """ + This method is overridden to avoid using login_customer_id from dummy_customers. + + login_customer_id is used in the stream_slices to pass it to child customers, + but we don't need it here as this class iterate over customers accessible from user creds. + """ + if stream_slice is None: + return [] + + customer_id = stream_slice["customer_id"] + + try: + response_records = self.google_ads_client.send_request(self.get_query(stream_slice), customer_id=customer_id) + + yield from self.parse_records_with_backoff(response_records, stream_slice) + except GoogleAdsException as exception: + traced_exception(exception, customer_id, self.CATCH_CUSTOMER_NOT_ENABLED_ERROR) + + def parse_response(self, response: SearchPager, stream_slice: Optional[Mapping[str, Any]] = None) -> Iterable[Mapping]: + """ + login_cusotmer_id is populated to child customers if they are under managers account + """ + records = [record for record in super().parse_response(response)] + + # read_records get all customers connected to customer_id from stream_slice + # if the result is more than one customer, it's a manager, otherwise it is client account for which we don't need login_customer_id + root_is_manager = len(records) > 1 + for record in records: + record["login_customer_id"] = stream_slice["login_customer_id"] if root_is_manager else "default" + yield record + + class CustomerLabel(GoogleAdsStream): """ Customer Label stream: https://developers.google.com/google-ads/api/fields/v15/customer_label @@ -589,7 +649,13 @@ def stream_slices(self, stream_state: Mapping[str, Any] = None, **kwargs) -> Ite yield from slices_generator else: for customer in self.customers: - yield {"customer_id": customer.id, "updated_ids": set(), "deleted_ids": set(), "record_changed_time_map": dict()} + yield { + "customer_id": customer.id, + "login_customer_id": customer.login_customer_id, + "updated_ids": set(), + "deleted_ids": set(), + "record_changed_time_map": dict(), + } def _process_parent_record(self, parent_record: MutableMapping[str, Any], child_slice: MutableMapping[str, Any]) -> bool: """Process a single parent_record and update the child_slice.""" @@ -613,7 +679,13 @@ def read_parent_stream( sync_mode=sync_mode, cursor_field=cursor_field, stream_state=stream_state.get(self.parent_stream_name) ): customer_id = parent_slice.get("customer_id") - child_slice = {"customer_id": customer_id, "updated_ids": set(), "deleted_ids": set(), "record_changed_time_map": dict()} + child_slice = { + "customer_id": customer_id, + "updated_ids": set(), + "deleted_ids": set(), + "record_changed_time_map": dict(), + "login_customer_id": parent_slice.get("login_customer_id"), + } if not self.get_current_state(customer_id): yield child_slice continue @@ -673,13 +745,20 @@ def _split_slice(child_slice: MutableMapping[str, Any], chunk_size: int = 10000) record_changed_time_map = child_slice["record_changed_time_map"] customer_id = child_slice["customer_id"] + login_customer_id = child_slice["login_customer_id"] # Split the updated_ids into chunks and yield them for i in range(0, len(updated_ids), chunk_size): chunk_ids = set(updated_ids[i : i + chunk_size]) chunk_time_map = {k: record_changed_time_map[k] for k in chunk_ids} - yield {"updated_ids": chunk_ids, "record_changed_time_map": chunk_time_map, "customer_id": customer_id, "deleted_ids": set()} + yield { + "updated_ids": chunk_ids, + "record_changed_time_map": chunk_time_map, + "customer_id": customer_id, + "deleted_ids": set(), + "login_customer_id": login_customer_id, + } def read_records( self, sync_mode: SyncMode, cursor_field: List[str] = None, stream_slice: MutableMapping[str, Any] = None, **kwargs diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/common.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/common.py index d1cc1033e31b9..b2bff404d6e27 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/common.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/common.py @@ -44,9 +44,12 @@ def get_service(self, service): def load_from_dict(config, version=None): return MockGoogleAdsClient(config) - def send_request(self, query, customer_id): + def send_request(self, query, customer_id, login_customer_id="none"): yield from () + def get_accessible_accounts(self): + yield from ["fake_customer_id", "fake_customer_id_2"] + class MockGoogleAdsFieldService: _instance = None diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/conftest.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/conftest.py index 845780e9f3838..859c7b31d81ff 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/conftest.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/conftest.py @@ -3,6 +3,8 @@ # +from unittest.mock import Mock + import pytest from source_google_ads.models import CustomerModel diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_errors.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_errors.py index ef599d97cbf8f..9bf943bb145d1 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_errors.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_errors.py @@ -10,11 +10,21 @@ from airbyte_cdk import AirbyteLogger from airbyte_cdk.utils import AirbyteTracedException from source_google_ads.google_ads import GoogleAds +from source_google_ads.models import CustomerModel from source_google_ads.source import SourceGoogleAds from source_google_ads.streams import AdGroupLabel, Label, ServiceAccounts from .common import MockGoogleAdsClient, mock_google_ads_request_failure + +@pytest.fixture +def mock_get_customers(mocker): + mocker.patch( + "source_google_ads.source.SourceGoogleAds.get_customers", + Mock(return_value=[CustomerModel(is_manager_account=False, time_zone="Europe/Berlin", id="123")]), + ) + + params = [ ( ["USER_PERMISSION_DENIED"], @@ -51,6 +61,10 @@ @pytest.mark.parametrize(("exception", "error_message"), params) def test_expected_errors(mocker, config, exception, error_message): mock_google_ads_request_failure(mocker, exception) + mocker.patch( + "source_google_ads.google_ads.GoogleAds.get_accessible_accounts", + Mock(return_value=["123", "12345"]), + ) source = SourceGoogleAds() with pytest.raises(AirbyteTracedException) as exception: status_ok, error = source.check_connection(AirbyteLogger(), config) @@ -74,7 +88,7 @@ def test_read_record_error_handling(mocker, config, customers, cls, raise_expect context = pytest.raises(AirbyteTracedException) if raise_expected else does_not_raise() with context as exception: - for _ in stream.read_records(sync_mode=Mock(), stream_slice={"customer_id": "1234567890"}): + for _ in stream.read_records(sync_mode=Mock(), stream_slice={"customer_id": "1234567890", "login_customer_id": "default"}): pass if raise_expected: @@ -131,8 +145,8 @@ def test_read_record_error_handling(mocker, config, customers, cls, raise_expect def test_check_custom_queries(mocker, config, custom_query, is_manager_account, error_message, warning): config["custom_queries_array"] = [custom_query] mocker.patch( - "source_google_ads.source.SourceGoogleAds.get_account_info", - Mock(return_value=[[{"customer.manager": is_manager_account, "customer.time_zone": "Europe/Berlin", "customer.id": "8765"}]]), + "source_google_ads.source.SourceGoogleAds.get_customers", + Mock(return_value=[CustomerModel(is_manager_account=is_manager_account, time_zone="Europe/Berlin", id="8765")]), ) mocker.patch("source_google_ads.google_ads.GoogleAdsClient", return_value=MockGoogleAdsClient) source = SourceGoogleAds() diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_google_ads.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_google_ads.py index ecb65916c5442..3f66564846f44 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_google_ads.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_google_ads.py @@ -169,7 +169,7 @@ def test_get_fields_metadata(mocker): response = google_ads_client.get_fields_metadata(fields) # Get the mock service to check the request query - mock_service = google_ads_client.client.get_service("GoogleAdsFieldService") + mock_service = google_ads_client.get_client().get_service("GoogleAdsFieldService") # Assert the constructed request query expected_query = """ diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_incremental_events_streams.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_incremental_events_streams.py index 929d5f22f29c6..8ddf8bd80fbac 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_incremental_events_streams.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_incremental_events_streams.py @@ -54,7 +54,7 @@ class MockGoogleAds(GoogleAds): def parse_single_result(self, schema, result): return result - def send_request(self, query: str, customer_id: str): + def send_request(self, query: str, customer_id: str, login_customer_id: str = "default"): if query == "query_parent": return mock_response_parent() else: @@ -64,7 +64,7 @@ def send_request(self, query: str, customer_id: str): def test_change_status_stream(config, customers): """ """ customer_id = next(iter(customers)).id - stream_slice = {"customer_id": customer_id} + stream_slice = {"customer_id": customer_id, "login_customer_id": "default"} google_api = MockGoogleAds(credentials=config["credentials"]) @@ -78,7 +78,7 @@ def test_change_status_stream(config, customers): ) assert len(result) == 4 assert stream.get_query.call_count == 1 - stream.get_query.assert_called_with({"customer_id": customer_id}) + stream.get_query.assert_called_with({"customer_id": customer_id, "login_customer_id": "default"}) def test_child_incremental_events_read(config, customers): @@ -89,7 +89,7 @@ def test_child_incremental_events_read(config, customers): It shouldn't read records on 2021-01-01, 2021-01-02 """ customer_id = next(iter(customers)).id - parent_stream_slice = {"customer_id": customer_id, "resource_type": "CAMPAIGN_CRITERION"} + parent_stream_slice = {"customer_id": customer_id, "resource_type": "CAMPAIGN_CRITERION", "login_customer_id": "default"} stream_state = {"change_status": {customer_id: {"change_status.last_change_date_time": "2023-08-16 13:20:01.003295"}}} google_api = MockGoogleAds(credentials=config["credentials"]) @@ -121,6 +121,7 @@ def test_child_incremental_events_read(config, customers): "3": "2023-06-13 12:36:03.772447", "4": "2023-06-13 12:36:04.772447", }, + "login_customer_id": "default", } ] @@ -221,7 +222,7 @@ class MockGoogleAdsLimit(GoogleAds): def parse_single_result(self, schema, result): return result - def send_request(self, query: str, customer_id: str): + def send_request(self, query: str, customer_id: str, login_customer_id: str = "default"): self.count += 1 if self.count == 1: return mock_response_1() @@ -255,7 +256,12 @@ def test_query_limit_hit(config, customers): This test simulates a scenario where the limit is hit and slice start_date is updated with latest record cursor """ customer_id = next(iter(customers)).id - stream_slice = {"customer_id": customer_id, "start_date": "2023-06-13 11:35:04.772447", "end_date": "2023-06-13 13:36:04.772447"} + stream_slice = { + "customer_id": customer_id, + "start_date": "2023-06-13 11:35:04.772447", + "end_date": "2023-06-13 13:36:04.772447", + "login_customer_id": "default", + } google_api = MockGoogleAdsLimit(credentials=config["credentials"]) stream_config = dict( @@ -275,16 +281,37 @@ def test_query_limit_hit(config, customers): assert stream.get_query.call_count == 3 get_query_calls = [ - call({"customer_id": "123", "start_date": "2023-06-13 11:35:04.772447", "end_date": "2023-06-13 13:36:04.772447"}), - call({"customer_id": "123", "start_date": "2023-06-13 12:36:02.772447", "end_date": "2023-06-13 13:36:04.772447"}), - call({"customer_id": "123", "start_date": "2023-06-13 12:36:04.772447", "end_date": "2023-06-13 13:36:04.772447"}), + call( + { + "customer_id": "123", + "start_date": "2023-06-13 11:35:04.772447", + "end_date": "2023-06-13 13:36:04.772447", + "login_customer_id": "default", + } + ), + call( + { + "customer_id": "123", + "start_date": "2023-06-13 12:36:02.772447", + "end_date": "2023-06-13 13:36:04.772447", + "login_customer_id": "default", + } + ), + call( + { + "customer_id": "123", + "start_date": "2023-06-13 12:36:04.772447", + "end_date": "2023-06-13 13:36:04.772447", + "login_customer_id": "default", + } + ), ] get_query_mock.assert_has_calls(get_query_calls) class MockGoogleAdsLimitException(MockGoogleAdsLimit): - def send_request(self, query: str, customer_id: str): + def send_request(self, query: str, customer_id: str, login_customer_id: str = "default"): self.count += 1 if self.count == 1: return mock_response_1() @@ -302,7 +329,12 @@ def test_query_limit_hit_exception(config, customers): then error will be raised """ customer_id = next(iter(customers)).id - stream_slice = {"customer_id": customer_id, "start_date": "2023-06-13 11:35:04.772447", "end_date": "2023-06-13 13:36:04.772447"} + stream_slice = { + "customer_id": customer_id, + "start_date": "2023-06-13 11:35:04.772447", + "end_date": "2023-06-13 13:36:04.772447", + "login_customer_id": "default", + } google_api = MockGoogleAdsLimitException(credentials=config["credentials"]) stream_config = dict( @@ -342,6 +374,7 @@ def test_change_status_get_query(mocker, config, customers): "start_date": "2023-01-01 00:00:00.000000", "end_date": "2023-09-19 00:00:00.000000", "resource_type": "SOME_RESOURCE_TYPE", + "login_customer_id": "default", } # Call the get_query method with the stream_slice @@ -402,6 +435,7 @@ def test_incremental_events_stream_get_query(mocker, config, customers): "customers/1234567890/adGroupCriteria/111111111111~4": "2023-09-18 08:56:59.165599", "customers/1234567890/adGroupCriteria/111111111111~5": "2023-09-18 08:56:59.165599", }, + "login_customer_id": "default", } # Call the get_query method with the stream_slice @@ -431,6 +465,7 @@ def test_read_records_with_slice_splitting(mocker, config): "record_changed_time_map": {i: f"time_{i}" for i in range(15000)}, "customer_id": "sample_customer_id", "deleted_ids": set(), + "login_customer_id": "default", } # Create a mock instance of the CampaignCriterion stream @@ -455,12 +490,14 @@ def test_read_records_with_slice_splitting(mocker, config): "record_changed_time_map": {i: f"time_{i}" for i in range(10000)}, "customer_id": "sample_customer_id", "deleted_ids": set(), + "login_customer_id": "default", } expected_second_slice = { "updated_ids": set(range(10000, 15000)), "record_changed_time_map": {i: f"time_{i}" for i in range(10000, 15000)}, "customer_id": "sample_customer_id", "deleted_ids": set(), + "login_customer_id": "default", } # Verify the arguments passed to the parent's read_records method for both calls diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_models.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_models.py index 0edfdea1213c0..7606a76bc7bf8 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_models.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_models.py @@ -3,20 +3,25 @@ # +from unittest.mock import Mock + import pytest +from pendulum.tz.timezone import Timezone from source_google_ads.models import CustomerModel -def test_time_zone(): - mock_account_info = [[{"customer.id": "8765"}]] +def test_time_zone(mocker): + mocker.patch("source_google_ads.models.local_timezone", Mock(return_value=Timezone("Europe/Riga"))) + + mock_account_info = [{"customer_client.id": "8765"}] customers = CustomerModel.from_accounts(mock_account_info) for customer in customers: - assert customer.time_zone == "local" + assert customer.time_zone.name == Timezone("Europe/Riga").name @pytest.mark.parametrize("is_manager_account", (True, False)) def test_manager_account(is_manager_account): - mock_account_info = [[{"customer.manager": is_manager_account, "customer.id": "8765"}]] + mock_account_info = [{"customer_client.manager": is_manager_account, "customer_client.id": "8765"}] customers = CustomerModel.from_accounts(mock_account_info) for customer in customers: assert customer.is_manager_account is is_manager_account diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_source.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_source.py index f39fa1e3f95df..6394817edd99b 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_source.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_source.py @@ -5,7 +5,7 @@ import re from collections import namedtuple -from unittest.mock import Mock +from unittest.mock import Mock, call import pendulum import pytest @@ -14,6 +14,7 @@ from pendulum import today from source_google_ads.custom_query_stream import IncrementalCustomQuery from source_google_ads.google_ads import GoogleAds +from source_google_ads.models import CustomerModel from source_google_ads.source import SourceGoogleAds from source_google_ads.streams import AdGroupAdLegacy, chunk_date_range from source_google_ads.utils import GAQL @@ -22,10 +23,10 @@ @pytest.fixture -def mock_account_info(mocker): +def mock_get_customers(mocker): mocker.patch( - "source_google_ads.source.SourceGoogleAds.get_account_info", - Mock(return_value=[[{"customer.manager": False, "customer.time_zone": "Europe/Berlin", "customer.id": "8765"}]]), + "source_google_ads.source.SourceGoogleAds.get_customers", + Mock(return_value=[CustomerModel(is_manager_account=False, time_zone="Europe/Berlin", id="8765")]), ) @@ -113,7 +114,7 @@ def test_chunk_date_range(): ] == slices -def test_streams_count(config, mock_account_info): +def test_streams_count(config, mock_get_customers): source = SourceGoogleAds() streams = source.streams(config) expected_streams_number = 30 @@ -121,7 +122,7 @@ def test_streams_count(config, mock_account_info): assert len(streams) == expected_streams_number -def test_read_missing_stream(config, mock_account_info): +def test_read_missing_stream(config, mock_get_customers): source = SourceGoogleAds() catalog = ConfiguredAirbyteCatalog( @@ -437,8 +438,84 @@ def test_stream_slices(config, customers): ) slices = list(stream.stream_slices()) assert slices == [ - {"start_date": "2020-12-18", "end_date": "2021-01-01", "customer_id": "123"}, - {"start_date": "2021-01-02", "end_date": "2021-01-16", "customer_id": "123"}, - {"start_date": "2021-01-17", "end_date": "2021-01-31", "customer_id": "123"}, - {"start_date": "2021-02-01", "end_date": "2021-02-10", "customer_id": "123"}, + {"start_date": "2020-12-18", "end_date": "2021-01-01", "customer_id": "123", "login_customer_id": None}, + {"start_date": "2021-01-02", "end_date": "2021-01-16", "customer_id": "123", "login_customer_id": None}, + {"start_date": "2021-01-17", "end_date": "2021-01-31", "customer_id": "123", "login_customer_id": None}, + {"start_date": "2021-02-01", "end_date": "2021-02-10", "customer_id": "123", "login_customer_id": None}, ] + + +def mock_send_request(query: str, customer_id: str, login_customer_id: str = "default"): + print(query, customer_id, login_customer_id) + if customer_id == "123": + if "WHERE customer_client.status in ('active')" in query: + return [ + [ + {"customer_client.id": "123", "customer_client.status": "active"}, + ] + ] + else: + return [ + [ + {"customer_client.id": "123", "customer_client.status": "active"}, + {"customer_client.id": "456", "customer_client.status": "disabled"}, + ] + ] + else: + return [ + [ + {"customer_client.id": "789", "customer_client.status": "active"}, + ] + ] + + +@pytest.mark.parametrize( + "customer_status_filter, expected_ids, send_request_calls", + [ + ( + [], + ["123", "456", "789"], + [ + call( + "SELECT customer_client.client_customer, customer_client.level, customer_client.id, customer_client.manager, customer_client.time_zone, customer_client.status FROM customer_client", + customer_id="123", + ), + call( + "SELECT customer_client.client_customer, customer_client.level, customer_client.id, customer_client.manager, customer_client.time_zone, customer_client.status FROM customer_client", + customer_id="789", + ), + ], + ), # Empty filter, expect all customers + ( + ["active"], + ["123", "789"], + [ + call( + "SELECT customer_client.client_customer, customer_client.level, customer_client.id, customer_client.manager, customer_client.time_zone, customer_client.status FROM customer_client WHERE customer_client.status in ('active')", + customer_id="123", + ), + call( + "SELECT customer_client.client_customer, customer_client.level, customer_client.id, customer_client.manager, customer_client.time_zone, customer_client.status FROM customer_client WHERE customer_client.status in ('active')", + customer_id="789", + ), + ], + ), # Non-empty filter, expect filtered customers + ], +) +def test_get_customers(mocker, customer_status_filter, expected_ids, send_request_calls): + mock_google_api = Mock() + + mock_google_api.get_accessible_accounts.return_value = ["123", "789"] + mock_google_api.send_request.side_effect = mock_send_request + mock_google_api.parse_single_result.side_effect = lambda schema, result: result + + mock_config = {"customer_status_filter": customer_status_filter, "customer_ids": ["123", "456", "789"]} + + source = SourceGoogleAds() + + customers = source.get_customers(mock_google_api, mock_config) + + mock_google_api.send_request.assert_has_calls(send_request_calls) + + assert len(customers) == len(expected_ids) + assert {customer.id for customer in customers} == set(expected_ids) diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_streams.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_streams.py index 67015ec041dfe..3323a6811a232 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_streams.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_streams.py @@ -51,7 +51,7 @@ class MockGoogleAds(GoogleAds): def parse_single_result(self, schema, result): return result - def send_request(self, query: str, customer_id: str): + def send_request(self, query: str, customer_id: str, login_customer_id: str = "none"): self.count += 1 if self.count == 1: return mock_response_1() @@ -67,7 +67,7 @@ def test_page_token_expired_retry_succeeds(config, customers): It shouldn't read records on 2021-01-01, 2021-01-02 """ customer_id = next(iter(customers)).id - stream_slice = {"customer_id": customer_id, "start_date": "2021-01-01", "end_date": "2021-01-15"} + stream_slice = {"customer_id": customer_id, "start_date": "2021-01-01", "end_date": "2021-01-15", "login_customer_id": customer_id} google_api = MockGoogleAds(credentials=config["credentials"]) incremental_stream_config = dict( @@ -84,7 +84,9 @@ def test_page_token_expired_retry_succeeds(config, customers): result = list(stream.read_records(sync_mode=SyncMode.incremental, cursor_field=["segments.date"], stream_slice=stream_slice)) assert len(result) == 9 assert stream.get_query.call_count == 2 - stream.get_query.assert_called_with({"customer_id": customer_id, "start_date": "2021-01-03", "end_date": "2021-01-15"}) + stream.get_query.assert_called_with( + {"customer_id": customer_id, "start_date": "2021-01-03", "end_date": "2021-01-15", "login_customer_id": customer_id} + ) def mock_response_fails_1(): @@ -110,7 +112,7 @@ def mock_response_fails_2(): class MockGoogleAdsFails(MockGoogleAds): - def send_request(self, query: str, customer_id: str): + def send_request(self, query: str, customer_id: str, login_customer_id: str = "none"): self.count += 1 if self.count == 1: return mock_response_fails_1() @@ -124,7 +126,7 @@ def test_page_token_expired_retry_fails(config, customers): because Google Ads API doesn't allow filter by datetime. """ customer_id = next(iter(customers)).id - stream_slice = {"customer_id": customer_id, "start_date": "2021-01-01", "end_date": "2021-01-15"} + stream_slice = {"customer_id": customer_id, "start_date": "2021-01-01", "end_date": "2021-01-15", "login_customer_id": customer_id} google_api = MockGoogleAdsFails(credentials=config["credentials"]) incremental_stream_config = dict( @@ -145,7 +147,9 @@ def test_page_token_expired_retry_fails(config, customers): "Please contact the Airbyte team with the link of your connection for assistance." ) - stream.get_query.assert_called_with({"customer_id": customer_id, "start_date": "2021-01-03", "end_date": "2021-01-15"}) + stream.get_query.assert_called_with( + {"customer_id": customer_id, "start_date": "2021-01-03", "end_date": "2021-01-15", "login_customer_id": customer_id} + ) assert stream.get_query.call_count == 2 @@ -161,7 +165,7 @@ def mock_response_fails_one_date(): class MockGoogleAdsFailsOneDate(MockGoogleAds): - def send_request(self, query: str, customer_id: str): + def send_request(self, query: str, customer_id: str, login_customer_id: str = "none"): return mock_response_fails_one_date() @@ -172,7 +176,7 @@ def test_page_token_expired_it_should_fail_date_range_1_day(config, customers): Minimum date range is 1 day. """ customer_id = next(iter(customers)).id - stream_slice = {"customer_id": customer_id, "start_date": "2021-01-03", "end_date": "2021-01-04"} + stream_slice = {"customer_id": customer_id, "start_date": "2021-01-03", "end_date": "2021-01-04", "login_customer_id": customer_id} google_api = MockGoogleAdsFailsOneDate(credentials=config["credentials"]) incremental_stream_config = dict( @@ -192,17 +196,21 @@ def test_page_token_expired_it_should_fail_date_range_1_day(config, customers): "Page token has expired during processing response. " "Please contact the Airbyte team with the link of your connection for assistance." ) - stream.get_query.assert_called_with({"customer_id": customer_id, "start_date": "2021-01-03", "end_date": "2021-01-04"}) + stream.get_query.assert_called_with( + {"customer_id": customer_id, "start_date": "2021-01-03", "end_date": "2021-01-04", "login_customer_id": customer_id} + ) assert stream.get_query.call_count == 1 @pytest.mark.parametrize("error_cls", (ResourceExhausted, TooManyRequests, InternalServerError, DataLoss)) def test_retry_transient_errors(mocker, config, customers, error_cls): + customer_id = next(iter(customers)).id + mocker.patch("time.sleep") credentials = config["credentials"] credentials.update(use_proto_plus=True) api = GoogleAds(credentials=credentials) - mocked_search = mocker.patch.object(api.ga_service, "search", side_effect=error_cls("Error message")) + mocked_search = mocker.patch.object(api.ga_services["default"], "search", side_effect=error_cls("Error message")) incremental_stream_config = dict( api=api, conversion_window_days=config["conversion_window_days"], @@ -211,8 +219,7 @@ def test_retry_transient_errors(mocker, config, customers, error_cls): customers=customers, ) stream = ClickView(**incremental_stream_config) - customer_id = next(iter(customers)).id - stream_slice = {"customer_id": customer_id, "start_date": "2021-01-03", "end_date": "2021-01-04"} + stream_slice = {"customer_id": customer_id, "start_date": "2021-01-03", "end_date": "2021-01-04", "login_customer_id": "default"} records = [] with pytest.raises(error_cls) as exception: records = list(stream.read_records(sync_mode=SyncMode.incremental, cursor_field=["segments.date"], stream_slice=stream_slice)) @@ -275,7 +282,8 @@ def test_read_records_unauthenticated(mocker, customers, config): ) stream = CustomerLabel(**stream_config) with pytest.raises(AirbyteTracedException) as exc_info: - list(stream.read_records(SyncMode.full_refresh, {"customer_id": "customer_id"})) + list(stream.read_records(SyncMode.full_refresh, {"customer_id": "customer_id", "login_customer_id": "default"})) - assert exc_info.value.message == ("Authentication failed for the customer 'customer_id'. " - "Please try to Re-authenticate your credentials on set up Google Ads page.") + assert exc_info.value.message == ( + "Authentication failed for the customer 'customer_id'. " "Please try to Re-authenticate your credentials on set up Google Ads page." + ) diff --git a/docs/integrations/sources/google-ads.md b/docs/integrations/sources/google-ads.md index a56f046a29fd5..4f1b239d9ff17 100644 --- a/docs/integrations/sources/google-ads.md +++ b/docs/integrations/sources/google-ads.md @@ -62,13 +62,14 @@ To set up Google Ads as a source in Airbyte Cloud: 3. Find and select **Google Ads** from the list of available sources. 4. Enter a **Source name** of your choosing. 5. Click **Sign in with Google** to authenticate your Google Ads account. In the pop-up, select the appropriate Google account and click **Continue** to proceed. -6. Enter a comma-separated list of the **Customer ID(s)** for your account. These IDs are 10-digit numbers that uniquely identify your account. To find your Customer ID, please follow [Google's instructions](https://support.google.com/google-ads/answer/1704344). -7. (Optional) Enter a **Start Date** using the provided datepicker, or by programmatically entering the date in YYYY-MM-DD format. The data added on and after this date will be replicated. (Default start date is 2 years ago) -8. (Optional) You can use the **Custom GAQL Queries** field to enter a custom query using Google Ads Query Language. Click **Add** and enter your query, as well as the desired name of the table for this data in the destination. Multiple queries can be provided. For more information on formulating these queries, refer to our [guide below](#custom-query-understanding-google-ads-query-language). -9. (Required for Manager accounts) If accessing your account through a Google Ads Manager account, you must enter the [**Customer ID**](https://developers.google.com/google-ads/api/docs/concepts/call-structure#cid) of the Manager account. -10. (Optional) Enter a **Conversion Window**. This is the number of days after an ad interaction during which a conversion is recorded in Google Ads. For more information on this topic, refer to the [Google Ads Help Center](https://support.google.com/google-ads/answer/3123169?hl=en). This field defaults to 14 days. -11. (Optional) Enter an **End Date** in YYYY-MM-DD format. Any data added after this date will not be replicated. Leaving this field blank will replicate all data from the start date onward. -12. Click **Set up source** and wait for the tests to complete. +6. (Optional) Enter a comma-separated list of the **Customer ID(s)** for your account. These IDs are 10-digit numbers that uniquely identify your account. To find your Customer ID, please follow [Google's instructions](https://support.google.com/google-ads/answer/1704344). Leaving this field blank will replicate data from all connected accounts. +7. (Optional) Enter customer statuses to filter customers. Leaving this field blank will replicate data from all accounts. Check [Google Ads documentation](https://developers.google.com/google-ads/api/reference/rpc/v15/CustomerStatusEnum.CustomerStatus) for more info. +8. (Optional) Enter a **Start Date** using the provided datepicker, or by programmatically entering the date in YYYY-MM-DD format. The data added on and after this date will be replicated. (Default start date is 2 years ago) +9. (Optional) You can use the **Custom GAQL Queries** field to enter a custom query using Google Ads Query Language. Click **Add** and enter your query, as well as the desired name of the table for this data in the destination. Multiple queries can be provided. For more information on formulating these queries, refer to our [guide below](#custom-query-understanding-google-ads-query-language). +10. (Required for Manager accounts) If accessing your account through a Google Ads Manager account, you must enter the [**Customer ID**](https://developers.google.com/google-ads/api/docs/concepts/call-structure#cid) of the Manager account. +11. (Optional) Enter a **Conversion Window**. This is the number of days after an ad interaction during which a conversion is recorded in Google Ads. For more information on this topic, refer to the [Google Ads Help Center](https://support.google.com/google-ads/answer/3123169?hl=en). This field defaults to 14 days. +12. (Optional) Enter an **End Date** in YYYY-MM-DD format. Any data added after this date will not be replicated. Leaving this field blank will replicate all data from the start date onward. +13. Click **Set up source** and wait for the tests to complete. @@ -83,13 +84,14 @@ To set up Google Ads as a source in Airbyte Open Source: 4. Enter a **Source name** of your choosing. 5. Enter the **Developer Token** you obtained from Google. 6. To authenticate your Google account, enter your Google application's **Client ID**, **Client Secret**, **Refresh Token**, and optionally, the **Access Token**. -7. Enter a comma-separated list of the **Customer ID(s)** for your account. These IDs are 10-digit numbers that uniquely identify your account. To find your Customer ID, please follow [Google's instructions](https://support.google.com/google-ads/answer/1704344). -8. (Optional) Enter a **Start Date** using the provided datepicker, or by programmatically entering the date in YYYY-MM-DD format. The data added on and after this date will be replicated. (Default start date is 2 years ago) -9. (Optional) You can use the **Custom GAQL Queries** field to enter a custom query using Google Ads Query Language. Click **Add** and enter your query, as well as the desired name of the table for this data in the destination. Multiple queries can be provided. For more information on formulating these queries, refer to our [guide below](#custom-query-understanding-google-ads-query-language). -10. (Required for Manager accounts) If accessing your account through a Google Ads Manager account, you must enter the [**Customer ID**](https://developers.google.com/google-ads/api/docs/concepts/call-structure#cid) of the Manager account. -11. (Optional) Enter a **Conversion Window**. This is the number of days after an ad interaction during which a conversion is recorded in Google Ads. For more information on this topic, see the section on [Conversion Windows](#note-on-conversion-windows) below, or refer to the [Google Ads Help Center](https://support.google.com/google-ads/answer/3123169?hl=en). This field defaults to 14 days. -12. (Optional) Enter an **End Date** in YYYY-MM-DD format. Any data added after this date will not be replicated. Leaving this field blank will replicate all data from the start date onward. -13. Click **Set up source** and wait for the tests to complete. +7. (Optional) Enter a comma-separated list of the **Customer ID(s)** for your account. These IDs are 10-digit numbers that uniquely identify your account. To find your Customer ID, please follow [Google's instructions](https://support.google.com/google-ads/answer/1704344). Leaving this field blank will replicate data from all connected accounts. +8. (Optional) Enter customer statuses to filter customers. Leaving this field blank will replicate data from all accounts. Check [Google Ads documentation](https://developers.google.com/google-ads/api/reference/rpc/v15/CustomerStatusEnum.CustomerStatus) for more info. +9. (Optional) Enter a **Start Date** using the provided datepicker, or by programmatically entering the date in YYYY-MM-DD format. The data added on and after this date will be replicated. (Default start date is 2 years ago) +10. (Optional) You can use the **Custom GAQL Queries** field to enter a custom query using Google Ads Query Language. Click **Add** and enter your query, as well as the desired name of the table for this data in the destination. Multiple queries can be provided. For more information on formulating these queries, refer to our [guide below](#custom-query-understanding-google-ads-query-language). +11. (Required for Manager accounts) If accessing your account through a Google Ads Manager account, you must enter the [**Customer ID**](https://developers.google.com/google-ads/api/docs/concepts/call-structure#cid) of the Manager account. +12. (Optional) Enter a **Conversion Window**. This is the number of days after an ad interaction during which a conversion is recorded in Google Ads. For more information on this topic, see the section on [Conversion Windows](#note-on-conversion-windows) below, or refer to the [Google Ads Help Center](https://support.google.com/google-ads/answer/3123169?hl=en). This field defaults to 14 days. +13. (Optional) Enter an **End Date** in YYYY-MM-DD format. Any data added after this date will not be replicated. Leaving this field blank will replicate all data from the start date onward. +14. Click **Set up source** and wait for the tests to complete. @@ -276,100 +278,101 @@ Due to a limitation in the Google Ads API which does not allow getting performan ## Changelog -| Version | Date | Pull Request | Subject | -|:---------|:-----------|:---------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------| -| `3.1.0` | 2024-01-09 | [33603](https://github.com/airbytehq/airbyte/pull/33603) | Fix two issues in the custom queries: automatic addition of `segments.date` in the query; incorrect field type for `DATE` fields. | -| `3.0.2` | 2024-01-08 | [33494](https://github.com/airbytehq/airbyte/pull/33494) | Add handling for 401 error while parsing response. Add `metrics.cost_micros` field to Ad Group stream. | -| `3.0.1` | 2023-12-26 | [33769](https://github.com/airbytehq/airbyte/pull/33769) | Run a read function in a separate thread to enforce a time limit for its execution | -| `3.0.0` | 2023-12-07 | [33120](https://github.com/airbytehq/airbyte/pull/33120) | Upgrade API version to v15 | -| `2.0.4` | 2023-11-10 | [32414](https://github.com/airbytehq/airbyte/pull/32414) | Add backoff strategy for read_records method | -| `2.0.3` | 2023-11-02 | [32102](https://github.com/airbytehq/airbyte/pull/32102) | Fix incremental events streams | -| `2.0.2` | 2023-10-31 | [32001](https://github.com/airbytehq/airbyte/pull/32001) | Added handling (retry) for `InternalServerError` while reading the streams | -| `2.0.1` | 2023-10-27 | [31908](https://github.com/airbytehq/airbyte/pull/31908) | Base image migration: remove Dockerfile and use the python-connector-base image | -| `2.0.0` | 2023-10-04 | [31048](https://github.com/airbytehq/airbyte/pull/31048) | Fix schem default streams, change names of streams. | -| `1.0.0` | 2023-09-28 | [30705](https://github.com/airbytehq/airbyte/pull/30705) | Fix schemas for custom queries | -| `0.11.1` | 2023-09-26 | [30758](https://github.com/airbytehq/airbyte/pull/30758) | Exception should not be raises if a stream is not found | -| `0.11.0` | 2023-09-23 | [30704](https://github.com/airbytehq/airbyte/pull/30704) | Update error handling | -| `0.10.0` | 2023-09-19 | [30091](https://github.com/airbytehq/airbyte/pull/30091) | Fix schemas for correct primary and foreign keys | -| `0.9.0` | 2023-09-14 | [28970](https://github.com/airbytehq/airbyte/pull/28970) | Add incremental deletes for Campaign and Ad Group Criterion streams | -| `0.8.1` | 2023-09-13 | [30376](https://github.com/airbytehq/airbyte/pull/30376) | Revert pagination changes from 0.8.0 | -| `0.8.0` | 2023-09-01 | [30071](https://github.com/airbytehq/airbyte/pull/30071) | Delete start_date from required parameters and fix pagination | -| `0.7.4` | 2023-07-28 | [28832](https://github.com/airbytehq/airbyte/pull/28832) | Update field descriptions | -| `0.7.3` | 2023-07-24 | [28510](https://github.com/airbytehq/airbyte/pull/28510) | Set dates with client's timezone | -| `0.7.2` | 2023-07-20 | [28535](https://github.com/airbytehq/airbyte/pull/28535) | UI improvement: Make the query field in custom reports a multi-line string field | -| `0.7.1` | 2023-07-17 | [28365](https://github.com/airbytehq/airbyte/pull/28365) | 0.3.1 and 0.3.2 follow up: make today the end date, not yesterday | -| `0.7.0` | 2023-07-12 | [28246](https://github.com/airbytehq/airbyte/pull/28246) | Add new streams: labels, criterions, biddig strategies | -| `0.6.1` | 2023-07-12 | [28230](https://github.com/airbytehq/airbyte/pull/28230) | Reduce amount of logs produced by the connector while working with big amount of data | -| `0.6.0` | 2023-07-10 | [28078](https://github.com/airbytehq/airbyte/pull/28078) | Add new stream `Campaign Budget` | -| `0.5.0` | 2023-07-07 | [28042](https://github.com/airbytehq/airbyte/pull/28042) | Add metrics & segment to `Campaigns` stream | -| `0.4.3` | 2023-07-05 | [27959](https://github.com/airbytehq/airbyte/pull/27959) | Add `audience` and `user_interest` streams | -| `0.3.3` | 2023-07-03 | [27913](https://github.com/airbytehq/airbyte/pull/27913) | Improve Google Ads exception handling (wrong customer ID) | -| `0.3.2` | 2023-06-29 | [27835](https://github.com/airbytehq/airbyte/pull/27835) | Fix bug introduced in 0.3.1: update query template | -| `0.3.1` | 2023-06-26 | [27711](https://github.com/airbytehq/airbyte/pull/27711) | Refactor date slicing; make start date inclusive | -| `0.3.0` | 2023-06-26 | [27738](https://github.com/airbytehq/airbyte/pull/27738) | License Update: Elv2 | -| `0.2.24` | 2023-06-06 | [27608](https://github.com/airbytehq/airbyte/pull/27608) | Improve Google Ads exception handling | -| `0.2.23` | 2023-06-06 | [26905](https://github.com/airbytehq/airbyte/pull/26905) | Replace deprecated `authSpecification` in the connector specification with `advancedAuth` | -| `0.2.22` | 2023-06-02 | [26948](https://github.com/airbytehq/airbyte/pull/26948) | Refactor error messages; add `pattern_descriptor` for fields in spec | -| `0.2.21` | 2023-05-30 | [25314](https://github.com/airbytehq/airbyte/pull/25314) | Add full refresh custom table `asset_group_listing_group_filter` | -| `0.2.20` | 2023-05-30 | [25624](https://github.com/airbytehq/airbyte/pull/25624) | Add `asset` Resource to full refresh custom tables (GAQL Queries) | -| `0.2.19` | 2023-05-15 | [26209](https://github.com/airbytehq/airbyte/pull/26209) | Handle Token Refresh errors as `config_error` | -| `0.2.18` | 2023-05-15 | [25947](https://github.com/airbytehq/airbyte/pull/25947) | Improve GAQL parser error message if multiple resources provided | -| `0.2.17` | 2023-05-11 | [25987](https://github.com/airbytehq/airbyte/pull/25987) | Categorized Config Errors Accurately | -| `0.2.16` | 2023-05-10 | [25965](https://github.com/airbytehq/airbyte/pull/25965) | Fix Airbyte date-time data-types | -| `0.2.14` | 2023-03-21 | [24945](https://github.com/airbytehq/airbyte/pull/24945) | For custom google query fixed schema type for "data_type: ENUM" and "is_repeated: true" to array of strings | -| `0.2.13` | 2023-03-21 | [24338](https://github.com/airbytehq/airbyte/pull/24338) | Migrate to v13 | -| `0.2.12` | 2023-03-17 | [22985](https://github.com/airbytehq/airbyte/pull/22985) | Specified date formatting in specification | -| `0.2.11` | 2023-03-13 | [23999](https://github.com/airbytehq/airbyte/pull/23999) | Fix incremental sync for Campaigns stream | -| `0.2.10` | 2023-02-11 | [22703](https://github.com/airbytehq/airbyte/pull/22703) | Add support for custom full_refresh streams | -| `0.2.9` | 2023-01-23 | [21705](https://github.com/airbytehq/airbyte/pull/21705) | Fix multibyte issue; Bump google-ads package to 19.0.0 | -| `0.2.8` | 2023-01-18 | [21517](https://github.com/airbytehq/airbyte/pull/21517) | Write fewer logs | -| `0.2.7` | 2023-01-10 | [20755](https://github.com/airbytehq/airbyte/pull/20755) | Add more logs to debug stuck syncs | -| `0.2.6` | 2022-12-22 | [20855](https://github.com/airbytehq/airbyte/pull/20855) | Retry 429 and 5xx errors | -| `0.2.5` | 2022-11-22 | [19700](https://github.com/airbytehq/airbyte/pull/19700) | Fix schema for `campaigns` stream | -| `0.2.4` | 2022-11-09 | [19208](https://github.com/airbytehq/airbyte/pull/19208) | Add TypeTransofrmer to Campaings stream to force proper type casting | -| `0.2.3` | 2022-10-17 | [18069](https://github.com/airbytehq/airbyte/pull/18069) | Add `segments.hour`, `metrics.ctr`, `metrics.conversions` and `metrics.conversions_values` fields to `campaigns` report stream | -| `0.2.2` | 2022-10-21 | [17412](https://github.com/airbytehq/airbyte/pull/17412) | Release with CDK >= 0.2.2 | -| `0.2.1` | 2022-09-29 | [17412](https://github.com/airbytehq/airbyte/pull/17412) | Always use latest CDK version | -| `0.2.0` | 2022-08-23 | [15858](https://github.com/airbytehq/airbyte/pull/15858) | Mark the `query` and `table_name` fields in `custom_queries` as required | -| `0.1.44` | 2022-07-27 | [15084](https://github.com/airbytehq/airbyte/pull/15084) | Fix data type `ad_group_criterion.topic.path` in `display_topics_performance_report` and shifted `campaigns` to non-managers streams | -| `0.1.43` | 2022-07-12 | [14614](https://github.com/airbytehq/airbyte/pull/14614) | Update API version to `v11`, update `google-ads` to 17.0.0 | -| `0.1.42` | 2022-06-08 | [13624](https://github.com/airbytehq/airbyte/pull/13624) | Update `google-ads` to 15.1.1, pin `protobuf==3.20.0` to work on MacOS M1 machines (AMD) | -| `0.1.41` | 2022-06-08 | [13618](https://github.com/airbytehq/airbyte/pull/13618) | Add missing dependency | -| `0.1.40` | 2022-06-02 | [13423](https://github.com/airbytehq/airbyte/pull/13423) | Fix the missing data [issue](https://github.com/airbytehq/airbyte/issues/12999) | -| `0.1.39` | 2022-05-18 | [12914](https://github.com/airbytehq/airbyte/pull/12914) | Fix GAQL query validation and log auth errors instead of failing the sync | -| `0.1.38` | 2022-05-12 | [12807](https://github.com/airbytehq/airbyte/pull/12807) | Documentation updates | -| `0.1.37` | 2022-05-06 | [12651](https://github.com/airbytehq/airbyte/pull/12651) | Improve integration and unit tests | -| `0.1.36` | 2022-04-19 | [12158](https://github.com/airbytehq/airbyte/pull/12158) | Fix `*_labels` streams data type | -| `0.1.35` | 2022-04-18 | [9310](https://github.com/airbytehq/airbyte/pull/9310) | Add new fields to reports | -| `0.1.34` | 2022-03-29 | [11602](https://github.com/airbytehq/airbyte/pull/11602) | Add budget amount to campaigns stream. | -| `0.1.33` | 2022-03-29 | [11513](https://github.com/airbytehq/airbyte/pull/11513) | When `end_date` is configured in the future, use today's date instead. | -| `0.1.32` | 2022-03-24 | [11371](https://github.com/airbytehq/airbyte/pull/11371) | Improve how connection check returns error messages | -| `0.1.31` | 2022-03-23 | [11301](https://github.com/airbytehq/airbyte/pull/11301) | Update docs and spec to clarify usage | -| `0.1.30` | 2022-03-23 | [11221](https://github.com/airbytehq/airbyte/pull/11221) | Add `*_labels` streams to fetch the label text rather than their IDs | -| `0.1.29` | 2022-03-22 | [10919](https://github.com/airbytehq/airbyte/pull/10919) | Fix user location report schema and add to acceptance tests | -| `0.1.28` | 2022-02-25 | [10372](https://github.com/airbytehq/airbyte/pull/10372) | Add network fields to click view stream | -| `0.1.27` | 2022-02-16 | [10315](https://github.com/airbytehq/airbyte/pull/10315) | Make `ad_group_ads` and other streams support incremental sync. | -| `0.1.26` | 2022-02-11 | [10150](https://github.com/airbytehq/airbyte/pull/10150) | Add support for multiple customer IDs. | -| `0.1.25` | 2022-02-04 | [9812](https://github.com/airbytehq/airbyte/pull/9812) | Handle `EXPIRED_PAGE_TOKEN` exception and retry with updated state. | -| `0.1.24` | 2022-02-04 | [9996](https://github.com/airbytehq/airbyte/pull/9996) | Use Google Ads API version V9. | -| `0.1.23` | 2022-01-25 | [8669](https://github.com/airbytehq/airbyte/pull/8669) | Add end date parameter in spec. | -| `0.1.22` | 2022-01-24 | [9608](https://github.com/airbytehq/airbyte/pull/9608) | Reduce stream slice date range. | -| `0.1.21` | 2021-12-28 | [9149](https://github.com/airbytehq/airbyte/pull/9149) | Update title and description | -| `0.1.20` | 2021-12-22 | [9071](https://github.com/airbytehq/airbyte/pull/9071) | Fix: Keyword schema enum | -| `0.1.19` | 2021-12-14 | [8431](https://github.com/airbytehq/airbyte/pull/8431) | Add new streams: Geographic and Keyword | -| `0.1.18` | 2021-12-09 | [8225](https://github.com/airbytehq/airbyte/pull/8225) | Include time_zone to sync. Remove streams for manager account. | -| `0.1.16` | 2021-11-22 | [8178](https://github.com/airbytehq/airbyte/pull/8178) | Clarify setup fields | -| `0.1.15` | 2021-10-07 | [6684](https://github.com/airbytehq/airbyte/pull/6684) | Add new stream `click_view` | -| `0.1.14` | 2021-10-01 | [6565](https://github.com/airbytehq/airbyte/pull/6565) | Fix OAuth Spec File | -| `0.1.13` | 2021-09-27 | [6458](https://github.com/airbytehq/airbyte/pull/6458) | Update OAuth Spec File | -| `0.1.11` | 2021-09-22 | [6373](https://github.com/airbytehq/airbyte/pull/6373) | Fix inconsistent segments.date field type across all streams | -| `0.1.10` | 2021-09-13 | [6022](https://github.com/airbytehq/airbyte/pull/6022) | Annotate Oauth2 flow initialization parameters in connector spec | -| `0.1.9` | 2021-09-07 | [5302](https://github.com/airbytehq/airbyte/pull/5302) | Add custom query stream support | -| `0.1.8` | 2021-08-03 | [5509](https://github.com/airbytehq/airbyte/pull/5509) | Allow additionalProperties in spec.json | -| `0.1.7` | 2021-08-03 | [5422](https://github.com/airbytehq/airbyte/pull/5422) | Correct query to not skip dates | -| `0.1.6` | 2021-08-03 | [5423](https://github.com/airbytehq/airbyte/pull/5423) | Added new stream UserLocationReport | -| `0.1.5` | 2021-08-03 | [5159](https://github.com/airbytehq/airbyte/pull/5159) | Add field `login_customer_id` to spec | -| `0.1.4` | 2021-07-28 | [4962](https://github.com/airbytehq/airbyte/pull/4962) | Support new Report streams | -| `0.1.3` | 2021-07-23 | [4788](https://github.com/airbytehq/airbyte/pull/4788) | Support main streams, fix bug with exception `DATE_RANGE_TOO_NARROW` for incremental streams | -| `0.1.2` | 2021-07-06 | [4539](https://github.com/airbytehq/airbyte/pull/4539) | Add `AIRBYTE_ENTRYPOINT` for Kubernetes support | -| `0.1.1` | 2021-06-23 | [4288](https://github.com/airbytehq/airbyte/pull/4288) | Fix `Bugfix: Correctly declare required parameters` | +| Version | Date | Pull Request | Subject | +|:---------|:-----------|:---------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------| +| `3.2.0` | 2024-01-09 | [33707](https://github.com/airbytehq/airbyte/pull/33707) | Add possibility to sync all connected accounts | +| `3.1.0` | 2024-01-09 | [33603](https://github.com/airbytehq/airbyte/pull/33603) | Fix two issues in the custom queries: automatic addition of `segments.date` in the query; incorrect field type for `DATE` fields. | +| `3.0.2` | 2024-01-08 | [33494](https://github.com/airbytehq/airbyte/pull/33494) | Add handling for 401 error while parsing response. Add `metrics.cost_micros` field to Ad Group stream. | +| `3.0.1` | 2023-12-26 | [33769](https://github.com/airbytehq/airbyte/pull/33769) | Run a read function in a separate thread to enforce a time limit for its execution | +| `3.0.0` | 2023-12-07 | [33120](https://github.com/airbytehq/airbyte/pull/33120) | Upgrade API version to v15 | +| `2.0.4` | 2023-11-10 | [32414](https://github.com/airbytehq/airbyte/pull/32414) | Add backoff strategy for read_records method | +| `2.0.3` | 2023-11-02 | [32102](https://github.com/airbytehq/airbyte/pull/32102) | Fix incremental events streams | +| `2.0.2` | 2023-10-31 | [32001](https://github.com/airbytehq/airbyte/pull/32001) | Added handling (retry) for `InternalServerError` while reading the streams | +| `2.0.1` | 2023-10-27 | [31908](https://github.com/airbytehq/airbyte/pull/31908) | Base image migration: remove Dockerfile and use the python-connector-base image | +| `2.0.0` | 2023-10-04 | [31048](https://github.com/airbytehq/airbyte/pull/31048) | Fix schem default streams, change names of streams. | +| `1.0.0` | 2023-09-28 | [30705](https://github.com/airbytehq/airbyte/pull/30705) | Fix schemas for custom queries | +| `0.11.1` | 2023-09-26 | [30758](https://github.com/airbytehq/airbyte/pull/30758) | Exception should not be raises if a stream is not found | +| `0.11.0` | 2023-09-23 | [30704](https://github.com/airbytehq/airbyte/pull/30704) | Update error handling | +| `0.10.0` | 2023-09-19 | [30091](https://github.com/airbytehq/airbyte/pull/30091) | Fix schemas for correct primary and foreign keys | +| `0.9.0` | 2023-09-14 | [28970](https://github.com/airbytehq/airbyte/pull/28970) | Add incremental deletes for Campaign and Ad Group Criterion streams | +| `0.8.1` | 2023-09-13 | [30376](https://github.com/airbytehq/airbyte/pull/30376) | Revert pagination changes from 0.8.0 | +| `0.8.0` | 2023-09-01 | [30071](https://github.com/airbytehq/airbyte/pull/30071) | Delete start_date from required parameters and fix pagination | +| `0.7.4` | 2023-07-28 | [28832](https://github.com/airbytehq/airbyte/pull/28832) | Update field descriptions | +| `0.7.3` | 2023-07-24 | [28510](https://github.com/airbytehq/airbyte/pull/28510) | Set dates with client's timezone | +| `0.7.2` | 2023-07-20 | [28535](https://github.com/airbytehq/airbyte/pull/28535) | UI improvement: Make the query field in custom reports a multi-line string field | +| `0.7.1` | 2023-07-17 | [28365](https://github.com/airbytehq/airbyte/pull/28365) | 0.3.1 and 0.3.2 follow up: make today the end date, not yesterday | +| `0.7.0` | 2023-07-12 | [28246](https://github.com/airbytehq/airbyte/pull/28246) | Add new streams: labels, criterions, biddig strategies | +| `0.6.1` | 2023-07-12 | [28230](https://github.com/airbytehq/airbyte/pull/28230) | Reduce amount of logs produced by the connector while working with big amount of data | +| `0.6.0` | 2023-07-10 | [28078](https://github.com/airbytehq/airbyte/pull/28078) | Add new stream `Campaign Budget` | +| `0.5.0` | 2023-07-07 | [28042](https://github.com/airbytehq/airbyte/pull/28042) | Add metrics & segment to `Campaigns` stream | +| `0.4.3` | 2023-07-05 | [27959](https://github.com/airbytehq/airbyte/pull/27959) | Add `audience` and `user_interest` streams | +| `0.3.3` | 2023-07-03 | [27913](https://github.com/airbytehq/airbyte/pull/27913) | Improve Google Ads exception handling (wrong customer ID) | +| `0.3.2` | 2023-06-29 | [27835](https://github.com/airbytehq/airbyte/pull/27835) | Fix bug introduced in 0.3.1: update query template | +| `0.3.1` | 2023-06-26 | [27711](https://github.com/airbytehq/airbyte/pull/27711) | Refactor date slicing; make start date inclusive | +| `0.3.0` | 2023-06-26 | [27738](https://github.com/airbytehq/airbyte/pull/27738) | License Update: Elv2 | +| `0.2.24` | 2023-06-06 | [27608](https://github.com/airbytehq/airbyte/pull/27608) | Improve Google Ads exception handling | +| `0.2.23` | 2023-06-06 | [26905](https://github.com/airbytehq/airbyte/pull/26905) | Replace deprecated `authSpecification` in the connector specification with `advancedAuth` | +| `0.2.22` | 2023-06-02 | [26948](https://github.com/airbytehq/airbyte/pull/26948) | Refactor error messages; add `pattern_descriptor` for fields in spec | +| `0.2.21` | 2023-05-30 | [25314](https://github.com/airbytehq/airbyte/pull/25314) | Add full refresh custom table `asset_group_listing_group_filter` | +| `0.2.20` | 2023-05-30 | [25624](https://github.com/airbytehq/airbyte/pull/25624) | Add `asset` Resource to full refresh custom tables (GAQL Queries) | +| `0.2.19` | 2023-05-15 | [26209](https://github.com/airbytehq/airbyte/pull/26209) | Handle Token Refresh errors as `config_error` | +| `0.2.18` | 2023-05-15 | [25947](https://github.com/airbytehq/airbyte/pull/25947) | Improve GAQL parser error message if multiple resources provided | +| `0.2.17` | 2023-05-11 | [25987](https://github.com/airbytehq/airbyte/pull/25987) | Categorized Config Errors Accurately | +| `0.2.16` | 2023-05-10 | [25965](https://github.com/airbytehq/airbyte/pull/25965) | Fix Airbyte date-time data-types | +| `0.2.14` | 2023-03-21 | [24945](https://github.com/airbytehq/airbyte/pull/24945) | For custom google query fixed schema type for "data_type: ENUM" and "is_repeated: true" to array of strings | +| `0.2.13` | 2023-03-21 | [24338](https://github.com/airbytehq/airbyte/pull/24338) | Migrate to v13 | +| `0.2.12` | 2023-03-17 | [22985](https://github.com/airbytehq/airbyte/pull/22985) | Specified date formatting in specification | +| `0.2.11` | 2023-03-13 | [23999](https://github.com/airbytehq/airbyte/pull/23999) | Fix incremental sync for Campaigns stream | +| `0.2.10` | 2023-02-11 | [22703](https://github.com/airbytehq/airbyte/pull/22703) | Add support for custom full_refresh streams | +| `0.2.9` | 2023-01-23 | [21705](https://github.com/airbytehq/airbyte/pull/21705) | Fix multibyte issue; Bump google-ads package to 19.0.0 | +| `0.2.8` | 2023-01-18 | [21517](https://github.com/airbytehq/airbyte/pull/21517) | Write fewer logs | +| `0.2.7` | 2023-01-10 | [20755](https://github.com/airbytehq/airbyte/pull/20755) | Add more logs to debug stuck syncs | +| `0.2.6` | 2022-12-22 | [20855](https://github.com/airbytehq/airbyte/pull/20855) | Retry 429 and 5xx errors | +| `0.2.5` | 2022-11-22 | [19700](https://github.com/airbytehq/airbyte/pull/19700) | Fix schema for `campaigns` stream | +| `0.2.4` | 2022-11-09 | [19208](https://github.com/airbytehq/airbyte/pull/19208) | Add TypeTransofrmer to Campaings stream to force proper type casting | +| `0.2.3` | 2022-10-17 | [18069](https://github.com/airbytehq/airbyte/pull/18069) | Add `segments.hour`, `metrics.ctr`, `metrics.conversions` and `metrics.conversions_values` fields to `campaigns` report stream | +| `0.2.2` | 2022-10-21 | [17412](https://github.com/airbytehq/airbyte/pull/17412) | Release with CDK >= 0.2.2 | +| `0.2.1` | 2022-09-29 | [17412](https://github.com/airbytehq/airbyte/pull/17412) | Always use latest CDK version | +| `0.2.0` | 2022-08-23 | [15858](https://github.com/airbytehq/airbyte/pull/15858) | Mark the `query` and `table_name` fields in `custom_queries` as required | +| `0.1.44` | 2022-07-27 | [15084](https://github.com/airbytehq/airbyte/pull/15084) | Fix data type `ad_group_criterion.topic.path` in `display_topics_performance_report` and shifted `campaigns` to non-managers streams | +| `0.1.43` | 2022-07-12 | [14614](https://github.com/airbytehq/airbyte/pull/14614) | Update API version to `v11`, update `google-ads` to 17.0.0 | +| `0.1.42` | 2022-06-08 | [13624](https://github.com/airbytehq/airbyte/pull/13624) | Update `google-ads` to 15.1.1, pin `protobuf==3.20.0` to work on MacOS M1 machines (AMD) | +| `0.1.41` | 2022-06-08 | [13618](https://github.com/airbytehq/airbyte/pull/13618) | Add missing dependency | +| `0.1.40` | 2022-06-02 | [13423](https://github.com/airbytehq/airbyte/pull/13423) | Fix the missing data [issue](https://github.com/airbytehq/airbyte/issues/12999) | +| `0.1.39` | 2022-05-18 | [12914](https://github.com/airbytehq/airbyte/pull/12914) | Fix GAQL query validation and log auth errors instead of failing the sync | +| `0.1.38` | 2022-05-12 | [12807](https://github.com/airbytehq/airbyte/pull/12807) | Documentation updates | +| `0.1.37` | 2022-05-06 | [12651](https://github.com/airbytehq/airbyte/pull/12651) | Improve integration and unit tests | +| `0.1.36` | 2022-04-19 | [12158](https://github.com/airbytehq/airbyte/pull/12158) | Fix `*_labels` streams data type | +| `0.1.35` | 2022-04-18 | [9310](https://github.com/airbytehq/airbyte/pull/9310) | Add new fields to reports | +| `0.1.34` | 2022-03-29 | [11602](https://github.com/airbytehq/airbyte/pull/11602) | Add budget amount to campaigns stream. | +| `0.1.33` | 2022-03-29 | [11513](https://github.com/airbytehq/airbyte/pull/11513) | When `end_date` is configured in the future, use today's date instead. | +| `0.1.32` | 2022-03-24 | [11371](https://github.com/airbytehq/airbyte/pull/11371) | Improve how connection check returns error messages | +| `0.1.31` | 2022-03-23 | [11301](https://github.com/airbytehq/airbyte/pull/11301) | Update docs and spec to clarify usage | +| `0.1.30` | 2022-03-23 | [11221](https://github.com/airbytehq/airbyte/pull/11221) | Add `*_labels` streams to fetch the label text rather than their IDs | +| `0.1.29` | 2022-03-22 | [10919](https://github.com/airbytehq/airbyte/pull/10919) | Fix user location report schema and add to acceptance tests | +| `0.1.28` | 2022-02-25 | [10372](https://github.com/airbytehq/airbyte/pull/10372) | Add network fields to click view stream | +| `0.1.27` | 2022-02-16 | [10315](https://github.com/airbytehq/airbyte/pull/10315) | Make `ad_group_ads` and other streams support incremental sync. | +| `0.1.26` | 2022-02-11 | [10150](https://github.com/airbytehq/airbyte/pull/10150) | Add support for multiple customer IDs. | +| `0.1.25` | 2022-02-04 | [9812](https://github.com/airbytehq/airbyte/pull/9812) | Handle `EXPIRED_PAGE_TOKEN` exception and retry with updated state. | +| `0.1.24` | 2022-02-04 | [9996](https://github.com/airbytehq/airbyte/pull/9996) | Use Google Ads API version V9. | +| `0.1.23` | 2022-01-25 | [8669](https://github.com/airbytehq/airbyte/pull/8669) | Add end date parameter in spec. | +| `0.1.22` | 2022-01-24 | [9608](https://github.com/airbytehq/airbyte/pull/9608) | Reduce stream slice date range. | +| `0.1.21` | 2021-12-28 | [9149](https://github.com/airbytehq/airbyte/pull/9149) | Update title and description | +| `0.1.20` | 2021-12-22 | [9071](https://github.com/airbytehq/airbyte/pull/9071) | Fix: Keyword schema enum | +| `0.1.19` | 2021-12-14 | [8431](https://github.com/airbytehq/airbyte/pull/8431) | Add new streams: Geographic and Keyword | +| `0.1.18` | 2021-12-09 | [8225](https://github.com/airbytehq/airbyte/pull/8225) | Include time_zone to sync. Remove streams for manager account. | +| `0.1.16` | 2021-11-22 | [8178](https://github.com/airbytehq/airbyte/pull/8178) | Clarify setup fields | +| `0.1.15` | 2021-10-07 | [6684](https://github.com/airbytehq/airbyte/pull/6684) | Add new stream `click_view` | +| `0.1.14` | 2021-10-01 | [6565](https://github.com/airbytehq/airbyte/pull/6565) | Fix OAuth Spec File | +| `0.1.13` | 2021-09-27 | [6458](https://github.com/airbytehq/airbyte/pull/6458) | Update OAuth Spec File | +| `0.1.11` | 2021-09-22 | [6373](https://github.com/airbytehq/airbyte/pull/6373) | Fix inconsistent segments.date field type across all streams | +| `0.1.10` | 2021-09-13 | [6022](https://github.com/airbytehq/airbyte/pull/6022) | Annotate Oauth2 flow initialization parameters in connector spec | +| `0.1.9` | 2021-09-07 | [5302](https://github.com/airbytehq/airbyte/pull/5302) | Add custom query stream support | +| `0.1.8` | 2021-08-03 | [5509](https://github.com/airbytehq/airbyte/pull/5509) | Allow additionalProperties in spec.json | +| `0.1.7` | 2021-08-03 | [5422](https://github.com/airbytehq/airbyte/pull/5422) | Correct query to not skip dates | +| `0.1.6` | 2021-08-03 | [5423](https://github.com/airbytehq/airbyte/pull/5423) | Added new stream UserLocationReport | +| `0.1.5` | 2021-08-03 | [5159](https://github.com/airbytehq/airbyte/pull/5159) | Add field `login_customer_id` to spec | +| `0.1.4` | 2021-07-28 | [4962](https://github.com/airbytehq/airbyte/pull/4962) | Support new Report streams | +| `0.1.3` | 2021-07-23 | [4788](https://github.com/airbytehq/airbyte/pull/4788) | Support main streams, fix bug with exception `DATE_RANGE_TOO_NARROW` for incremental streams | +| `0.1.2` | 2021-07-06 | [4539](https://github.com/airbytehq/airbyte/pull/4539) | Add `AIRBYTE_ENTRYPOINT` for Kubernetes support | +| `0.1.1` | 2021-06-23 | [4288](https://github.com/airbytehq/airbyte/pull/4288) | Fix `Bugfix: Correctly declare required parameters` | From 887023a59ca33cb6ed0786474b1b1013ac3e7670 Mon Sep 17 00:00:00 2001 From: Gireesh Sreepathi Date: Thu, 11 Jan 2024 12:12:40 -0800 Subject: [PATCH 071/574] cdk bump to 0.12.0 (#34185) Signed-off-by: Gireesh Sreepathi --- .../connectors/destination-redshift/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airbyte-integrations/connectors/destination-redshift/build.gradle b/airbyte-integrations/connectors/destination-redshift/build.gradle index dfe01f34dc6d6..7b137ee3a8bbe 100644 --- a/airbyte-integrations/connectors/destination-redshift/build.gradle +++ b/airbyte-integrations/connectors/destination-redshift/build.gradle @@ -4,9 +4,9 @@ plugins { } airbyteJavaConnector { - cdkVersionRequired = '0.11.1' + cdkVersionRequired = '0.12.0' features = ['db-destinations', 's3-destinations'] - useLocalCdk = true + useLocalCdk = false } //remove once upgrading the CDK version to 0.4.x or later From 9a3666058eb5c827932c7057615c7a68d6e080ce Mon Sep 17 00:00:00 2001 From: Ben Church Date: Thu, 11 Jan 2024 13:08:04 -0800 Subject: [PATCH 072/574] Airbyte-ci: Ensure we set the working directory earlier (#34136) --- airbyte-ci/connectors/pipelines/README.md | 1 + .../pipelines/pipelines/cli/airbyte_ci.py | 62 ++----------------- .../pipelines/cli/ensure_repo_root.py | 57 +++++++++++++++++ .../connectors/pipelines/pyproject.toml | 2 +- 4 files changed, 65 insertions(+), 57 deletions(-) create mode 100644 airbyte-ci/connectors/pipelines/pipelines/cli/ensure_repo_root.py diff --git a/airbyte-ci/connectors/pipelines/README.md b/airbyte-ci/connectors/pipelines/README.md index a0781b26c37d7..4f26d9ad07fe9 100644 --- a/airbyte-ci/connectors/pipelines/README.md +++ b/airbyte-ci/connectors/pipelines/README.md @@ -521,6 +521,7 @@ E.G.: running `pytest` on a specific test folder: | Version | PR | Description | | ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| 3.1.3 | [#34136](https://github.com/airbytehq/airbyte/pull/34136) | Fix issue where dagger excludes were not being properly applied | | 3.1.2 | [#33972](https://github.com/airbytehq/airbyte/pull/33972) | Remove secrets scrubbing hack for --is-local and other small tweaks. | | 3.1.1 | [#33979](https://github.com/airbytehq/airbyte/pull/33979) | Fix AssertionError on report existence again | | 3.1.0 | [#33994](https://github.com/airbytehq/airbyte/pull/33994) | Log more context information in CI. | diff --git a/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py b/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py index 028bb54df7c74..cb5226728929a 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py +++ b/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py @@ -6,16 +6,20 @@ from __future__ import annotations +# Important: This import and function call must be the first import in this file +# This is needed to ensure that the working directory is the root of the airbyte repo +from pipelines.cli.ensure_repo_root import set_working_directory_to_root + +set_working_directory_to_root() + import logging import multiprocessing import os import sys -from pathlib import Path from typing import Optional import asyncclick as click import docker # type: ignore -import git from github import PullRequest from pipelines import main_logger from pipelines.cli.auto_update import __installed_version__, check_for_upgrade, pre_confirm_auto_update_flag @@ -30,58 +34,6 @@ from pipelines.helpers.utils import get_current_epoch_time -def _validate_airbyte_repo(repo: git.Repo) -> bool: - """Check if any of the remotes are the airbyte repo.""" - expected_repo_name = "airbytehq/airbyte" - for remote in repo.remotes: - if expected_repo_name in remote.url: - return True - - warning_message = f""" - ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ - - It looks like you are not running this command from the airbyte repo ({expected_repo_name}). - - If this command is run from outside the airbyte repo, it will not work properly. - - Please run this command your local airbyte project. - - ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ - """ - - logging.warning(warning_message) - - return False - - -def get_airbyte_repo() -> git.Repo: - """Get the airbyte repo.""" - repo = git.Repo(search_parent_directories=True) - _validate_airbyte_repo(repo) - return repo - - -def get_airbyte_repo_path_with_fallback() -> Path: - """Get the path to the airbyte repo.""" - try: - repo_path = get_airbyte_repo().working_tree_dir - if repo_path is not None: - return Path(str(get_airbyte_repo().working_tree_dir)) - except git.exc.InvalidGitRepositoryError: - pass - logging.warning("Could not find the airbyte repo, falling back to the current working directory.") - path = Path.cwd() - logging.warning(f"Using {path} as the airbyte repo path.") - return path - - -def set_working_directory_to_root() -> None: - """Set the working directory to the root of the airbyte repo.""" - working_dir = get_airbyte_repo_path_with_fallback() - logging.info(f"Setting working directory to {working_dir}") - os.chdir(working_dir) - - def log_context_info(ctx: click.Context) -> None: main_logger.info(f"Running airbyte-ci version {__installed_version__}") main_logger.info(f"Running dagger version {get_dagger_sdk_version()}") @@ -241,7 +193,5 @@ async def airbyte_ci(ctx: click.Context) -> None: # noqa D103 log_context_info(ctx) -set_working_directory_to_root() - if __name__ == "__main__": airbyte_ci() diff --git a/airbyte-ci/connectors/pipelines/pipelines/cli/ensure_repo_root.py b/airbyte-ci/connectors/pipelines/pipelines/cli/ensure_repo_root.py new file mode 100644 index 0000000000000..7b0c4b39576a1 --- /dev/null +++ b/airbyte-ci/connectors/pipelines/pipelines/cli/ensure_repo_root.py @@ -0,0 +1,57 @@ +import logging +import os +from pathlib import Path + +import git + + +def _validate_airbyte_repo(repo: git.Repo) -> bool: + """Check if any of the remotes are the airbyte repo.""" + expected_repo_name = "airbytehq/airbyte" + for remote in repo.remotes: + if expected_repo_name in remote.url: + return True + + warning_message = f""" + ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ + + It looks like you are not running this command from the airbyte repo ({expected_repo_name}). + + If this command is run from outside the airbyte repo, it will not work properly. + + Please run this command your local airbyte project. + + ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ + """ + + logging.warning(warning_message) + + return False + + +def get_airbyte_repo() -> git.Repo: + """Get the airbyte repo.""" + repo = git.Repo(search_parent_directories=True) + _validate_airbyte_repo(repo) + return repo + + +def get_airbyte_repo_path_with_fallback() -> Path: + """Get the path to the airbyte repo.""" + try: + repo_path = get_airbyte_repo().working_tree_dir + if repo_path is not None: + return Path(str(get_airbyte_repo().working_tree_dir)) + except git.exc.InvalidGitRepositoryError: + pass + logging.warning("Could not find the airbyte repo, falling back to the current working directory.") + path = Path.cwd() + logging.warning(f"Using {path} as the airbyte repo path.") + return path + + +def set_working_directory_to_root() -> None: + """Set the working directory to the root of the airbyte repo.""" + working_dir = get_airbyte_repo_path_with_fallback() + logging.info(f"Setting working directory to {working_dir}") + os.chdir(working_dir) diff --git a/airbyte-ci/connectors/pipelines/pyproject.toml b/airbyte-ci/connectors/pipelines/pyproject.toml index 2411a03fe1dbe..256d1075bad88 100644 --- a/airbyte-ci/connectors/pipelines/pyproject.toml +++ b/airbyte-ci/connectors/pipelines/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pipelines" -version = "3.1.2" +version = "3.1.3" description = "Packaged maintained by the connector operations team to perform CI for connectors' pipelines" authors = ["Airbyte "] From a4ff89c1ba4356fe5bd5338e99ca7d1a0d7cec7d Mon Sep 17 00:00:00 2001 From: Edward Gao Date: Thu, 11 Jan 2024 16:16:40 -0800 Subject: [PATCH 073/574] DV2 TypingDedupingTest: read container stdout in real time (#34173) --- .../BaseTypingDedupingTest.java | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/java/io/airbyte/integrations/base/destination/typing_deduping/BaseTypingDedupingTest.java b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/java/io/airbyte/integrations/base/destination/typing_deduping/BaseTypingDedupingTest.java index 70653097d2f8e..d4e46d7cc3d51 100644 --- a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/java/io/airbyte/integrations/base/destination/typing_deduping/BaseTypingDedupingTest.java +++ b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/java/io/airbyte/integrations/base/destination/typing_deduping/BaseTypingDedupingTest.java @@ -35,6 +35,8 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.function.Function; import java.util.stream.Stream; import org.apache.commons.lang3.RandomStringUtils; @@ -601,14 +603,12 @@ public void identicalNameSimultaneousSync() throws Exception { for (int i = 0; i < 100_000; i++) { pushMessages(messages2, sync2); } - // This will dump sync1's entire stdout to our stdout endSync(sync1); // Write some more messages to the second sync. It should not be affected by the first sync's // shutdown. for (int i = 0; i < 100_000; i++) { pushMessages(messages2, sync2); } - // And this will dump sync2's entire stdout to our stdout endSync(sync2); // For simplicity, don't verify the raw table. Assume that if the final table is correct, then @@ -825,6 +825,26 @@ protected AirbyteDestination startSync(final ConfiguredAirbyteCatalog catalog, destination.start(destinationConfig, jobRoot, Collections.emptyMap()); + // In the background, read messages from the destination until it terminates. We need to clear + // stdout in real time, to prevent the buffer from filling up and blocking the destination. + // TODO Eventually we'll want to somehow extract the state messages while a sync is running, to + // verify checkpointing. + final ExecutorService messageHandler = Executors.newSingleThreadExecutor( + // run as a daemon thread just in case we run into an exception or something + r -> { + final Thread t = Executors.defaultThreadFactory().newThread(r); + t.setDaemon(true); + return t; + }); + messageHandler.submit(() -> { + while (!destination.isFinished()) { + // attemptRead isn't threadsafe, we read stdout fully here. + // i.e. we shouldn't call attemptRead anywhere else. + destination.attemptRead(); + } + }); + messageHandler.shutdown(); + return destination; } @@ -833,14 +853,8 @@ protected static void pushMessages(final List messages, final Ai message -> Exceptions.toRuntime(() -> destination.accept(convertProtocolObject(message, io.airbyte.protocol.models.AirbyteMessage.class)))); } - // TODO Eventually we'll want to somehow extract the state messages while a sync is running, to - // verify checkpointing. - // That's going to require some nontrivial changes to how attemptRead() works. protected static void endSync(final AirbyteDestination destination) throws Exception { destination.notifyEndOfInput(); - while (!destination.isFinished()) { - destination.attemptRead(); - } destination.close(); } From e93703dbcecc87341d721544b2940064860d4dab Mon Sep 17 00:00:00 2001 From: Augustin Date: Fri, 12 Jan 2024 10:34:35 +0100 Subject: [PATCH 074/574] airbyte-ci: fix format (#34199) --- .../connectors/pipelines/pipelines/cli/ensure_repo_root.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/airbyte-ci/connectors/pipelines/pipelines/cli/ensure_repo_root.py b/airbyte-ci/connectors/pipelines/pipelines/cli/ensure_repo_root.py index 7b0c4b39576a1..5970979d9d713 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/cli/ensure_repo_root.py +++ b/airbyte-ci/connectors/pipelines/pipelines/cli/ensure_repo_root.py @@ -1,3 +1,5 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + import logging import os from pathlib import Path From 42eff7a2d6d3b01341c01d46ccc0679757650389 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 12 Jan 2024 11:51:24 +0100 Subject: [PATCH 075/574] Source Slack: Convert to airbyte-lib (#34098) --- .../connectors/source-slack/main.py | 9 ++------- .../connectors/source-slack/metadata.yaml | 2 +- .../connectors/source-slack/setup.py | 5 +++++ .../connectors/source-slack/source_slack/run.py | 14 ++++++++++++++ docs/integrations/sources/slack.md | 1 + 5 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 airbyte-integrations/connectors/source-slack/source_slack/run.py diff --git a/airbyte-integrations/connectors/source-slack/main.py b/airbyte-integrations/connectors/source-slack/main.py index 735ad5e72296d..b2ff9c8511630 100644 --- a/airbyte-integrations/connectors/source-slack/main.py +++ b/airbyte-integrations/connectors/source-slack/main.py @@ -2,12 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_slack import SourceSlack +from source_slack.run import run if __name__ == "__main__": - source = SourceSlack() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-slack/metadata.yaml b/airbyte-integrations/connectors/source-slack/metadata.yaml index 1350953b139cb..85b8fbb337be8 100644 --- a/airbyte-integrations/connectors/source-slack/metadata.yaml +++ b/airbyte-integrations/connectors/source-slack/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: c2281cee-86f9-4a86-bb48-d23286b4c7bd - dockerImageTag: 0.3.6 + dockerImageTag: 0.3.7 dockerRepository: airbyte/source-slack documentationUrl: https://docs.airbyte.com/integrations/sources/slack githubIssueLabel: source-slack diff --git a/airbyte-integrations/connectors/source-slack/setup.py b/airbyte-integrations/connectors/source-slack/setup.py index 66fc79eeca33f..f1040f3acca22 100644 --- a/airbyte-integrations/connectors/source-slack/setup.py +++ b/airbyte-integrations/connectors/source-slack/setup.py @@ -12,6 +12,11 @@ ] setup( + entry_points={ + "console_scripts": [ + "source-slack=source_slack.run:run", + ], + }, name="source_slack", description="Source implementation for Slack.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-slack/source_slack/run.py b/airbyte-integrations/connectors/source-slack/source_slack/run.py new file mode 100644 index 0000000000000..14caa9ab08e1e --- /dev/null +++ b/airbyte-integrations/connectors/source-slack/source_slack/run.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_slack import SourceSlack + + +def run(): + source = SourceSlack() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/slack.md b/docs/integrations/sources/slack.md index ae2c0a12a0e09..2edb9eb6fcc67 100644 --- a/docs/integrations/sources/slack.md +++ b/docs/integrations/sources/slack.md @@ -163,6 +163,7 @@ Slack has [rate limit restrictions](https://api.slack.com/docs/rate-limits). | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:------------------------------------------------------------------------------------| +| 0.3.7 | 2024-01-10 | [1234](https://github.com/airbytehq/airbyte/pull/1234) | prepare for airbyte-lib | | 0.3.6 | 2023-11-21 | [32707](https://github.com/airbytehq/airbyte/pull/32707) | Threads: do not use client-side record filtering | | 0.3.5 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | | 0.3.4 | 2023-10-06 | [31134](https://github.com/airbytehq/airbyte/pull/31134) | Update CDK and remove non iterable return from records | From e2146ea63ddd4d0752c1a9b3c17251483b5c28fa Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 12 Jan 2024 11:52:29 +0100 Subject: [PATCH 076/574] Source Freshdesk: Convert to airbyte-lib (#34101) --- .../connectors/source-freshdesk/main.py | 9 ++------- .../connectors/source-freshdesk/metadata.yaml | 2 +- .../connectors/source-freshdesk/setup.py | 5 +++++ .../source-freshdesk/source_freshdesk/run.py | 14 ++++++++++++++ docs/integrations/sources/freshdesk.md | 1 + 5 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 airbyte-integrations/connectors/source-freshdesk/source_freshdesk/run.py diff --git a/airbyte-integrations/connectors/source-freshdesk/main.py b/airbyte-integrations/connectors/source-freshdesk/main.py index 319505ff4bb53..d32eaa6ca9e52 100644 --- a/airbyte-integrations/connectors/source-freshdesk/main.py +++ b/airbyte-integrations/connectors/source-freshdesk/main.py @@ -2,12 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_freshdesk import SourceFreshdesk +from source_freshdesk.run import run if __name__ == "__main__": - source = SourceFreshdesk() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-freshdesk/metadata.yaml b/airbyte-integrations/connectors/source-freshdesk/metadata.yaml index 4a9cd521ff4a5..e532adfce2a55 100644 --- a/airbyte-integrations/connectors/source-freshdesk/metadata.yaml +++ b/airbyte-integrations/connectors/source-freshdesk/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: ec4b9503-13cb-48ab-a4ab-6ade4be46567 - dockerImageTag: 3.0.5 + dockerImageTag: 3.0.6 dockerRepository: airbyte/source-freshdesk documentationUrl: https://docs.airbyte.com/integrations/sources/freshdesk githubIssueLabel: source-freshdesk diff --git a/airbyte-integrations/connectors/source-freshdesk/setup.py b/airbyte-integrations/connectors/source-freshdesk/setup.py index c0d8b408f781f..b9cabbadddfc8 100644 --- a/airbyte-integrations/connectors/source-freshdesk/setup.py +++ b/airbyte-integrations/connectors/source-freshdesk/setup.py @@ -15,6 +15,11 @@ ] setup( + entry_points={ + "console_scripts": [ + "source-freshdesk=source_freshdesk.run:run", + ], + }, name="source_freshdesk", description="Source implementation for Freshdesk.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-freshdesk/source_freshdesk/run.py b/airbyte-integrations/connectors/source-freshdesk/source_freshdesk/run.py new file mode 100644 index 0000000000000..5486a3c150610 --- /dev/null +++ b/airbyte-integrations/connectors/source-freshdesk/source_freshdesk/run.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_freshdesk import SourceFreshdesk + + +def run(): + source = SourceFreshdesk() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/freshdesk.md b/docs/integrations/sources/freshdesk.md index b148533bb801c..95b855e3b96de 100644 --- a/docs/integrations/sources/freshdesk.md +++ b/docs/integrations/sources/freshdesk.md @@ -68,6 +68,7 @@ If you don't use the start date Freshdesk will retrieve only the last 30 days. M | Version | Date | Pull Request | Subject | | :------ | :--------- | :------------------------------------------------------- | :------------------------------------------------------------------------------------ | +| 3.0.6 | 2024-01-10 | [34101](https://github.com/airbytehq/airbyte/pull/34101) | Base image migration: remove Dockerfile and use the python-connector-base image | | 3.0.5 | 2023-11-30 | [33000](https://github.com/airbytehq/airbyte/pull/33000) | Base image migration: remove Dockerfile and use the python-connector-base image | | 3.0.4 | 2023-06-24 | [27680](https://github.com/airbytehq/airbyte/pull/27680) | Fix formatting | | 3.0.3 | 2023-06-02 | [26978](https://github.com/airbytehq/airbyte/pull/26978) | Skip the stream if subscription level had changed during sync | From 94dc6861cf051c83944b7ac1a674f1d4cf55af83 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 12 Jan 2024 11:55:28 +0100 Subject: [PATCH 077/574] Source GCS: Fix unstructured format (#34158) --- .../connectors/source-gcs/integration_tests/spec.json | 6 +++--- airbyte-integrations/connectors/source-gcs/metadata.yaml | 2 +- .../connectors/source-gcs/source_gcs/config.py | 4 ++++ .../connectors/source-gcs/source_gcs/stream_reader.py | 7 +------ docs/integrations/sources/gcs.md | 1 + 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/airbyte-integrations/connectors/source-gcs/integration_tests/spec.json b/airbyte-integrations/connectors/source-gcs/integration_tests/spec.json index 560361ec3b309..5f69da41c02ac 100644 --- a/airbyte-integrations/connectors/source-gcs/integration_tests/spec.json +++ b/airbyte-integrations/connectors/source-gcs/integration_tests/spec.json @@ -58,9 +58,9 @@ }, "primary_key": { "title": "Primary Key", - "description": "The column or columns (for a composite key) that serves as the unique identifier of a record.", - "type": "string", - "airbyte_hidden": true + "description": "The column or columns (for a composite key) that serves as the unique identifier of a record. If empty, the primary key will default to the parser's default primary key.", + "airbyte_hidden": true, + "type": "string" }, "days_to_sync_if_history_is_full": { "title": "Days To Sync If History Is Full", diff --git a/airbyte-integrations/connectors/source-gcs/metadata.yaml b/airbyte-integrations/connectors/source-gcs/metadata.yaml index e9364232fbd2e..1ed2d67577834 100644 --- a/airbyte-integrations/connectors/source-gcs/metadata.yaml +++ b/airbyte-integrations/connectors/source-gcs/metadata.yaml @@ -7,7 +7,7 @@ data: connectorSubtype: file connectorType: source definitionId: 2a8c41ae-8c23-4be0-a73f-2ab10ca1a820 - dockerImageTag: 0.3.3 + dockerImageTag: 0.3.4 dockerRepository: airbyte/source-gcs documentationUrl: https://docs.airbyte.com/integrations/sources/gcs githubIssueLabel: source-gcs diff --git a/airbyte-integrations/connectors/source-gcs/source_gcs/config.py b/airbyte-integrations/connectors/source-gcs/source_gcs/config.py index ebd1117841e11..04dc1e16b8d37 100644 --- a/airbyte-integrations/connectors/source-gcs/source_gcs/config.py +++ b/airbyte-integrations/connectors/source-gcs/source_gcs/config.py @@ -80,3 +80,7 @@ def replace_enum_allOf_and_anyOf(schema): objects_to_check["anyOf"] = objects_to_check.pop("allOf") return super(Config, Config).replace_enum_allOf_and_anyOf(schema) + + @staticmethod + def remove_discriminator(schema) -> None: + pass diff --git a/airbyte-integrations/connectors/source-gcs/source_gcs/stream_reader.py b/airbyte-integrations/connectors/source-gcs/source_gcs/stream_reader.py index 3552d75980fd9..ec44dd27048ed 100644 --- a/airbyte-integrations/connectors/source-gcs/source_gcs/stream_reader.py +++ b/airbyte-integrations/connectors/source-gcs/source_gcs/stream_reader.py @@ -5,7 +5,6 @@ import itertools import json import logging -from contextlib import contextmanager from datetime import datetime, timedelta from io import IOBase from typing import Iterable, List, Optional @@ -94,7 +93,6 @@ def _handle_file_listing_error(self, exc: Exception, prefix: str, logger: loggin prefix=prefix, ) from exc - @contextmanager def open_file(self, file: RemoteFile, mode: FileReadMode, encoding: Optional[str], logger: logging.Logger) -> IOBase: """ Open and yield a remote file from GCS for reading. @@ -105,7 +103,4 @@ def open_file(self, file: RemoteFile, mode: FileReadMode, encoding: Optional[str except OSError as oe: logger.warning(ERROR_MESSAGE_ACCESS.format(uri=file.uri, bucket=self.config.bucket)) logger.exception(oe) - try: - yield result - finally: - result.close() + return result diff --git a/docs/integrations/sources/gcs.md b/docs/integrations/sources/gcs.md index 1206c289e4f65..1dcb6d735fc0c 100644 --- a/docs/integrations/sources/gcs.md +++ b/docs/integrations/sources/gcs.md @@ -37,6 +37,7 @@ Use the service account ID from above, grant read access to your target bucket. | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:----------------------------------------------------| +| 0.3.4 | 2024-01-11 | [34158](https://github.com/airbytehq/airbyte/pull/34158) | Fix issue in stream reader for document file type parser | | 0.3.3 | 2023-12-06 | [33187](https://github.com/airbytehq/airbyte/pull/33187) | Bump CDK version to hide source-defined primary key | | 0.3.2 | 2023-11-16 | [32608](https://github.com/airbytehq/airbyte/pull/32608) | Improve document file type parser | | 0.3.1 | 2023-11-13 | [32357](https://github.com/airbytehq/airbyte/pull/32357) | Improve spec schema | From c967f146dde322079b0dc8cd5ced6dd63d7bba6f Mon Sep 17 00:00:00 2001 From: Anatolii Yatsuk <35109939+tolik0@users.noreply.github.com> Date: Fri, 12 Jan 2024 12:58:32 +0200 Subject: [PATCH 078/574] =?UTF-8?q?=F0=9F=90=9B=20Source=20Google=20Ads:?= =?UTF-8?q?=20Disable=20raising=20error=20for=20not=20enabled=20accounts?= =?UTF-8?q?=20(#34200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../connectors/source-google-ads/metadata.yaml | 2 +- .../source-google-ads/source_google_ads/streams.py | 1 - .../source-google-ads/unit_tests/test_errors.py | 9 --------- docs/integrations/sources/google-ads.md | 1 + 4 files changed, 2 insertions(+), 11 deletions(-) diff --git a/airbyte-integrations/connectors/source-google-ads/metadata.yaml b/airbyte-integrations/connectors/source-google-ads/metadata.yaml index 50a62c4282e3f..9c19525503148 100644 --- a/airbyte-integrations/connectors/source-google-ads/metadata.yaml +++ b/airbyte-integrations/connectors/source-google-ads/metadata.yaml @@ -11,7 +11,7 @@ data: connectorSubtype: api connectorType: source definitionId: 253487c0-2246-43ba-a21f-5116b20a2c50 - dockerImageTag: 3.2.0 + dockerImageTag: 3.2.1 dockerRepository: airbyte/source-google-ads documentationUrl: https://docs.airbyte.com/integrations/sources/google-ads githubIssueLabel: source-google-ads diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py b/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py index d843771b82e2d..284728394c9f5 100644 --- a/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py @@ -236,7 +236,6 @@ class CustomerClient(GoogleAdsStream): Customer Client stream: https://developers.google.com/google-ads/api/fields/v15/customer_client """ - CATCH_CUSTOMER_NOT_ENABLED_ERROR = False primary_key = ["customer_client.id"] def __init__(self, customer_status_filter: List[str], **kwargs): diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_errors.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_errors.py index 9bf943bb145d1..e71263296007d 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_errors.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_errors.py @@ -34,15 +34,6 @@ def mock_get_customers(mocker): ["CUSTOMER_NOT_FOUND"], "Failed to access the customer '123'. Ensure the customer is linked to your manager account or check your permissions to access this customer account.", ), - ( - ["CUSTOMER_NOT_ENABLED"], - ( - "The customer account '123' hasn't finished signup or has been deactivated. " - "Sign in to the Google Ads UI to verify its status. " - "For reactivating deactivated accounts, refer to: " - "https://support.google.com/google-ads/answer/2375392." - ), - ), (["QUERY_ERROR"], "Incorrect custom query. Error in query: unexpected end of query."), ( ["RESOURCE_EXHAUSTED"], diff --git a/docs/integrations/sources/google-ads.md b/docs/integrations/sources/google-ads.md index 4f1b239d9ff17..1aafb621bf8b7 100644 --- a/docs/integrations/sources/google-ads.md +++ b/docs/integrations/sources/google-ads.md @@ -280,6 +280,7 @@ Due to a limitation in the Google Ads API which does not allow getting performan | Version | Date | Pull Request | Subject | |:---------|:-----------|:---------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------| +| `3.2.1` | 2024-01-12 | [34200](https://github.com/airbytehq/airbyte/pull/34200) | Disable raising error for not enabled accounts | | `3.2.0` | 2024-01-09 | [33707](https://github.com/airbytehq/airbyte/pull/33707) | Add possibility to sync all connected accounts | | `3.1.0` | 2024-01-09 | [33603](https://github.com/airbytehq/airbyte/pull/33603) | Fix two issues in the custom queries: automatic addition of `segments.date` in the query; incorrect field type for `DATE` fields. | | `3.0.2` | 2024-01-08 | [33494](https://github.com/airbytehq/airbyte/pull/33494) | Add handling for 401 error while parsing response. Add `metrics.cost_micros` field to Ad Group stream. | From f637e11ed78e2f08d87a39bdf7995e5252cf26fa Mon Sep 17 00:00:00 2001 From: Alexandre Cuoci Date: Fri, 12 Jan 2024 12:00:56 -0500 Subject: [PATCH 079/574] Add S3 IAM roles + ALB ingress definition (#33944) Co-authored-by: perangel --- docs/enterprise-setup/implementation-guide.md | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/docs/enterprise-setup/implementation-guide.md b/docs/enterprise-setup/implementation-guide.md index b94e89c50dd0a..a12843e3c19e2 100644 --- a/docs/enterprise-setup/implementation-guide.md +++ b/docs/enterprise-setup/implementation-guide.md @@ -143,7 +143,7 @@ minio: - + ```yaml global: @@ -173,6 +173,37 @@ global: For each of `accessKey` and `secretKey`, the `password` and `existingSecret` fields are mutually exclusive. +3. Ensure your access key is tied to an IAM user with the [following policies](https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-policies-s3.html#iam-policy-ex0), allowing the user access to S3 storage: + +```yaml +{ + "Version":"2012-10-17", + "Statement":[ + { + "Effect":"Allow", + "Action": "s3:ListAllMyBuckets", + "Resource":"*" + }, + { + "Effect":"Allow", + "Action":["s3:ListBucket","s3:GetBucketLocation"], + "Resource":"arn:aws:s3:::YOUR-S3-BUCKET-NAME" + }, + { + "Effect":"Allow", + "Action":[ + "s3:PutObject", + "s3:PutObjectAcl", + "s3:GetObject", + "s3:GetObjectAcl", + "s3:DeleteObject" + ], + "Resource":"arn:aws:s3:::YOUR-S3-BUCKET-NAME/*" + } + ] +} +``` + @@ -204,6 +235,11 @@ Note that the `credentials` and `credentialsJson` fields are mutually exclusive. To access the Airbyte UI, you will need to manually attach an ingress configuration to your deployment. The following is a skimmed down definition of an ingress resource you could use for Self-Managed Enterprise: +
+Ingress configuration setup steps + + + ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress @@ -234,6 +270,59 @@ spec: pathType: Prefix ``` + + + +If you are intending on using Amazon Application Load Balancer (ALB) for ingress, this ingress definition will be close to what's needed to get up and running: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: + annotations: + # Specifies that the Ingress should use an AWS ALB. + kubernetes.io/ingress.class: "alb" + # Redirects HTTP traffic to HTTPS. + ingress.kubernetes.io/ssl-redirect: "true" + # Creates an internal ALB, which is only accessible within your VPC or through a VPN. + alb.ingress.kubernetes.io/scheme: internal + # Specifies the ARN of the SSL certificate managed by AWS ACM, essential for HTTPS. + alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-x:xxxxxxxxx:certificate/xxxxxxxxx-xxxxx-xxxx-xxxx-xxxxxxxxxxx + # Sets the idle timeout value for the ALB. + alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=30 + # [If Applicable] Specifies the VPC subnets and security groups for the ALB + # alb.ingress.kubernetes.io/subnets: '' e.g. 'subnet-12345, subnet-67890' + # alb.ingress.kubernetes.io/security-groups: +spec: + rules: + - host: e.g. enterprise-demo.airbyte.com + http: + paths: + - backend: + service: + name: airbyte-pro-airbyte-webapp-svc + port: + number: 80 + path: / + pathType: Prefix + - backend: + service: + name: airbyte-pro-airbyte-keycloak-svc + port: + number: 8180 + path: /auth + pathType: Prefix +``` + +The ALB controller will use a `ServiceAccount` that requires the [following IAM policy](https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json) to be attached. + + + +
+ +Once this is complete, ensure that the value of the `webapp-url` field in your `airbyte.yml` is configured to match the ingress URL. + You may configure ingress using a load balancer or an API Gateway. We do not currently support most service meshes (such as Istio). If you are having networking issues after fully deploying Airbyte, please verify that firewalls or lacking permissions are not interfering with pod-pod communication. Please also verify that deployed pods have the right permissions to make requests to your external database. ### Install Airbyte Enterprise From 99a23dc3dc9dd5478a4062190e961eda195e41c1 Mon Sep 17 00:00:00 2001 From: "Aaron (\"AJ\") Steers" Date: Fri, 12 Jan 2024 09:43:55 -0800 Subject: [PATCH 080/574] AirbyteLib: add SQLCaches for DuckDB and Postgres (includes Ruff+Mypy cleanup) (#33607) --- airbyte-lib/airbyte_lib/__init__.py | 15 +- .../airbyte_lib/{executor.py => _executor.py} | 58 +- .../airbyte_lib/_factories/__init__.py | 0 .../airbyte_lib/_factories/cache_factories.py | 58 + .../connector_factories.py} | 9 +- .../airbyte_lib/_file_writers/__init__.py | 11 + airbyte-lib/airbyte_lib/_file_writers/base.py | 112 ++ .../airbyte_lib/_file_writers/parquet.py | 59 + airbyte-lib/airbyte_lib/_processors.py | 312 ++++ airbyte-lib/airbyte_lib/_util/__init__.py | 0 .../airbyte_lib/_util/protocol_util.py | 67 + airbyte-lib/airbyte_lib/cache.py | 54 - airbyte-lib/airbyte_lib/caches/__init__.py | 15 + airbyte-lib/airbyte_lib/caches/base.py | 738 +++++++++ airbyte-lib/airbyte_lib/caches/duckdb.py | 147 ++ airbyte-lib/airbyte_lib/caches/postgres.py | 51 + airbyte-lib/airbyte_lib/caches/snowflake.py | 70 + airbyte-lib/airbyte_lib/config.py | 13 + airbyte-lib/airbyte_lib/datasets/__init__.py | 10 + airbyte-lib/airbyte_lib/datasets/_base.py | 28 + airbyte-lib/airbyte_lib/datasets/_cached.py | 34 + airbyte-lib/airbyte_lib/datasets/_lazy.py | 43 + airbyte-lib/airbyte_lib/datasets/_map.py | 27 + airbyte-lib/airbyte_lib/registry.py | 11 +- airbyte-lib/airbyte_lib/results.py | 22 + airbyte-lib/airbyte_lib/source.py | 164 +- airbyte-lib/airbyte_lib/sync_result.py | 32 - airbyte-lib/airbyte_lib/types.py | 110 ++ airbyte-lib/airbyte_lib/validate.py | 30 +- airbyte-lib/docs/generated/airbyte_lib.html | 160 +- .../docs/generated/airbyte_lib/caches.html | 678 +++++++++ .../docs/generated/airbyte_lib/datasets.html | 112 ++ .../docs/generated/airbyte_lib/factories.html | 46 - .../generated/airbyte_lib/file_writers.html | 7 - airbyte-lib/examples/run_faker.py | 26 + airbyte-lib/examples/run_spacex.py | 15 +- airbyte-lib/examples/run_test_source.py | 5 +- airbyte-lib/poetry.lock | 1336 ++++++++++++++++- airbyte-lib/pyproject.toml | 147 +- airbyte-lib/tests/conftest.py | 91 ++ .../integration_tests/test_integration.py | 156 +- airbyte-lib/tests/lint_tests/__init__.py | 0 airbyte-lib/tests/lint_tests/test_mypy.py | 21 + airbyte-lib/tests/lint_tests/test_ruff.py | 60 + airbyte-lib/tests/unit_tests/__init__.py | 0 airbyte-lib/tests/unit_tests/test_caches.py | 60 + .../tests/unit_tests/test_type_translation.py | 27 + airbyte-lib/tests/unit_tests/test_writers.py | 38 + pyproject.toml | 11 +- 49 files changed, 4925 insertions(+), 371 deletions(-) rename airbyte-lib/airbyte_lib/{executor.py => _executor.py} (79%) create mode 100644 airbyte-lib/airbyte_lib/_factories/__init__.py create mode 100644 airbyte-lib/airbyte_lib/_factories/cache_factories.py rename airbyte-lib/airbyte_lib/{factories.py => _factories/connector_factories.py} (92%) create mode 100644 airbyte-lib/airbyte_lib/_file_writers/__init__.py create mode 100644 airbyte-lib/airbyte_lib/_file_writers/base.py create mode 100644 airbyte-lib/airbyte_lib/_file_writers/parquet.py create mode 100644 airbyte-lib/airbyte_lib/_processors.py create mode 100644 airbyte-lib/airbyte_lib/_util/__init__.py create mode 100644 airbyte-lib/airbyte_lib/_util/protocol_util.py delete mode 100644 airbyte-lib/airbyte_lib/cache.py create mode 100644 airbyte-lib/airbyte_lib/caches/__init__.py create mode 100644 airbyte-lib/airbyte_lib/caches/base.py create mode 100644 airbyte-lib/airbyte_lib/caches/duckdb.py create mode 100644 airbyte-lib/airbyte_lib/caches/postgres.py create mode 100644 airbyte-lib/airbyte_lib/caches/snowflake.py create mode 100644 airbyte-lib/airbyte_lib/config.py create mode 100644 airbyte-lib/airbyte_lib/datasets/__init__.py create mode 100644 airbyte-lib/airbyte_lib/datasets/_base.py create mode 100644 airbyte-lib/airbyte_lib/datasets/_cached.py create mode 100644 airbyte-lib/airbyte_lib/datasets/_lazy.py create mode 100644 airbyte-lib/airbyte_lib/datasets/_map.py create mode 100644 airbyte-lib/airbyte_lib/results.py delete mode 100644 airbyte-lib/airbyte_lib/sync_result.py create mode 100644 airbyte-lib/airbyte_lib/types.py delete mode 100644 airbyte-lib/docs/generated/airbyte_lib/factories.html delete mode 100644 airbyte-lib/docs/generated/airbyte_lib/file_writers.html create mode 100644 airbyte-lib/examples/run_faker.py create mode 100644 airbyte-lib/tests/conftest.py create mode 100644 airbyte-lib/tests/lint_tests/__init__.py create mode 100644 airbyte-lib/tests/lint_tests/test_mypy.py create mode 100644 airbyte-lib/tests/lint_tests/test_ruff.py create mode 100644 airbyte-lib/tests/unit_tests/__init__.py create mode 100644 airbyte-lib/tests/unit_tests/test_caches.py create mode 100644 airbyte-lib/tests/unit_tests/test_type_translation.py create mode 100644 airbyte-lib/tests/unit_tests/test_writers.py diff --git a/airbyte-lib/airbyte_lib/__init__.py b/airbyte-lib/airbyte_lib/__init__.py index a0c1b81906c14..8ba1300c69730 100644 --- a/airbyte-lib/airbyte_lib/__init__.py +++ b/airbyte-lib/airbyte_lib/__init__.py @@ -1,12 +1,15 @@ +from airbyte_lib._factories.cache_factories import get_default_cache, new_local_cache +from airbyte_lib._factories.connector_factories import get_connector +from airbyte_lib.datasets import CachedDataset +from airbyte_lib.results import ReadResult +from airbyte_lib.source import Source -from .factories import (get_connector, get_in_memory_cache) -from .sync_result import (Dataset, SyncResult) -from .source import (Source) __all__ = [ "get_connector", - "get_in_memory_cache", - "Dataset", - "SyncResult", + "get_default_cache", + "new_local_cache", + "CachedDataset", + "ReadResult", "Source", ] diff --git a/airbyte-lib/airbyte_lib/executor.py b/airbyte-lib/airbyte_lib/_executor.py similarity index 79% rename from airbyte-lib/airbyte_lib/executor.py rename to airbyte-lib/airbyte_lib/_executor.py index 71dd1897c5fe1..a051feb603661 100644 --- a/airbyte-lib/airbyte_lib/executor.py +++ b/airbyte-lib/airbyte_lib/_executor.py @@ -4,12 +4,14 @@ import subprocess import sys from abc import ABC, abstractmethod +from collections.abc import Generator, Iterable, Iterator from contextlib import contextmanager from pathlib import Path -from typing import IO, Generator, Iterable, List +from typing import IO, Any, NoReturn from airbyte_lib.registry import ConnectorMetadata + _LATEST_VERSION = "latest" @@ -27,24 +29,24 @@ def __init__( self.target_version = target_version @abstractmethod - def execute(self, args: List[str]) -> Iterable[str]: + def execute(self, args: list[str]) -> Iterator[str]: pass @abstractmethod - def ensure_installation(self): + def ensure_installation(self) -> None: pass @abstractmethod - def install(self): + def install(self) -> None: pass @abstractmethod - def uninstall(self): + def uninstall(self) -> None: pass @contextmanager -def _stream_from_subprocess(args: List[str]) -> Generator[Iterable[str], None, None]: +def _stream_from_subprocess(args: list[str]) -> Generator[Iterable[str], None, None]: process = subprocess.Popen( args, stdout=subprocess.PIPE, @@ -52,7 +54,7 @@ def _stream_from_subprocess(args: List[str]) -> Generator[Iterable[str], None, N universal_newlines=True, ) - def _stream_from_file(file: IO[str]): + def _stream_from_file(file: IO[str]) -> Generator[str, Any, None]: while True: line = file.readline() if not line: @@ -102,23 +104,23 @@ def __init__( # TODO: Replace with `f"airbyte-{self.metadata.name}"` self.pip_url = pip_url or f"../airbyte-integrations/connectors/{self.metadata.name}" - def _get_venv_name(self): + def _get_venv_name(self) -> str: return f".venv-{self.metadata.name}" - def _get_connector_path(self): + def _get_connector_path(self) -> Path: return Path(self._get_venv_name(), "bin", self.metadata.name) - def _run_subprocess_and_raise_on_failure(self, args: List[str]): - result = subprocess.run(args) + def _run_subprocess_and_raise_on_failure(self, args: list[str]) -> None: + result = subprocess.run(args, check=False) if result.returncode != 0: raise Exception(f"Install process exited with code {result.returncode}") - def uninstall(self): + def uninstall(self) -> None: venv_name = self._get_venv_name() if os.path.exists(venv_name): self._run_subprocess_and_raise_on_failure(["rm", "-rf", venv_name]) - def install(self): + def install(self) -> None: venv_name = self._get_venv_name() self._run_subprocess_and_raise_on_failure([sys.executable, "-m", "venv", venv_name]) @@ -126,7 +128,7 @@ def install(self): self._run_subprocess_and_raise_on_failure([pip_path, "install", "-e", self.pip_url]) - def _get_installed_version(self): + def _get_installed_version(self) -> str: """ In the venv, run the following: python -c "from importlib.metadata import version; print(version(''))" """ @@ -143,7 +145,7 @@ def _get_installed_version(self): def ensure_installation( self, - ): + ) -> None: """ Ensure that the connector is installed in a virtual environment. If not yet installed and if install_if_missing is True, then install. @@ -157,13 +159,15 @@ def ensure_installation( venv_path = Path(venv_name) if not venv_path.exists(): if not self.install_if_missing: - raise Exception(f"Connector {self.metadata.name} is not available - venv {venv_name} does not exist") + raise Exception( + f"Connector {self.metadata.name} is not available - venv {venv_name} does not exist" + ) self.install() connector_path = self._get_connector_path() if not connector_path.exists(): raise FileNotFoundError( - f"Could not find connector '{self.metadata.name}' " f"in venv '{venv_name}' with connector path '{connector_path}'." + f"Could not find connector '{self.metadata.name}' in venv '{venv_name}' with connector path '{connector_path}'.", ) if self.enforce_version: @@ -176,10 +180,10 @@ def ensure_installation( version_after_install = self._get_installed_version() if version_after_install != self.target_version: raise Exception( - f"Failed to install connector {self.metadata.name} version {self.target_version}. Installed version is {version_after_install}" + f"Failed to install connector {self.metadata.name} version {self.target_version}. Installed version is {version_after_install}", ) - def execute(self, args: List[str]) -> Iterable[str]: + def execute(self, args: list[str]) -> Iterator[str]: connector_path = self._get_connector_path() with _stream_from_subprocess([str(connector_path)] + args) as stream: @@ -187,18 +191,22 @@ def execute(self, args: List[str]) -> Iterable[str]: class PathExecutor(Executor): - def ensure_installation(self): + def ensure_installation(self) -> None: try: self.execute(["spec"]) except Exception as e: - raise Exception(f"Connector {self.metadata.name} is not available - executing it failed: {e}") + raise Exception( + f"Connector {self.metadata.name} is not available - executing it failed: {e}" + ) - def install(self): + def install(self) -> NoReturn: raise Exception(f"Connector {self.metadata.name} is not available - cannot install it") - def uninstall(self): - raise Exception(f"Connector {self.metadata.name} is installed manually and not managed by airbyte-lib - please remove it manually") + def uninstall(self) -> NoReturn: + raise Exception( + f"Connector {self.metadata.name} is installed manually and not managed by airbyte-lib - please remove it manually" + ) - def execute(self, args: List[str]) -> Iterable[str]: + def execute(self, args: list[str]) -> Iterator[str]: with _stream_from_subprocess([self.metadata.name] + args) as stream: yield from stream diff --git a/airbyte-lib/airbyte_lib/_factories/__init__.py b/airbyte-lib/airbyte_lib/_factories/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/airbyte-lib/airbyte_lib/_factories/cache_factories.py b/airbyte-lib/airbyte_lib/_factories/cache_factories.py new file mode 100644 index 0000000000000..ea863b7bdb00b --- /dev/null +++ b/airbyte-lib/airbyte_lib/_factories/cache_factories.py @@ -0,0 +1,58 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + + +from pathlib import Path + +import ulid + +from airbyte_lib.caches.duckdb import DuckDBCache, DuckDBCacheConfig + + +def get_default_cache() -> DuckDBCache: + """Get a local cache for storing data, using the default database path. + + Cache files are stored in the `.cache` directory, relative to the current + working directory. + """ + config = DuckDBCacheConfig( + db_path="./.cache/default_cache_db.duckdb", + ) + return DuckDBCache(config=config) + + +def new_local_cache( + cache_name: str | None = None, + cache_dir: str | Path | None = None, + cleanup: bool = True, +) -> DuckDBCache: + """Get a local cache for storing data, using a name string to seed the path. + + Args: + cache_name: Name to use for the cache. Defaults to None. + root_dir: Root directory to store the cache in. Defaults to None. + cleanup: Whether to clean up temporary files. Defaults to True. + + Cache files are stored in the `.cache` directory, relative to the current + working directory. + """ + if cache_name: + if " " in cache_name: + raise ValueError(f"Cache name '{cache_name}' cannot contain spaces") + + if not cache_name.replace("_", "").isalnum(): + raise ValueError( + f"Cache name '{cache_name}' can only contain alphanumeric " + "characters and underscores." + ) + + cache_name = cache_name or str(ulid.ULID()) + cache_dir = cache_dir or Path(f"./.cache/{cache_name}") + if not isinstance(cache_dir, Path): + cache_dir = Path(cache_dir) + + config = DuckDBCacheConfig( + db_path=cache_dir / f"db_{cache_name}.duckdb", + cache_dir=cache_dir, + cleanup=cleanup, + ) + return DuckDBCache(config=config) diff --git a/airbyte-lib/airbyte_lib/factories.py b/airbyte-lib/airbyte_lib/_factories/connector_factories.py similarity index 92% rename from airbyte-lib/airbyte_lib/factories.py rename to airbyte-lib/airbyte_lib/_factories/connector_factories.py index adb9e6388dd8e..06482d67aa1c7 100644 --- a/airbyte-lib/airbyte_lib/factories.py +++ b/airbyte-lib/airbyte_lib/_factories/connector_factories.py @@ -3,16 +3,11 @@ from typing import Any -from airbyte_lib.cache import InMemoryCache -from airbyte_lib.executor import Executor, PathExecutor, VenvExecutor +from airbyte_lib._executor import Executor, PathExecutor, VenvExecutor from airbyte_lib.registry import get_connector_metadata from airbyte_lib.source import Source -def get_in_memory_cache(): - return InMemoryCache() - - def get_connector( name: str, version: str | None = None, @@ -20,7 +15,7 @@ def get_connector( config: dict[str, Any] | None = None, use_local_install: bool = False, install_if_missing: bool = True, -): +) -> Source: """ Get a connector by name and version. :param name: connector name diff --git a/airbyte-lib/airbyte_lib/_file_writers/__init__.py b/airbyte-lib/airbyte_lib/_file_writers/__init__.py new file mode 100644 index 0000000000000..007dde8324345 --- /dev/null +++ b/airbyte-lib/airbyte_lib/_file_writers/__init__.py @@ -0,0 +1,11 @@ +from .base import FileWriterBase, FileWriterBatchHandle, FileWriterConfigBase +from .parquet import ParquetWriter, ParquetWriterConfig + + +__all__ = [ + "FileWriterBatchHandle", + "FileWriterBase", + "FileWriterConfigBase", + "ParquetWriter", + "ParquetWriterConfig", +] diff --git a/airbyte-lib/airbyte_lib/_file_writers/base.py b/airbyte-lib/airbyte_lib/_file_writers/base.py new file mode 100644 index 0000000000000..a4913f0f7bb30 --- /dev/null +++ b/airbyte-lib/airbyte_lib/_file_writers/base.py @@ -0,0 +1,112 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +"""Define abstract base class for File Writers, which write and read from file storage.""" + +from __future__ import annotations + +import abc +from dataclasses import dataclass, field +from pathlib import Path +from typing import TYPE_CHECKING, cast, final + +from overrides import overrides + +from airbyte_lib._processors import BatchHandle, RecordProcessor +from airbyte_lib.config import CacheConfigBase + + +if TYPE_CHECKING: + import pyarrow as pa + + +DEFAULT_BATCH_SIZE = 10000 + + +# The batch handle for file writers is a list of Path objects. +@dataclass +class FileWriterBatchHandle(BatchHandle): + """The file writer batch handle is a list of Path objects.""" + + files: list[Path] = field(default_factory=list) + + +class FileWriterConfigBase(CacheConfigBase): + """Configuration for the Snowflake cache.""" + + cache_dir: Path = Path("./.cache/files/") + """The directory to store cache files in.""" + cleanup: bool = True + """Whether to clean up temporary files after processing a batch.""" + + +class FileWriterBase(RecordProcessor, abc.ABC): + """A generic base implementation for a file-based cache.""" + + config_class = FileWriterConfigBase + config: FileWriterConfigBase + + @abc.abstractmethod + @overrides + def _write_batch( + self, + stream_name: str, + batch_id: str, + record_batch: pa.Table | pa.RecordBatch, + ) -> FileWriterBatchHandle: + """ + Process a record batch. + + Return a list of paths to one or more cache files. + """ + ... + + @final + def write_batch( + self, + stream_name: str, + batch_id: str, + record_batch: pa.Table | pa.RecordBatch, + ) -> FileWriterBatchHandle: + """Write a batch of records to the cache. + + This method is final because it should not be overridden. + + Subclasses should override `_write_batch` instead. + """ + return self._write_batch(stream_name, batch_id, record_batch) + + @overrides + def _cleanup_batch( + self, + stream_name: str, + batch_id: str, + batch_handle: BatchHandle, + ) -> None: + """Clean up the cache. + + For file writers, this means deleting the files created and declared in the batch. + + This method is a no-op if the `cleanup` config option is set to False. + """ + if self.config.cleanup: + batch_handle = cast(FileWriterBatchHandle, batch_handle) + _ = stream_name, batch_id + for file_path in batch_handle.files: + file_path.unlink() + + @final + def cleanup_batch( + self, + stream_name: str, + batch_id: str, + batch_handle: BatchHandle, + ) -> None: + """Clean up the cache. + + For file writers, this means deleting the files created and declared in the batch. + + This method is final because it should not be overridden. + + Subclasses should override `_cleanup_batch` instead. + """ + self._cleanup_batch(stream_name, batch_id, batch_handle) diff --git a/airbyte-lib/airbyte_lib/_file_writers/parquet.py b/airbyte-lib/airbyte_lib/_file_writers/parquet.py new file mode 100644 index 0000000000000..201fb4952eefa --- /dev/null +++ b/airbyte-lib/airbyte_lib/_file_writers/parquet.py @@ -0,0 +1,59 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +"""A Parquet cache implementation.""" + +from pathlib import Path +from typing import cast + +import pyarrow as pa +import ulid +from overrides import overrides +from pyarrow import parquet + +from .base import FileWriterBase, FileWriterBatchHandle, FileWriterConfigBase + + +class ParquetWriterConfig(FileWriterConfigBase): + """Configuration for the Snowflake cache.""" + + # Inherits from base class: + # cache_dir: str | Path + + +class ParquetWriter(FileWriterBase): + """A Parquet cache implementation.""" + + config_class = ParquetWriterConfig + + def get_new_cache_file_path( + self, + stream_name: str, + batch_id: str | None = None, # ULID of the batch + ) -> Path: + """Return a new cache file path for the given stream.""" + batch_id = batch_id or str(ulid.ULID()) + config: ParquetWriterConfig = cast(ParquetWriterConfig, self.config) + target_dir = Path(config.cache_dir) + target_dir.mkdir(parents=True, exist_ok=True) + return target_dir / f"{stream_name}_{batch_id}.parquet" + + @overrides + def _write_batch( + self, + stream_name: str, + batch_id: str, + record_batch: pa.Table | pa.RecordBatch, + ) -> FileWriterBatchHandle: + """ + Process a record batch. + + Return the path to the cache file. + """ + output_file_path = self.get_new_cache_file_path(stream_name) + + with parquet.ParquetWriter(output_file_path, record_batch.schema) as writer: + writer.write_table(cast(pa.Table, record_batch)) + + batch_handle = FileWriterBatchHandle() + batch_handle.files.append(output_file_path) + return batch_handle diff --git a/airbyte-lib/airbyte_lib/_processors.py b/airbyte-lib/airbyte_lib/_processors.py new file mode 100644 index 0000000000000..f105bc2488645 --- /dev/null +++ b/airbyte-lib/airbyte_lib/_processors.py @@ -0,0 +1,312 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +"""Define abstract base class for Processors, including Caches and File writers. + +Processors can all take input from STDIN or a stream of Airbyte messages. + +Caches will pass their input to the File Writer. They share a common base class so certain +abstractions like "write" and "finalize" can be handled in either layer, or both. +""" + +from __future__ import annotations + +import abc +import contextlib +import io +import sys +from collections import defaultdict +from typing import TYPE_CHECKING, Any, cast, final + +import pyarrow as pa +import ulid + +from airbyte_protocol.models import ( + AirbyteMessage, + AirbyteRecordMessage, + AirbyteStateMessage, + AirbyteStateType, + AirbyteStreamState, + ConfiguredAirbyteCatalog, + Type, +) + +from airbyte_lib._util import protocol_util # Internal utility functions + + +if TYPE_CHECKING: + from collections.abc import Generator, Iterable, Iterator + + from airbyte_lib.config import CacheConfigBase + + +DEFAULT_BATCH_SIZE = 10000 + + +class BatchHandle: + pass + + +class AirbyteMessageParsingError(Exception): + """Raised when an Airbyte message is invalid or cannot be parsed.""" + + +class RecordProcessor(abc.ABC): + """Abstract base class for classes which can process input records.""" + + config_class: type[CacheConfigBase] + skip_finalize_step: bool = False + + def __init__( + self, + config: CacheConfigBase | dict | None, + ) -> None: + if isinstance(config, dict): + config = self.config_class(**config) + + self.config = config or self.config_class() + if not isinstance(self.config, self.config_class): + err_msg = ( + f"Expected config class of type '{self.config_class.__name__}'. " + f"Instead found '{type(self.config).__name__}'." + ) + raise TypeError(err_msg) + + self.source_catalog: ConfiguredAirbyteCatalog | None = None + + self._pending_batches: dict[str, dict[str, Any]] = defaultdict(lambda: {}, {}) + self._finalized_batches: dict[str, dict[str, Any]] = defaultdict(lambda: {}, {}) + + self._pending_state_messages: dict[str, list[AirbyteStateMessage]] = defaultdict(list, {}) + self._finalized_state_messages: dict[ + str, + list[AirbyteStateMessage], + ] = defaultdict(list, {}) + + self._setup() + + def register_source( + self, + source_name: str, + source_catalog: ConfiguredAirbyteCatalog, + ) -> None: + """Register the source name and catalog. + + For now, only one source at a time is supported. + If this method is called multiple times, the last call will overwrite the previous one. + + TODO: Expand this to handle mutliple sources. + """ + _ = source_name + self.source_catalog = source_catalog + + @final + def process_stdin( + self, + max_batch_size: int = DEFAULT_BATCH_SIZE, + ) -> None: + """ + Process the input stream from stdin. + + Return a list of summaries for testing. + """ + input_stream = io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8") + self.process_input_stream(input_stream, max_batch_size) + + @final + def _airbyte_messages_from_buffer( + self, + buffer: io.TextIOBase, + ) -> Iterator[AirbyteMessage]: + """Yield messages from a buffer.""" + yield from (AirbyteMessage.parse_raw(line) for line in buffer) + + @final + def process_input_stream( + self, + input_stream: io.TextIOBase, + max_batch_size: int = DEFAULT_BATCH_SIZE, + ) -> None: + """ + Parse the input stream and process data in batches. + + Return a list of summaries for testing. + """ + messages = self._airbyte_messages_from_buffer(input_stream) + self.process_airbyte_messages(messages, max_batch_size) + + @final + def process_airbyte_messages( + self, + messages: Iterable[AirbyteMessage], + max_batch_size: int = DEFAULT_BATCH_SIZE, + ) -> None: + stream_batches: dict[str, list[dict]] = defaultdict(list, {}) + + # Process messages, writing to batches as we go + for message in messages: + if message.type is Type.RECORD: + record_msg = cast(AirbyteRecordMessage, message.record) + stream_name = record_msg.stream + stream_batch = stream_batches[stream_name] + stream_batch.append(protocol_util.airbyte_record_message_to_dict(record_msg)) + + if len(stream_batch) >= max_batch_size: + record_batch = pa.Table.from_pylist(stream_batch) + self._process_batch(stream_name, record_batch) + stream_batch.clear() + + elif message.type is Type.STATE: + state_msg = cast(AirbyteStateMessage, message.state) + if state_msg.type in [AirbyteStateType.GLOBAL, AirbyteStateType.LEGACY]: + self._pending_state_messages[f"_{state_msg.type}"].append(state_msg) + else: + stream_state = cast(AirbyteStreamState, state_msg.stream) + stream_name = stream_state.stream_descriptor.name + self._pending_state_messages[stream_name].append(state_msg) + + elif message.type in [Type.LOG, Type.TRACE]: + pass + + else: + raise ValueError(f"Unexpected message type: {message.type}") + + # We are at the end of the stream. Process whatever else is queued. + for stream_name, batch in stream_batches.items(): + if batch: + record_batch = pa.Table.from_pylist(batch) + self._process_batch(stream_name, record_batch) + + # Finalize any pending batches + for stream_name in list(self._pending_batches.keys()): + self._finalize_batches(stream_name) + + @final + def _process_batch( + self, + stream_name: str, + record_batch: pa.Table, + ) -> tuple[str, Any, Exception | None]: + """Process a single batch. + + Returns a tuple of the batch ID, batch handle, and an exception if one occurred. + """ + batch_id = self._new_batch_id() + batch_handle = self._write_batch( + stream_name, + batch_id, + record_batch, + ) or self._get_batch_handle(stream_name, batch_id) + + if self.skip_finalize_step: + self._finalized_batches[stream_name][batch_id] = batch_handle + else: + self._pending_batches[stream_name][batch_id] = batch_handle + + return batch_id, batch_handle, None + + @abc.abstractmethod + def _write_batch( + self, + stream_name: str, + batch_id: str, + record_batch: pa.Table | pa.RecordBatch, + ) -> BatchHandle: + """Process a single batch. + + Returns a batch handle, such as a path or any other custom reference. + """ + + def _cleanup_batch( # noqa: B027 # Intentionally empty, not abstract + self, + stream_name: str, + batch_id: str, + batch_handle: BatchHandle, + ) -> None: + """Clean up the cache. + + This method is called after the given batch has been finalized. + + For instance, file writers can override this method to delete the files created. Caches, + similarly, can override this method to delete any other temporary artifacts. + """ + pass # noqa: PIE790 # Intentional no-op + + def _new_batch_id(self) -> str: + """Return a new batch handle.""" + return str(ulid.ULID()) + + def _get_batch_handle( + self, + stream_name: str, + batch_id: str | None = None, # ULID of the batch + ) -> str: + """Return a new batch handle. + + By default this is a concatenation of the stream name and batch ID. + However, any Python object can be returned, such as a Path object. + """ + batch_id = batch_id or self._new_batch_id() + return f"{stream_name}_{batch_id}" + + def _finalize_batches(self, stream_name: str) -> dict[str, BatchHandle]: + """Finalize all uncommitted batches. + + Returns a mapping of batch IDs to batch handles, for processed batches. + + This is a generic implementation, which can be overridden. + """ + with self._finalizing_batches(stream_name) as batches_to_finalize: + if batches_to_finalize and not self.skip_finalize_step: + raise NotImplementedError( + "Caches need to be finalized but no _finalize_batch() method " + f"exists for class {self.__class__.__name__}", + ) + + return batches_to_finalize + + @final + @contextlib.contextmanager + def _finalizing_batches( + self, + stream_name: str, + ) -> Generator[dict[str, BatchHandle], str, None]: + """Context manager to use for finalizing batches, if applicable. + + Returns a mapping of batch IDs to batch handles, for those processed batches. + """ + batches_to_finalize = self._pending_batches[stream_name].copy() + state_messages_to_finalize = self._pending_state_messages[stream_name].copy() + self._pending_batches[stream_name].clear() + self._pending_state_messages[stream_name].clear() + yield batches_to_finalize + + self._finalized_batches[stream_name].update(batches_to_finalize) + self._finalized_state_messages[stream_name] += state_messages_to_finalize + + for batch_id, batch_handle in batches_to_finalize.items(): + self._cleanup_batch(stream_name, batch_id, batch_handle) + + def _setup(self) -> None: # noqa: B027 # Intentionally empty, not abstract + """Create the database. + + By default this is a no-op but subclasses can override this method to prepare + any necessary resources. + """ + + def _teardown(self) -> None: + """Teardown the processor resources. + + By default, the base implementation simply calls _cleanup_batch() for all pending batches. + """ + for stream_name, pending_batches in self._pending_batches.items(): + for batch_id, batch_handle in pending_batches.items(): + self._cleanup_batch( + stream_name=stream_name, + batch_id=batch_id, + batch_handle=batch_handle, + ) + + @final + def __del__(self) -> None: + """Teardown temporary resources when instance is unloaded from memory.""" + self._teardown() diff --git a/airbyte-lib/airbyte_lib/_util/__init__.py b/airbyte-lib/airbyte_lib/_util/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/airbyte-lib/airbyte_lib/_util/protocol_util.py b/airbyte-lib/airbyte_lib/_util/protocol_util.py new file mode 100644 index 0000000000000..56b28b2c628a1 --- /dev/null +++ b/airbyte-lib/airbyte_lib/_util/protocol_util.py @@ -0,0 +1,67 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +"""Internal utility functions, especially for dealing with Airbyte Protocol.""" + +from collections.abc import Iterable, Iterator +from typing import Any, cast + +from airbyte_protocol.models import ( + AirbyteMessage, + AirbyteRecordMessage, + ConfiguredAirbyteCatalog, + Type, +) + + +def airbyte_messages_to_record_dicts( + messages: Iterable[AirbyteMessage], +) -> Iterator[dict[str, Any]]: + """Convert an AirbyteMessage to a dictionary.""" + yield from ( + cast(dict[str, Any], airbyte_message_to_record_dict(message)) + for message in messages + if message is not None + ) + + +def airbyte_message_to_record_dict(message: AirbyteMessage) -> dict[str, Any] | None: + """Convert an AirbyteMessage to a dictionary. + + Return None if the message is not a record message. + """ + if message.type != Type.RECORD: + return None + + return airbyte_record_message_to_dict(message.record) + + +def airbyte_record_message_to_dict( + record_message: AirbyteRecordMessage, +) -> dict[str, Any]: + """Convert an AirbyteMessage to a dictionary. + + Return None if the message is not a record message. + """ + result = record_message.data + + # TODO: Add the metadata columns (this breaks tests) + # result["_airbyte_extracted_at"] = datetime.datetime.fromtimestamp( + # record_message.emitted_at + # ) + + return result # noqa: RET504 # unnecessary assignment and then return (see TODO above) + + +def get_primary_keys_from_stream( + stream_name: str, + configured_catalog: ConfiguredAirbyteCatalog, +) -> set[str]: + """Get the primary keys from a stream in the configured catalog.""" + stream = next( + (stream for stream in configured_catalog.streams if stream.stream.name == stream_name), + None, + ) + if stream is None: + raise ValueError(f"Stream {stream_name} not found in catalog.") + + return set(stream.stream.source_defined_primary_key or []) diff --git a/airbyte-lib/airbyte_lib/cache.py b/airbyte-lib/airbyte_lib/cache.py deleted file mode 100644 index 10d443cc81dea..0000000000000 --- a/airbyte-lib/airbyte_lib/cache.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) 2023 Airbyte, Inc., all rights reserved. - - -from abc import ABC, abstractmethod -from typing import Any, Dict, Iterable, List - -from airbyte_protocol.models import AirbyteRecordMessage - - -class Cache(ABC): - @abstractmethod - def write(self, messages: Iterable[AirbyteRecordMessage]): - pass - - @abstractmethod - def get_iterable(self, stream: str) -> Iterable[Dict[str, Any]]: - pass - - @abstractmethod - def get_pandas(self, stream: str) -> Any: - pass - - @abstractmethod - def get_sql_table(self, stream: str) -> Any: - pass - - @abstractmethod - def get_sql_engine(self) -> Any: - pass - - -class InMemoryCache(Cache): - """The in-memory cache is accepting airbyte messages and stores them in a dictionary for streams (one list of dicts per stream).""" - - def __init__(self) -> None: - self.streams: Dict[str, List[Dict[str, Any]]] = {} - - def write(self, messages: Iterable[AirbyteRecordMessage]) -> None: - for message in messages: - if message.stream not in self.streams: - self.streams[message.stream] = [] - self.streams[message.stream].append(message.data) - - def get_iterable(self, stream: str) -> Iterable[Dict[str, Any]]: - return iter(self.streams[stream]) - - def get_pandas(self, stream: str) -> Any: - raise NotImplementedError() - - def get_sql_table(self, stream: str) -> Any: - raise NotImplementedError() - - def get_sql_engine(self) -> Any: - raise NotImplementedError() diff --git a/airbyte-lib/airbyte_lib/caches/__init__.py b/airbyte-lib/airbyte_lib/caches/__init__.py new file mode 100644 index 0000000000000..39680027d02a1 --- /dev/null +++ b/airbyte-lib/airbyte_lib/caches/__init__.py @@ -0,0 +1,15 @@ +"""Base module for all caches.""" + +from airbyte_lib.caches.base import SQLCacheBase +from airbyte_lib.caches.duckdb import DuckDBCache, DuckDBCacheConfig +from airbyte_lib.caches.postgres import PostgresCache, PostgresCacheConfig + + +# We export these classes for easy access: `airbyte_lib.caches...` +__all__ = [ + "DuckDBCache", + "DuckDBCacheConfig", + "PostgresCache", + "PostgresCacheConfig", + "SQLCacheBase", +] diff --git a/airbyte-lib/airbyte_lib/caches/base.py b/airbyte-lib/airbyte_lib/caches/base.py new file mode 100644 index 0000000000000..298b3856e6317 --- /dev/null +++ b/airbyte-lib/airbyte_lib/caches/base.py @@ -0,0 +1,738 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +"""A SQL Cache implementation.""" + +import abc +import enum +from collections.abc import Generator, Iterator, Mapping +from contextlib import contextmanager +from functools import cached_property, lru_cache +from pathlib import Path +from typing import TYPE_CHECKING, Any, cast, final + +import pandas as pd +import pyarrow as pa +import sqlalchemy +import ulid +from overrides import overrides +from sqlalchemy import CursorResult, Executable, TextClause, create_engine, text +from sqlalchemy.engine import Engine +from sqlalchemy.pool import StaticPool + +from airbyte_protocol.models import ConfiguredAirbyteStream + +from airbyte_lib._file_writers.base import FileWriterBase, FileWriterBatchHandle +from airbyte_lib._processors import BatchHandle, RecordProcessor +from airbyte_lib.config import CacheConfigBase +from airbyte_lib.types import SQLTypeConverter + + +if TYPE_CHECKING: + from sqlalchemy.engine import Connection + from sqlalchemy.engine.reflection import Inspector + + from airbyte_lib.datasets._base import DatasetBase + + +DEBUG_MODE = False # Set to True to enable additional debug logging. + + +class RecordDedupeMode(enum.Enum): + APPEND = "append" + REPLACE = "replace" + + +class SQLRuntimeError(Exception): + """Raised when an SQL operation fails.""" + + +class SQLCacheConfigBase(CacheConfigBase): + """Same as a regular config except it exposes the 'get_sql_alchemy_url()' method.""" + + dedupe_mode = RecordDedupeMode.REPLACE + schema_name: str = "airbyte_raw" + + table_prefix: str | None = None + """ A prefix to add to all table names. + If 'None', a prefix will be created based on the source name. + """ + + table_suffix: str = "" + """A suffix to add to all table names.""" + + @abc.abstractmethod + def get_sql_alchemy_url(self) -> str: + """Returns a SQL Alchemy URL.""" + ... + + @abc.abstractmethod + def get_database_name(self) -> str: + """Return the name of the database.""" + ... + + +class GenericSQLCacheConfig(SQLCacheConfigBase): + """Allows configuring 'sql_alchemy_url' directly.""" + + sql_alchemy_url: str + + @overrides + def get_sql_alchemy_url(self) -> str: + """Returns a SQL Alchemy URL.""" + return self.sql_alchemy_url + + +class SQLCacheBase(RecordProcessor): + """A base class to be used for SQL Caches. + + Optionally we can use a file cache to store the data in parquet files. + """ + + type_converter_class: type[SQLTypeConverter] = SQLTypeConverter + config_class: type[SQLCacheConfigBase] + file_writer_class: type[FileWriterBase] + + supports_merge_insert = False + use_singleton_connection = False # If true, the same connection is used for all operations. + + # Constructor: + + @final # We don't want subclasses to have to override the constructor. + def __init__( + self, + config: SQLCacheConfigBase | None = None, + file_writer: FileWriterBase | None = None, + **kwargs: dict[str, Any], # Added for future proofing purposes. + ) -> None: + self.config: SQLCacheConfigBase + self._engine: Engine | None = None + self._connection_to_reuse: Connection | None = None + super().__init__(config, **kwargs) + self._ensure_schema_exists() + + self.file_writer = file_writer or self.file_writer_class(config) + self.type_converter = self.type_converter_class() + + # Public interface: + + def get_sql_alchemy_url(self) -> str: + """Return the SQLAlchemy URL to use.""" + return self.config.get_sql_alchemy_url() + + @final + @cached_property + def database_name(self) -> str: + """Return the name of the database.""" + return self.config.get_database_name() + + @final + def get_sql_engine(self) -> Engine: + """Return a new SQL engine to use.""" + if self._engine: + return self._engine + + sql_alchemy_url = self.get_sql_alchemy_url() + if self.use_singleton_connection: + if self._connection_to_reuse is None: + # This temporary bootstrap engine will be created once and is needed to + # create the long-lived connection object. + bootstrap_engine = create_engine( + sql_alchemy_url, + ) + self._connection_to_reuse = bootstrap_engine.connect() + + self._engine = create_engine( + sql_alchemy_url, + creator=lambda: self._connection_to_reuse, + poolclass=StaticPool, + echo=DEBUG_MODE, + # isolation_level="AUTOCOMMIT", + ) + else: + # Regular engine creation for new connections + self._engine = create_engine( + sql_alchemy_url, + echo=DEBUG_MODE, + # isolation_level="AUTOCOMMIT", + ) + + return self._engine + + @contextmanager + def get_sql_connection(self) -> Generator[sqlalchemy.engine.Connection, None, None]: + """A context manager which returns a new SQL connection for running queries. + + If the connection needs to close, it will be closed automatically. + """ + if self.use_singleton_connection and self._connection_to_reuse is not None: + connection = self._connection_to_reuse + yield connection + + else: + with self.get_sql_engine().begin() as connection: + yield connection + + if not self.use_singleton_connection: + connection.close() + del connection + + def get_sql_table_name( + self, + stream_name: str, + ) -> str: + """Return the name of the SQL table for the given stream.""" + table_prefix = self.config.table_prefix or "" + + # TODO: Add default prefix based on the source name. + + return self._normalize_table_name( + f"{table_prefix}{stream_name}{self.config.table_suffix}", + ) + + @final + def get_sql_table( + self, + stream_name: str, + ) -> sqlalchemy.Table: + """Return a temporary table name.""" + table_name = self.get_sql_table_name(stream_name) + return sqlalchemy.Table( + table_name, + sqlalchemy.MetaData(schema=self.config.schema_name), + autoload_with=self.get_sql_engine(), + ) + + @final + @property + def streams( + self, + ) -> dict[str, "DatasetBase"]: + """Return a temporary table name.""" + # TODO: Add support for streams map, based on the cached catalog. + raise NotImplementedError("Streams map is not yet supported.") + + # Read methods: + + def get_records( + self, + stream_name: str, + ) -> Iterator[Mapping[str, Any]]: + """Uses SQLAlchemy to select all rows from the table. + + # TODO: Refactor to return a LazyDataset here. + """ + table_ref = self.get_sql_table(stream_name) + stmt = table_ref.select() + with self.get_sql_connection() as conn: + for row in conn.execute(stmt): + # Access to private member required because SQLAlchemy doesn't expose a public API. + # https://pydoc.dev/sqlalchemy/latest/sqlalchemy.engine.row.RowMapping.html + yield cast(Mapping[str, Any], row._mapping) # noqa: SLF001 + + def get_pandas_dataframe( + self, + stream_name: str, + ) -> pd.DataFrame: + """Return a Pandas data frame with the stream's data.""" + table_name = self.get_sql_table_name(stream_name) + engine = self.get_sql_engine() + return pd.read_sql_table(table_name, engine) + + # Protected members (non-public interface): + + def _ensure_schema_exists( + self, + ) -> None: + """Return a new (unique) temporary table name.""" + schema_name = self.config.schema_name + if schema_name in self._get_schemas_list(): + return + + sql = f"CREATE SCHEMA IF NOT EXISTS {schema_name}" + + try: + self._execute_sql(sql) + except Exception as ex: # noqa: BLE001 # Too-wide catch because we don't know what the DB will throw. + # Ignore schema exists errors. + if "already exists" not in str(ex): + raise + + if DEBUG_MODE: + found_schemas = self._get_schemas_list() + assert ( + schema_name in found_schemas + ), f"Schema {schema_name} was not created. Found: {found_schemas}" + + @final + def _get_temp_table_name( + self, + stream_name: str, + batch_id: str | None = None, # ULID of the batch + ) -> str: + """Return a new (unique) temporary table name.""" + batch_id = batch_id or str(ulid.ULID()) + return self._normalize_table_name(f"{stream_name}_{batch_id}") + + def _fully_qualified( + self, + table_name: str, + ) -> str: + """Return the fully qualified name of the given table.""" + # return f"{self.database_name}.{self.config.schema_name}.{table_name}" + return f"{self.config.schema_name}.{table_name}" + + @final + def _create_table_for_loading( + self, + /, + stream_name: str, + batch_id: str, + ) -> str: + """Create a new table for loading data.""" + temp_table_name = self._get_temp_table_name(stream_name, batch_id) + column_definition_str = ",\n ".join( + f"{column_name} {sql_type}" + for column_name, sql_type in self._get_sql_column_definitions(stream_name).items() + ) + self._create_table(temp_table_name, column_definition_str) + + return temp_table_name + + def _get_tables_list( + self, + ) -> list[str]: + """Return a list of all tables in the database.""" + with self.get_sql_connection() as conn: + inspector: Inspector = sqlalchemy.inspect(conn) + return inspector.get_table_names(schema=self.config.schema_name) + + def _get_schemas_list( + self, + database_name: str | None = None, + ) -> list[str]: + """Return a list of all tables in the database.""" + inspector: Inspector = sqlalchemy.inspect(self.get_sql_engine()) + database_name = database_name or self.database_name + found_schemas = inspector.get_schema_names() + return [ + found_schema.split(".")[-1].strip('"') + for found_schema in found_schemas + if "." not in found_schema + or (found_schema.split(".")[0].lower().strip('"') == database_name.lower()) + ] + + def _ensure_final_table_exists( + self, + stream_name: str, + create_if_missing: bool = True, + ) -> str: + """ + Create the final table if it doesn't already exist. + + Return the table name. + """ + table_name = self.get_sql_table_name(stream_name) + did_exist = self._table_exists(table_name) + if not did_exist and create_if_missing: + column_definition_str = ",\n ".join( + f"{column_name} {sql_type}" + for column_name, sql_type in self._get_sql_column_definitions( + stream_name, + ).items() + ) + self._create_table(table_name, column_definition_str) + + return table_name + + def _ensure_compatible_table_schema( + self, + stream_name: str, + table_name: str, + raise_on_error: bool = False, + ) -> bool: + """Return true if the given table is compatible with the stream's schema. + + If raise_on_error is true, raise an exception if the table is not compatible. + + TODO: Expand this to check for column types and sizes, and to add missing columns. + + Returns true if the table is compatible, false if it is not. + """ + json_schema = self._get_stream_json_schema(stream_name) + stream_column_names: list[str] = json_schema["properties"].keys() + table_column_names: list[str] = self.get_sql_table(table_name).columns.keys() + + missing_columns: set[str] = set(stream_column_names) - set(table_column_names) + if missing_columns: + if raise_on_error: + raise RuntimeError( + f"Table {table_name} is missing columns: {missing_columns}", + ) + return False # Some columns are missing. + + return True # All columns exist. + + @final + def _create_table( + self, + table_name: str, + column_definition_str: str, + ) -> None: + if DEBUG_MODE: + assert table_name not in self._get_tables_list(), f"Table {table_name} already exists." + + cmd = f""" + CREATE TABLE {self._fully_qualified(table_name)} ( + {column_definition_str} + ) + """ + _ = self._execute_sql(cmd) + if DEBUG_MODE: + tables_list = self._get_tables_list() + assert ( + table_name in tables_list + ), f"Table {table_name} was not created. Found: {tables_list}" + + def _normalize_column_name( + self, + raw_name: str, + ) -> str: + return raw_name.lower().replace(" ", "_").replace("-", "_") + + def _normalize_table_name( + self, + raw_name: str, + ) -> str: + return raw_name.lower().replace(" ", "_").replace("-", "_") + + @final + def _get_sql_column_definitions( + self, + stream_name: str, + ) -> dict[str, sqlalchemy.types.TypeEngine]: + """Return the column definitions for the given stream.""" + columns: dict[str, sqlalchemy.types.TypeEngine] = {} + properties = self._get_stream_json_schema(stream_name)["properties"] + for property_name, json_schema_property_def in properties.items(): + clean_prop_name = self._normalize_column_name(property_name) + columns[clean_prop_name] = self.type_converter.to_sql_type( + json_schema_property_def, + ) + + # TODO: Add the metadata columns (this breaks tests) + # columns["_airbyte_extracted_at"] = sqlalchemy.TIMESTAMP() + # columns["_airbyte_loaded_at"] = sqlalchemy.TIMESTAMP() + return columns + + @final + def _get_stream_config( + self, + stream_name: str, + ) -> ConfiguredAirbyteStream: + """Return the column definitions for the given stream.""" + if not self.source_catalog: + raise RuntimeError("Cannot get stream JSON schema without a catalog.") + + matching_streams: list[ConfiguredAirbyteStream] = [ + stream for stream in self.source_catalog.streams if stream.stream.name == stream_name + ] + if not matching_streams: + raise RuntimeError(f"Stream '{stream_name}' not found in catalog.") + + if len(matching_streams) > 1: + raise RuntimeError(f"Multiple streams found with name '{stream_name}'.") + + return matching_streams[0] + + @final + def _get_stream_json_schema( + self, + stream_name: str, + ) -> dict[str, Any]: + """Return the column definitions for the given stream.""" + return self._get_stream_config(stream_name).stream.json_schema + + @overrides + def _write_batch( + self, + stream_name: str, + batch_id: str, + record_batch: pa.Table | pa.RecordBatch, + ) -> FileWriterBatchHandle: + """ + Process a record batch. + + Return the path to the cache file. + """ + return self.file_writer.write_batch(stream_name, batch_id, record_batch) + + def _cleanup_batch( + self, + stream_name: str, + batch_id: str, + batch_handle: BatchHandle, + ) -> None: + """Clean up the cache. + + For SQL caches, we only need to call the cleanup operation on the file writer. + + Subclasses should call super() if they override this method. + """ + self.file_writer.cleanup_batch(stream_name, batch_id, batch_handle) + + @final + @overrides + def _finalize_batches(self, stream_name: str) -> dict[str, BatchHandle]: + """Finalize all uncommitted batches. + + This is a generic 'final' implementation, which should not be overridden. + + Returns a mapping of batch IDs to batch handles, for those processed batches. + + TODO: Add a dedupe step here to remove duplicates from the temp table. + Some sources will send us duplicate records within the same stream, + although this is a fairly rare edge case we can ignore in V1. + """ + with self._finalizing_batches(stream_name) as batches_to_finalize: + if not batches_to_finalize: + return {} + + files: list[Path] = [] + # Get a list of all files to finalize from all pending batches. + for batch_handle in batches_to_finalize.values(): + batch_handle = cast(FileWriterBatchHandle, batch_handle) + files += batch_handle.files + # Use the max batch ID as the batch ID for table names. + max_batch_id = max(batches_to_finalize.keys()) + + # Make sure the target schema and target table exist. + self._ensure_schema_exists() + final_table_name = self._ensure_final_table_exists( + stream_name, + create_if_missing=True, + ) + self._ensure_compatible_table_schema( + stream_name=stream_name, + table_name=final_table_name, + raise_on_error=True, + ) + + try: + temp_table_name = self._write_files_to_new_table( + files, + stream_name, + max_batch_id, + ) + self._write_temp_table_to_final_table( + stream_name, + temp_table_name, + final_table_name, + ) + finally: + self._drop_temp_table(temp_table_name, if_exists=True) + + # Return the batch handles as measure of work completed. + return batches_to_finalize + + def _execute_sql(self, sql: str | TextClause | Executable) -> CursorResult: + """Execute the given SQL statement.""" + if isinstance(sql, str): + sql = text(sql) + if isinstance(sql, TextClause): + sql = sql.execution_options( + autocommit=True, + ) + + with self.get_sql_connection() as conn: + try: + result = conn.execute(sql) + except ( + sqlalchemy.exc.ProgrammingError, + sqlalchemy.exc.SQLAlchemyError, + ) as ex: + msg = f"Error when executing SQL:\n{sql}\n{type(ex).__name__}{ex!s}" + raise SQLRuntimeError(msg) from None # from ex + + return result + + def _drop_temp_table( + self, + table_name: str, + if_exists: bool = True, + ) -> None: + """Drop the given table.""" + exists_str = "IF EXISTS" if if_exists else "" + self._execute_sql(f"DROP TABLE {exists_str} {self._fully_qualified(table_name)}") + + def _write_files_to_new_table( + self, + files: list[Path], + stream_name: str, + batch_id: str, + ) -> str: + """Write a file(s) to a new table. + + This is a generic implementation, which can be overridden by subclasses + to improve performance. + """ + temp_table_name = self._create_table_for_loading(stream_name, batch_id) + for file_path in files: + with pa.parquet.ParquetFile(file_path) as pf: + record_batch = pf.read() + dataframe = record_batch.to_pandas() + + # Pandas will auto-create the table if it doesn't exist, which we don't want. + if not self._table_exists(temp_table_name): + raise RuntimeError(f"Table {temp_table_name} does not exist after creation.") + + dataframe.to_sql( + temp_table_name, + self.get_sql_alchemy_url(), + schema=self.config.schema_name, + if_exists="append", + index=False, + dtype=self._get_sql_column_definitions(stream_name), # type: ignore + ) + return temp_table_name + + @final + def _write_temp_table_to_final_table( + self, + stream_name: str, + temp_table_name: str, + final_table_name: str, + ) -> None: + """Merge the temp table into the final table.""" + if self.config.dedupe_mode == RecordDedupeMode.REPLACE: + if not self.supports_merge_insert: + raise NotImplementedError( + "Deduping was requested but merge-insert is not yet supported.", + ) + + if not self._get_primary_keys(stream_name): + self._swap_temp_table_with_final_table( + stream_name, + temp_table_name, + final_table_name, + ) + else: + self._merge_temp_table_to_final_table( + stream_name, + temp_table_name, + final_table_name, + ) + + else: + self._append_temp_table_to_final_table( + stream_name=stream_name, + temp_table_name=temp_table_name, + final_table_name=final_table_name, + ) + + def _append_temp_table_to_final_table( + self, + temp_table_name: str, + final_table_name: str, + stream_name: str, + ) -> None: + nl = "\n" + columns = self._get_sql_column_definitions(stream_name).keys() + self._execute_sql( + f""" + INSERT INTO {self._fully_qualified(final_table_name)} ( + {f',{nl} '.join(columns)} + ) + SELECT + {f',{nl} '.join(columns)} + FROM {self._fully_qualified(temp_table_name)} + """, + ) + + @lru_cache + def _get_primary_keys( + self, + stream_name: str, + ) -> list[str]: + pks = self._get_stream_config(stream_name).primary_key + if not pks: + return [] + + joined_pks = [".".join(pk) for pk in pks] + for pk in joined_pks: + if "." in pk: + msg = "Nested primary keys are not yet supported. Found: {pk}" + raise NotImplementedError(msg) + + return joined_pks + + def _swap_temp_table_with_final_table( + self, + stream_name: str, + temp_table_name: str, + final_table_name: str, + ) -> None: + """Merge the temp table into the main one. + + This implementation requires MERGE support in the SQL DB. + Databases that do not support this syntax can override this method. + """ + if final_table_name is None: + raise ValueError("Arg 'final_table_name' cannot be None.") + if temp_table_name is None: + raise ValueError("Arg 'temp_table_name' cannot be None.") + + _ = stream_name + deletion_name = f"{final_table_name}_deleteme" + commands = [ + f"ALTER TABLE {final_table_name} RENAME TO {deletion_name}", + f"ALTER TABLE {temp_table_name} RENAME TO {final_table_name}", + f"DROP TABLE {deletion_name}", + ] + for cmd in commands: + self._execute_sql(cmd) + + def _merge_temp_table_to_final_table( + self, + stream_name: str, + temp_table_name: str, + final_table_name: str, + ) -> None: + """Merge the temp table into the main one. + + This implementation requires MERGE support in the SQL DB. + Databases that do not support this syntax can override this method. + """ + nl = "\n" + columns = self._get_sql_column_definitions(stream_name).keys() + pk_columns = self._get_primary_keys(stream_name) + non_pk_columns = columns - pk_columns + join_clause = "{nl} AND ".join(f"tmp.{pk_col} = final.{pk_col}" for pk_col in pk_columns) + set_clause = "{nl} ".join(f"{col} = tmp.{col}" for col in non_pk_columns) + self._execute_sql( + f""" + MERGE INTO {self._fully_qualified(final_table_name)} final + USING ( + SELECT * + FROM {self._fully_qualified(temp_table_name)} + ) AS tmp + ON {join_clause} + WHEN MATCHED THEN UPDATE + SET + {set_clause} + WHEN NOT MATCHED THEN INSERT + ( + {f',{nl} '.join(columns)} + ) + VALUES ( + tmp.{f',{nl} tmp.'.join(columns)} + ); + """, + ) + + @final + def _table_exists( + self, + table_name: str, + ) -> bool: + """Return true if the given table exists.""" + return table_name in self._get_tables_list() diff --git a/airbyte-lib/airbyte_lib/caches/duckdb.py b/airbyte-lib/airbyte_lib/caches/duckdb.py new file mode 100644 index 0000000000000..e3d74d58aeb79 --- /dev/null +++ b/airbyte-lib/airbyte_lib/caches/duckdb.py @@ -0,0 +1,147 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +"""A DuckDB implementation of the cache.""" + +from __future__ import annotations + +from pathlib import Path +from typing import cast + +from overrides import overrides + +from airbyte_lib._file_writers import ParquetWriter, ParquetWriterConfig +from airbyte_lib.caches.base import SQLCacheBase, SQLCacheConfigBase + + +class DuckDBCacheConfig(SQLCacheConfigBase, ParquetWriterConfig): + """Configuration for the DuckDB cache. + + Also inherits config from the ParquetWriter, which is responsible for writing files to disk. + """ + + db_path: Path | str + """Normally db_path is a Path object. + + There are some cases, such as when connecting to MotherDuck, where it could be a string that + is not also a path, such as "md:" to connect the user's default MotherDuck DB. + """ + schema_name: str = "main" + """The name of the schema to write to. Defaults to "main".""" + + @overrides + def get_sql_alchemy_url(self) -> str: + """Return the SQLAlchemy URL to use.""" + # return f"duckdb:///{self.db_path}?schema={self.schema_name}" + return f"duckdb:///{self.db_path!s}" + + def get_database_name(self) -> str: + """Return the name of the database.""" + if self.db_path == ":memory:": + return "memory" + + # Return the file name without the extension + return str(self.db_path).split("/")[-1].split(".")[0] + + +class DuckDBCacheBase(SQLCacheBase): + """A DuckDB implementation of the cache. + + Parquet is used for local file storage before bulk loading. + Unlike the Snowflake implementation, we can't use the COPY command to load data + so we insert as values instead. + """ + + config_class = DuckDBCacheConfig + supports_merge_insert = True + + @overrides + def _setup(self) -> None: + """Create the database parent folder if it doesn't yet exist.""" + config = cast(DuckDBCacheConfig, self.config) + + if config.db_path == ":memory:": + return + + Path(config.db_path).parent.mkdir(parents=True, exist_ok=True) + + +class DuckDBCache(DuckDBCacheBase): + """A DuckDB implementation of the cache. + + Parquet is used for local file storage before bulk loading. + Unlike the Snowflake implementation, we can't use the COPY command to load data + so we insert as values instead. + """ + + file_writer_class = ParquetWriter + + @overrides + def _merge_temp_table_to_final_table( + self, + stream_name: str, + temp_table_name: str, + final_table_name: str, + ) -> None: + """Merge the temp table into the main one. + + This implementation requires MERGE support in the SQL DB. + Databases that do not support this syntax can override this method. + """ + if not self._get_primary_keys(stream_name): + raise RuntimeError( + f"Primary keys not found for stream {stream_name}. " + "Cannot run merge updates without primary keys." + ) + + _ = stream_name + final_table = self._fully_qualified(final_table_name) + staging_table = self._fully_qualified(temp_table_name) + self._execute_sql( + # https://duckdb.org/docs/sql/statements/insert.html + # NOTE: This depends on primary keys being set properly in the final table. + f""" + INSERT OR REPLACE INTO {final_table} BY NAME + (SELECT * FROM {staging_table}) + """ + ) + + @overrides + def _ensure_compatible_table_schema( + self, + stream_name: str, + table_name: str, + raise_on_error: bool = True, + ) -> bool: + """Return true if the given table is compatible with the stream's schema. + + In addition to the base implementation, this also checks primary keys. + """ + # call super + if not super()._ensure_compatible_table_schema(stream_name, table_name, raise_on_error): + return False + + pk_cols = self._get_primary_keys(stream_name) + table = self.get_sql_table(table_name) + table_pk_cols = table.primary_key.columns.keys() + if set(pk_cols) != set(table_pk_cols): + if raise_on_error: + raise RuntimeError( + f"Primary keys do not match for table {table_name}. " + f"Expected: {pk_cols}. " + f"Found: {table_pk_cols}.", + ) + return False + + return True + + def _write_files_to_new_table( + self, + files: list[Path], + stream_name: str, + batch_id: str, + ) -> str: + """Write a file(s) to a new table. + + TODO: Optimize this for DuckDB instead of calling the base implementation. + """ + return super()._write_files_to_new_table(files, stream_name, batch_id) diff --git a/airbyte-lib/airbyte_lib/caches/postgres.py b/airbyte-lib/airbyte_lib/caches/postgres.py new file mode 100644 index 0000000000000..6cbbd6cc21256 --- /dev/null +++ b/airbyte-lib/airbyte_lib/caches/postgres.py @@ -0,0 +1,51 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +"""A Postgres implementation of the cache.""" + +from __future__ import annotations + +from overrides import overrides + +from airbyte_lib._file_writers import ParquetWriter, ParquetWriterConfig +from airbyte_lib.caches.base import SQLCacheBase, SQLCacheConfigBase + + +class PostgresCacheConfig(SQLCacheConfigBase, ParquetWriterConfig): + """Configuration for the Postgres cache. + + Also inherits config from the ParquetWriter, which is responsible for writing files to disk. + """ + + host: str + port: int + username: str + password: str + database: str + + # Already defined in base class: + # schema_name: str + + @overrides + def get_sql_alchemy_url(self) -> str: + """Return the SQLAlchemy URL to use.""" + return f"postgresql+psycopg://{self.username}:{self.password}@{self.host}:{self.port}/{self.database}" + + def get_database_name(self) -> str: + """Return the name of the database.""" + return self.database + + +class PostgresCache(SQLCacheBase): + """A Postgres implementation of the cache. + + Parquet is used for local file storage before bulk loading. + Unlike the Snowflake implementation, we can't use the COPY command to load data + so we insert as values instead. + + TOOD: Add optimized bulk load path for Postgres. Could use an alternate file writer + or another import method. (Relatively low priority, since for now it works fine as-is.) + """ + + config_class = PostgresCacheConfig + file_writer_class = ParquetWriter + supports_merge_insert = True diff --git a/airbyte-lib/airbyte_lib/caches/snowflake.py b/airbyte-lib/airbyte_lib/caches/snowflake.py new file mode 100644 index 0000000000000..8118c173f4a9e --- /dev/null +++ b/airbyte-lib/airbyte_lib/caches/snowflake.py @@ -0,0 +1,70 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +"""A Snowflake implementation of the cache. + +TODO: FIXME: Snowflake Cache doesn't work yet. It's a work in progress. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from overrides import overrides + +from airbyte_lib._file_writers import ParquetWriter, ParquetWriterConfig +from airbyte_lib.caches.base import SQLCacheBase, SQLCacheConfigBase + + +if TYPE_CHECKING: + from pathlib import Path + + +class SnowflakeCacheConfig(SQLCacheConfigBase, ParquetWriterConfig): + """Configuration for the Snowflake cache. + + Also inherits config from the ParquetWriter, which is responsible for writing files to disk. + """ + + account: str + username: str + password: str + warehouse: str + database: str + + # Already defined in base class: + # schema_name: str + + @overrides + def get_sql_alchemy_url(self) -> str: + """Return the SQLAlchemy URL to use.""" + return ( + f"snowflake://{self.username}:{self.password}@{self.account}/" + f"?warehouse={self.warehouse}&database={self.database}&schema={self.schema_name}" + ) + + def get_database_name(self) -> str: + """Return the name of the database.""" + return self.database + + +class SnowflakeSQLCache(SQLCacheBase): + """A Snowflake implementation of the cache. + + Parquet is used for local file storage before bulk loading. + """ + + config_class = SnowflakeCacheConfig + file_writer_class = ParquetWriter + + @overrides + def _write_files_to_new_table( + self, + files: list[Path], + stream_name: str, + batch_id: str, + ) -> str: + """Write a file(s) to a new table. + + TODO: Override the base implementation to use the COPY command. + """ + return super()._write_files_to_new_table(files, stream_name, batch_id) diff --git a/airbyte-lib/airbyte_lib/config.py b/airbyte-lib/airbyte_lib/config.py new file mode 100644 index 0000000000000..4401ca72f85f9 --- /dev/null +++ b/airbyte-lib/airbyte_lib/config.py @@ -0,0 +1,13 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +"""Define base Config interface, used by Caches and also File Writers (Processors).""" + +from __future__ import annotations + +from pydantic import BaseModel + + +class CacheConfigBase( + BaseModel +): # TODO: meta=EnforceOverrides (Pydantic doesn't like it currently) + pass diff --git a/airbyte-lib/airbyte_lib/datasets/__init__.py b/airbyte-lib/airbyte_lib/datasets/__init__.py new file mode 100644 index 0000000000000..862eee3e8baf1 --- /dev/null +++ b/airbyte-lib/airbyte_lib/datasets/__init__.py @@ -0,0 +1,10 @@ +from airbyte_lib.datasets._base import DatasetBase +from airbyte_lib.datasets._cached import CachedDataset +from airbyte_lib.datasets._map import DatasetMap + + +__all__ = [ + "CachedDataset", + "DatasetBase", + "DatasetMap", +] diff --git a/airbyte-lib/airbyte_lib/datasets/_base.py b/airbyte-lib/airbyte_lib/datasets/_base.py new file mode 100644 index 0000000000000..b42e5258e5593 --- /dev/null +++ b/airbyte-lib/airbyte_lib/datasets/_base.py @@ -0,0 +1,28 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from abc import ABC, abstractmethod +from collections.abc import Iterator, Mapping +from typing import Any, cast + +from pandas import DataFrame +from typing_extensions import Self + + +class DatasetBase(ABC, Iterator[Mapping[str, Any]]): + """Base implementation for all datasets.""" + + def __iter__(self) -> Self: + """Return the iterator object (usually self).""" + return self + + @abstractmethod + def __next__(self) -> Mapping[str, Any]: + """Return the next value from the iterator.""" + raise NotImplementedError + + def to_pandas(self) -> DataFrame: + """Return a pandas DataFrame representation of the dataset.""" + # Technically, we return an iterator of Mapping objects. However, pandas + # expects an iterator of dict objects. This cast is safe because we know + # duck typing is correct for this use case. + return DataFrame(cast(Iterator[dict[str, Any]], self)) diff --git a/airbyte-lib/airbyte_lib/datasets/_cached.py b/airbyte-lib/airbyte_lib/datasets/_cached.py new file mode 100644 index 0000000000000..37aed0458312b --- /dev/null +++ b/airbyte-lib/airbyte_lib/datasets/_cached.py @@ -0,0 +1,34 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any + +from typing_extensions import Self + +from airbyte_lib.datasets._base import DatasetBase + + +if TYPE_CHECKING: + from pandas import DataFrame + from sqlalchemy import Table + + from airbyte_lib.caches import SQLCacheBase + + +class CachedDataset(DatasetBase): + def __init__(self, cache: "SQLCacheBase", stream: str) -> None: + self._cache = cache + self._stream = stream + self._iterator = iter(self._cache.get_records(self._stream)) + + def __iter__(self) -> Self: + return self + + def __next__(self) -> Mapping[str, Any]: + return next(self._iterator) + + def to_pandas(self) -> "DataFrame": + return self._cache.get_pandas_dataframe(self._stream) + + def to_sql_table(self) -> "Table": + return self._cache.get_sql_table(self._stream) diff --git a/airbyte-lib/airbyte_lib/datasets/_lazy.py b/airbyte-lib/airbyte_lib/datasets/_lazy.py new file mode 100644 index 0000000000000..e2c22dd0a0580 --- /dev/null +++ b/airbyte-lib/airbyte_lib/datasets/_lazy.py @@ -0,0 +1,43 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from collections.abc import Callable, Iterator +from typing import Any + +from overrides import overrides +from typing_extensions import Self + +from airbyte_lib.datasets import DatasetBase + + +class LazyDataset(DatasetBase): + """A dataset that is loaded incrementally from a source or a SQL query. + + TODO: Test and debug this. It is not yet implemented anywhere in the codebase. + For now it servers as a placeholder. + """ + + def __init__( + self, + iterator: Iterator, + on_open: Callable | None = None, + on_close: Callable | None = None, + ) -> None: + self._iterator = iterator + self._on_open = on_open + self._on_close = on_close + raise NotImplementedError("This class is not implemented yet.") + + @overrides + def __iter__(self) -> Self: + raise NotImplementedError("This class is not implemented yet.") + # Pseudocode: + # if self._on_open is not None: + # self._on_open() + + # yield from self._iterator + + # if self._on_close is not None: + # self._on_close() + + def __next__(self) -> dict[str, Any]: + return next(self._iterator) diff --git a/airbyte-lib/airbyte_lib/datasets/_map.py b/airbyte-lib/airbyte_lib/datasets/_map.py new file mode 100644 index 0000000000000..3881e1d33da84 --- /dev/null +++ b/airbyte-lib/airbyte_lib/datasets/_map.py @@ -0,0 +1,27 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +"""A generic interface for a set of streams. + +TODO: This is a work in progress. It is not yet used by any other code. +TODO: Implement before release, or delete. +""" + +from collections.abc import Iterator, Mapping + +from airbyte_lib.datasets._base import DatasetBase + + +class DatasetMap(Mapping): + """A generic interface for a set of streams or datasets.""" + + def __init__(self) -> None: + self._datasets: dict[str, DatasetBase] = {} + + def __getitem__(self, key: str) -> DatasetBase: + return self._datasets[key] + + def __iter__(self) -> Iterator[str]: + return iter(self._datasets) + + def __len__(self) -> int: + return len(self._datasets) diff --git a/airbyte-lib/airbyte_lib/registry.py b/airbyte-lib/airbyte_lib/registry.py index 05c107da80b92..a8c964578ab66 100644 --- a/airbyte-lib/airbyte_lib/registry.py +++ b/airbyte-lib/airbyte_lib/registry.py @@ -4,7 +4,6 @@ import json import os from dataclasses import dataclass -from typing import Dict, Optional import requests @@ -15,7 +14,7 @@ class ConnectorMetadata: latest_available_version: str -_cache: Optional[Dict[str, ConnectorMetadata]] = None +_cache: dict[str, ConnectorMetadata] | None = None airbyte_lib_version = importlib.metadata.version("airbyte-lib") REGISTRY_URL = "https://connectors.airbyte.com/files/registries/v0/oss_registry.json" @@ -24,10 +23,12 @@ class ConnectorMetadata: def _update_cache() -> None: global _cache if os.environ.get("AIRBYTE_LOCAL_REGISTRY"): - with open(str(os.environ.get("AIRBYTE_LOCAL_REGISTRY")), "r") as f: + with open(str(os.environ.get("AIRBYTE_LOCAL_REGISTRY"))) as f: data = json.load(f) else: - response = requests.get(REGISTRY_URL, headers={"User-Agent": f"airbyte-lib-{airbyte_lib_version}"}) + response = requests.get( + REGISTRY_URL, headers={"User-Agent": f"airbyte-lib-{airbyte_lib_version}"} + ) response.raise_for_status() data = response.json() _cache = {} @@ -36,7 +37,7 @@ def _update_cache() -> None: _cache[name] = ConnectorMetadata(name, connector["dockerImageTag"]) -def get_connector_metadata(name: str): +def get_connector_metadata(name: str) -> ConnectorMetadata: """ check the cache for the connector. If the cache is empty, populate by calling update_cache """ diff --git a/airbyte-lib/airbyte_lib/results.py b/airbyte-lib/airbyte_lib/results.py new file mode 100644 index 0000000000000..456c53d10a7f8 --- /dev/null +++ b/airbyte-lib/airbyte_lib/results.py @@ -0,0 +1,22 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from sqlalchemy import Engine + +from airbyte_lib.caches import SQLCacheBase +from airbyte_lib.datasets import CachedDataset + + +class ReadResult: + def __init__(self, processed_records: int, cache: SQLCacheBase) -> None: + self.processed_records = processed_records + self._cache = cache + + def __getitem__(self, stream: str) -> CachedDataset: + return CachedDataset(self._cache, stream) + + def get_sql_engine(self) -> Engine: + return self._cache.get_sql_engine() + + @property + def cache(self) -> SQLCacheBase: + return self._cache diff --git a/airbyte-lib/airbyte_lib/source.py b/airbyte-lib/airbyte_lib/source.py index 612dee86f0998..1042203e11f36 100644 --- a/airbyte-lib/airbyte_lib/source.py +++ b/airbyte-lib/airbyte_lib/source.py @@ -2,14 +2,13 @@ import json import tempfile +from collections.abc import Generator, Iterable, Iterator from contextlib import contextmanager from functools import lru_cache -from typing import Any, Dict, Iterable, List, Optional +from typing import Any, Optional import jsonschema -from airbyte_lib.cache import Cache, InMemoryCache -from airbyte_lib.executor import Executor -from airbyte_lib.sync_result import SyncResult + from airbyte_protocol.models import ( AirbyteCatalog, AirbyteMessage, @@ -23,14 +22,22 @@ Type, ) +from airbyte_lib._executor import Executor +from airbyte_lib._factories.cache_factories import get_default_cache +from airbyte_lib._util import protocol_util # Internal utility functions +from airbyte_lib.caches import SQLCacheBase +from airbyte_lib.results import ReadResult + @contextmanager -def as_temp_files(files: List[Any]): - temp_files: List[Any] = [] +def as_temp_files(files: list[Any]) -> Generator[list[Any], Any, None]: + temp_files: list[Any] = [] try: for content in files: temp_file = tempfile.NamedTemporaryFile(mode="w+t", delete=True) - temp_file.write(json.dumps(content) if isinstance(content, dict) else content) + temp_file.write( + json.dumps(content) if isinstance(content, dict) else content, + ) temp_file.flush() temp_files.append(temp_file) yield [file.name for file in temp_files] @@ -49,34 +56,39 @@ def __init__( self, executor: Executor, name: str, - config: Optional[Dict[str, Any]] = None, - streams: Optional[List[str]] = None, + config: Optional[dict[str, Any]] = None, + streams: Optional[list[str]] = None, ): self.executor = executor self.name = name - self.streams: Optional[List[str]] = None - self._config_dict: Optional[Dict[str, Any]] = None - self._last_log_messages: List[str] = [] + self.streams: Optional[list[str]] = None + self._processed_records = 0 + self._config_dict: Optional[dict[str, Any]] = None + self._last_log_messages: list[str] = [] if config is not None: self.set_config(config) if streams is not None: self.set_streams(streams) - def set_streams(self, streams: List[str]): + def set_streams(self, streams: list[str]) -> None: available_streams = self.get_available_streams() for stream in streams: if stream not in available_streams: - raise Exception(f"Stream {stream} is not available for connector {self.name}, choose from {available_streams}") + raise Exception( + f"Stream {stream} is not available for connector {self.name}, choose from {available_streams}", + ) self.streams = streams - def set_config(self, config: Dict[str, Any]): + def set_config(self, config: dict[str, Any]) -> None: self._validate_config(config) self._config_dict = config @property - def _config(self) -> Dict[str, Any]: + def _config(self) -> dict[str, Any]: if self._config_dict is None: - raise Exception("Config is not set, either set in get_connector or via source.set_config") + raise Exception( + "Config is not set, either set in get_connector or via source.set_config", + ) return self._config_dict def _discover(self) -> AirbyteCatalog: @@ -93,16 +105,18 @@ def _discover(self) -> AirbyteCatalog: for msg in self._execute(["discover", "--config", config_file]): if msg.type == Type.CATALOG and msg.catalog: return msg.catalog - raise Exception(f"Connector did not return a catalog. Last logs: {self._last_log_messages}") + raise Exception( + f"Connector did not return a catalog. Last logs: {self._last_log_messages}", + ) - def _validate_config(self, config: Dict[str, Any]) -> None: + def _validate_config(self, config: dict[str, Any]) -> None: """ Validate the config against the spec. """ spec = self._spec() jsonschema.validate(config, spec.connectionSpecification) - def get_available_streams(self) -> List[str]: + def get_available_streams(self) -> list[str]: """ Get the available streams from the spec. """ @@ -121,9 +135,40 @@ def _spec(self) -> ConnectorSpecification: for msg in self._execute(["spec"]): if msg.type == Type.SPEC and msg.spec: return msg.spec - raise Exception(f"Connector did not return a spec. Last logs: {self._last_log_messages}") + raise Exception( + f"Connector did not return a spec. Last logs: {self._last_log_messages}", + ) - def read_stream(self, stream: str) -> Iterable[Dict[str, Any]]: + @property + @lru_cache(maxsize=1) + def raw_catalog(self) -> AirbyteCatalog: + """ + Get the raw catalog for the given streams. + """ + catalog = self._discover() + return catalog + + @property + @lru_cache(maxsize=1) + def configured_catalog(self) -> ConfiguredAirbyteCatalog: + """ + Get the configured catalog for the given streams. + """ + catalog = self._discover() + return ConfiguredAirbyteCatalog( + streams=[ + ConfiguredAirbyteStream( + stream=s, + sync_mode=SyncMode.full_refresh, + destination_sync_mode=DestinationSyncMode.overwrite, + primary_key=None, + ) + for s in catalog.streams + if self.streams is None or s.name in self.streams + ], + ) + + def get_records(self, stream: str) -> Iterator[dict[str, Any]]: """ Read a stream from the connector. @@ -145,14 +190,20 @@ def read_stream(self, stream: str) -> Iterable[Dict[str, Any]]: ) for s in catalog.streams if s.name == stream - ] + ], ) if len(configured_catalog.streams) == 0: - raise Exception(f"Stream {stream} is not available for connector {self.name}, choose from {self.get_available_streams()}") - for message in self._read_catalog(configured_catalog): - yield message.data + raise ValueError( + f"Stream {stream} is not available for connector {self.name}, " + f"choose from {self.get_available_streams()}", + ) + + iterator: Iterable[dict[str, Any]] = protocol_util.airbyte_messages_to_record_dicts( + self._read_with_catalog(configured_catalog), + ) + yield from iterator # TODO: Refactor to use LazyDataset here - def check(self): + def check(self) -> None: """ Call check on the connector. @@ -166,24 +217,31 @@ def check(self): for msg in self._execute(["check", "--config", config_file]): if msg.type == Type.CONNECTION_STATUS and msg.connectionStatus: if msg.connectionStatus.status == Status.FAILED: - raise Exception(f"Connector returned failed status: {msg.connectionStatus.message}") + raise Exception( + f"Connector returned failed status: {msg.connectionStatus.message}", + ) else: return - raise Exception(f"Connector did not return check status. Last logs: {self._last_log_messages}") + raise Exception( + f"Connector did not return check status. Last logs: {self._last_log_messages}", + ) - def install(self): + def install(self) -> None: """ Install the connector if it is not yet installed. """ self.executor.install() - def uninstall(self): + def uninstall(self) -> None: """ - Uninstall the connector if it is installed. This only works if the use_local_install flag wasn't used and installation is managed by airbyte-lib. + Uninstall the connector if it is installed. + + This only works if the use_local_install flag wasn't used and installation is managed by + airbyte-lib. """ self.executor.uninstall() - def _read(self) -> Iterable[AirbyteRecordMessage]: + def _read(self) -> Iterator[AirbyteMessage]: """ Call read on the connector. @@ -204,11 +262,14 @@ def _read(self) -> Iterable[AirbyteRecordMessage]: ) for s in catalog.streams if self.streams is None or s.name in self.streams - ] + ], ) - yield from self._read_catalog(configured_catalog) + yield from self._read_with_catalog(configured_catalog) - def _read_catalog(self, catalog: ConfiguredAirbyteCatalog) -> Iterable[AirbyteRecordMessage]: + def _read_with_catalog( + self, + catalog: ConfiguredAirbyteCatalog, + ) -> Iterator[AirbyteMessage]: """ Call read on the connector. @@ -221,15 +282,16 @@ def _read_catalog(self, catalog: ConfiguredAirbyteCatalog) -> Iterable[AirbyteRe config_file, catalog_file, ]: - for msg in self._execute(["read", "--config", config_file, "--catalog", catalog_file]): - if msg.type == Type.RECORD: - yield msg.record + for msg in self._execute( + ["read", "--config", config_file, "--catalog", catalog_file], + ): + yield msg - def _add_to_logs(self, message: str): + def _add_to_logs(self, message: str) -> None: self._last_log_messages.append(message) self._last_log_messages = self._last_log_messages[-10:] - def _execute(self, args: List[str]) -> Iterable[AirbyteMessage]: + def _execute(self, args: list[str]) -> Iterator[AirbyteMessage]: """ Execute the connector with the given arguments. @@ -252,20 +314,26 @@ def _execute(self, args: List[str]) -> Iterable[AirbyteMessage]: except Exception: self._add_to_logs(line) except Exception as e: - raise Exception(f"{str(e)}. Last logs: {self._last_log_messages}") + raise Exception(f"{e!s}. Last logs: {self._last_log_messages}") - def _process(self, messages: Iterable[AirbyteRecordMessage]): - self._processed_records = 0 + def _tally_records( + self, + messages: Iterable[AirbyteRecordMessage], + ) -> Generator[AirbyteRecordMessage, Any, None]: + """This method simply tallies the number of records processed and yields the messages.""" + self._processed_records = 0 # Reset the counter before we start for message in messages: self._processed_records += 1 yield message - def read_all(self, cache: Optional[Cache] = None) -> SyncResult: + def read(self, cache: SQLCacheBase | None = None) -> ReadResult: if cache is None: - cache = InMemoryCache() - cache.write(self._process(self._read())) + cache = get_default_cache() + + cache.register_source(source_name=self.name, source_catalog=self.configured_catalog) + cache.process_airbyte_messages(self._tally_records(self._read())) - return SyncResult( + return ReadResult( processed_records=self._processed_records, cache=cache, ) diff --git a/airbyte-lib/airbyte_lib/sync_result.py b/airbyte-lib/airbyte_lib/sync_result.py deleted file mode 100644 index 57c814fdf139f..0000000000000 --- a/airbyte-lib/airbyte_lib/sync_result.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) 2023 Airbyte, Inc., all rights reserved. - -from typing import Any - -from airbyte_lib.cache import Cache - - -class Dataset: - def __init__(self, cache: Cache, stream: str) -> None: - self._cache = cache - self._stream = stream - - def __iter__(self): - return self._cache.get_iterable(self._stream) - - def to_pandas(self): - return self._cache.get_pandas(self._stream) - - def to_sql_table(self): - return self._cache.get_sql_table(self._stream) - - -class SyncResult: - def __init__(self, processed_records: int, cache: Cache) -> None: - self.processed_records = processed_records - self._cache = cache - - def __getitem__(self, stream: str) -> Dataset: - return Dataset(self._cache, stream) - - def get_sql_engine(self) -> Any: - return self._cache.get_sql_engine() diff --git a/airbyte-lib/airbyte_lib/types.py b/airbyte-lib/airbyte_lib/types.py new file mode 100644 index 0000000000000..eb87740a75cf3 --- /dev/null +++ b/airbyte-lib/airbyte_lib/types.py @@ -0,0 +1,110 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +"""Type conversion methods for SQL Caches.""" + +from typing import cast + +import sqlalchemy + + +# Compare to documentation here: https://docs.airbyte.com/understanding-airbyte/supported-data-types +CONVERSION_MAP = { + "string": sqlalchemy.types.VARCHAR, + "integer": sqlalchemy.types.BIGINT, + "number": sqlalchemy.types.DECIMAL, + "boolean": sqlalchemy.types.BOOLEAN, + "date": sqlalchemy.types.DATE, + "timestamp_with_timezone": sqlalchemy.types.TIMESTAMP, + "timestamp_without_timezone": sqlalchemy.types.TIMESTAMP, + "time_with_timezone": sqlalchemy.types.TIME, + "time_without_timezone": sqlalchemy.types.TIME, + # Technically 'object' and 'array' as JSON Schema types, not airbyte types. + # We include them here for completeness. + "object": sqlalchemy.types.VARCHAR, + "array": sqlalchemy.types.VARCHAR, +} + + +class SQLTypeConversionError(Exception): + """An exception to be raised when a type conversion fails.""" + + +def _get_airbyte_type( + json_schema_property_def: dict[str, str | dict], +) -> tuple[str, str | None]: + """Get the airbyte type and subtype from a JSON schema property definition. + + Subtype is only used for array types. Otherwise, subtype will return None. + """ + airbyte_type = cast(str, json_schema_property_def.get("airbyte_type", None)) + if airbyte_type: + return airbyte_type, None + + json_schema_type = json_schema_property_def.get("type", None) + json_schema_format = json_schema_property_def.get("format", None) + + if json_schema_type == "string": + if json_schema_format == "date": + return "date", None + + if json_schema_format == "date-time": + return "timestamp_with_timezone", None + + if json_schema_format == "time": + return "time_without_timezone", None + + if json_schema_type in ["string", "number", "boolean", "integer"]: + return cast(str, json_schema_type), None + + if json_schema_type == "object" and "properties" in json_schema_property_def: + return "object", None + + err_msg = f"Could not determine airbyte type from JSON schema type: {json_schema_property_def}" + raise SQLTypeConversionError(err_msg) + + +class SQLTypeConverter: + """A base class to perform type conversions.""" + + def __init__( + self, + conversion_map: dict | None = None, + ) -> None: + self.conversion_map = conversion_map or CONVERSION_MAP + + @staticmethod + def get_failover_type() -> sqlalchemy.types.TypeEngine: + """Get the 'last resort' type to use if no other type is found.""" + return sqlalchemy.types.VARCHAR() + + def to_sql_type( + self, + json_schema_property_def: dict[str, str | dict], + ) -> sqlalchemy.types.TypeEngine: + """Convert a value to a SQL type.""" + try: + airbyte_type, airbyte_subtype = _get_airbyte_type(json_schema_property_def) + return self.conversion_map[airbyte_type]() + except SQLTypeConversionError: + print(f"Could not determine airbyte type from JSON schema: {json_schema_property_def}") + except KeyError: + print(f"Could not find SQL type for airbyte type: {airbyte_type}") + + json_schema_type = json_schema_property_def.get("type", None) + json_schema_format = json_schema_property_def.get("format", None) + + if json_schema_type == "string" and json_schema_format == "date": + return sqlalchemy.types.DATE() + + if json_schema_type == "string" and json_schema_format == "date-time": + return sqlalchemy.types.TIMESTAMP() + + if json_schema_type == "array": + # TODO: Implement array type conversion. + return self.get_failover_type() + + if json_schema_type == "object": + # TODO: Implement object type handling. + return self.get_failover_type() + + return self.get_failover_type() diff --git a/airbyte-lib/airbyte_lib/validate.py b/airbyte-lib/airbyte_lib/validate.py index 58bae1b2466b2..f8aa646d86fc9 100644 --- a/airbyte-lib/airbyte_lib/validate.py +++ b/airbyte-lib/airbyte_lib/validate.py @@ -8,13 +8,13 @@ import sys import tempfile from pathlib import Path -from typing import List -import airbyte_lib as ab import yaml +import airbyte_lib as ab + -def _parse_args(): +def _parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Validate a connector") parser.add_argument( "--connector-dir", @@ -31,15 +31,19 @@ def _parse_args(): return parser.parse_args() -def _run_subprocess_and_raise_on_failure(args: List[str]): - result = subprocess.run(args) +def _run_subprocess_and_raise_on_failure(args: list[str]) -> None: + result = subprocess.run(args, check=False) if result.returncode != 0: raise Exception(f"{args} exited with code {result.returncode}") -def tests(connector_name, sample_config): +def tests(connector_name: str, sample_config: str) -> None: print("Creating source and validating spec and version...") - source = ab.get_connector(connector_name, config=json.load(open(sample_config))) + source = ab.get_connector( + # FIXME: noqa: SIM115, PTH123 + connector_name, + config=json.load(open(sample_config)), # noqa: SIM115, PTH123 + ) print("Running check...") source.check() @@ -51,7 +55,7 @@ def tests(connector_name, sample_config): for stream in streams: try: print(f"Trying to read from stream {stream}...") - record = next(source.read_stream(stream)) + record = next(source.get_records(stream)) assert record, "No record returned" break except Exception as e: @@ -60,7 +64,7 @@ def tests(connector_name, sample_config): raise Exception(f"Could not read from any stream from {streams}") -def run(): +def run() -> None: """ This is a CLI entrypoint for the `airbyte-lib-validate-source` command. It's called like this: airbyte-lib-validate-source —connector-dir . -—sample-config secrets/config.json @@ -77,10 +81,10 @@ def run(): validate(connector_dir, sample_config) -def validate(connector_dir, sample_config): +def validate(connector_dir: str, sample_config: str) -> None: # read metadata.yaml metadata_path = Path(connector_dir) / "metadata.yaml" - with open(metadata_path, "r") as stream: + with open(metadata_path) as stream: metadata = yaml.safe_load(stream)["data"] # TODO: Use remoteRegistries.pypi.packageName once set for connectors @@ -102,8 +106,8 @@ def validate(connector_dir, sample_config): { "dockerRepository": f"airbyte/{connector_name}", "dockerImageTag": "0.0.1", - } - ] + }, + ], } with tempfile.NamedTemporaryFile(mode="w+t", delete=True) as temp_file: diff --git a/airbyte-lib/docs/generated/airbyte_lib.html b/airbyte-lib/docs/generated/airbyte_lib.html index 18d441f26ff3b..15ce24f714eef 100644 --- a/airbyte-lib/docs/generated/airbyte_lib.html +++ b/airbyte-lib/docs/generated/airbyte_lib.html @@ -4,7 +4,7 @@
def - get_connector( name: str, version: str | None = None, pip_url: str | None = None, config: dict[str, typing.Any] | None = None, use_local_install: bool = False, install_if_missing: bool = True): + get_connector( name: str, version: str | None = None, pip_url: str | None = None, config: dict[str, typing.Any] | None = None, use_local_install: bool = False, install_if_missing: bool = True) -> Source:
@@ -26,114 +26,157 @@
Parameters
-
+
def - get_in_memory_cache(): + get_default_cache() -> airbyte_lib.caches.duckdb.DuckDBCache:
- + +

Get a local cache for storing data, using the default database path.

+ +

Cache files are stored in the .cache directory, relative to the current +working directory.

+
+ + +
+
+
+ + def + new_local_cache( cache_name: str | None = None, cache_dir: str | pathlib.Path | None = None, cleanup: bool = True) -> airbyte_lib.caches.duckdb.DuckDBCache: + + +
+ +

Get a local cache for storing data, using a name string to seed the path.

+ +

Args: + cache_name: Name to use for the cache. Defaults to None. + root_dir: Root directory to store the cache in. Defaults to None. + cleanup: Whether to clean up temporary files. Defaults to True.

+ +

Cache files are stored in the .cache directory, relative to the current +working directory.

+
+
-
+
class - Dataset: + CachedDataset(abc.ABC, collections.abc.Iterator[collections.abc.Mapping[str, typing.Any]]):
- - + +

Base implementation for all datasets.

+
+ -
+
- Dataset(cache: airbyte_lib.cache.Cache, stream: str) + CachedDataset(cache: airbyte_lib.caches.base.SQLCacheBase, stream: str)
- +
-
+
def - to_pandas(self): + to_pandas(self) -> pandas.core.frame.DataFrame:
- - + +

Return a pandas DataFrame representation of the dataset.

+
+
-
+
def - to_sql_table(self): + to_sql_table(self) -> sqlalchemy.sql.schema.Table:
- +
-
+
class - SyncResult: + ReadResult:
- + -
+
- SyncResult(processed_records: int, cache: airbyte_lib.cache.Cache) + ReadResult(processed_records: int, cache: airbyte_lib.caches.base.SQLCacheBase)
- +
-
+
processed_records
- +
-
+
def - get_sql_engine(self) -> Any: + get_sql_engine(self) -> sqlalchemy.engine.base.Engine: + + +
+ + + + +
+
+
+ cache: airbyte_lib.caches.base.SQLCacheBase
- + @@ -156,7 +199,7 @@
Parameters
- Source( executor: airbyte_lib.executor.Executor, name: str, config: Optional[Dict[str, Any]] = None, streams: Optional[List[str]] = None) + Source( executor: airbyte_lib._executor.Executor, name: str, config: Optional[dict[str, Any]] = None, streams: Optional[list[str]] = None)
@@ -189,7 +232,7 @@
Parameters
- streams: Optional[List[str]] + streams: Optional[list[str]]
@@ -202,7 +245,7 @@
Parameters
def - set_streams(self, streams: List[str]): + set_streams(self, streams: list[str]) -> None:
@@ -215,7 +258,7 @@
Parameters
def - set_config(self, config: Dict[str, Any]): + set_config(self, config: dict[str, typing.Any]) -> None:
@@ -228,7 +271,7 @@
Parameters
def - get_available_streams(self) -> List[str]: + get_available_streams(self) -> list[str]:
@@ -239,15 +282,41 @@
Parameters
-
+
+
+ raw_catalog: airbyte_protocol.models.airbyte_protocol.AirbyteCatalog + + +
+ + +

Get the raw catalog for the given streams.

+
+ + +
+
+
+ configured_catalog: airbyte_protocol.models.airbyte_protocol.ConfiguredAirbyteCatalog + + +
+ + +

Get the configured catalog for the given streams.

+
+ + +
+
def - read_stream(self, stream: str) -> Iterable[Dict[str, Any]]: + get_records(self, stream: str) -> collections.abc.Iterator[dict[str, typing.Any]]:
- +

Read a stream from the connector.

@@ -269,7 +338,7 @@
Parameters
def - check(self): + check(self) -> None:
@@ -293,7 +362,7 @@
Parameters
def - install(self): + install(self) -> None:
@@ -308,26 +377,29 @@
Parameters
def - uninstall(self): + uninstall(self) -> None:
-

Uninstall the connector if it is installed. This only works if the use_local_install flag wasn't used and installation is managed by airbyte-lib.

+

Uninstall the connector if it is installed.

+ +

This only works if the use_local_install flag wasn't used and installation is managed by +airbyte-lib.

-
+
def - read_all( self, cache: Optional[airbyte_lib.cache.Cache] = None) -> SyncResult: + read( self, cache: airbyte_lib.caches.base.SQLCacheBase | None = None) -> ReadResult:
- + diff --git a/airbyte-lib/docs/generated/airbyte_lib/caches.html b/airbyte-lib/docs/generated/airbyte_lib/caches.html index c0d27ca14eaa0..e6c3e5536a297 100644 --- a/airbyte-lib/docs/generated/airbyte_lib/caches.html +++ b/airbyte-lib/docs/generated/airbyte_lib/caches.html @@ -1,5 +1,683 @@
+
+
+ + class + DuckDBCache(airbyte_lib.caches.duckdb.DuckDBCacheBase): + + +
+ + +

A DuckDB implementation of the cache.

+ +

Parquet is used for local file storage before bulk loading. +Unlike the Snowflake implementation, we can't use the COPY command to load data +so we insert as values instead.

+
+ + +
+
+ file_writer_class = +<class 'airbyte_lib._file_writers.parquet.ParquetWriter'> + + +
+ + + + +
+
+
Inherited Members
+
+ +
airbyte_lib.caches.duckdb.DuckDBCacheBase
+
config_class
+
supports_merge_insert
+ +
+
airbyte_lib._processors.RecordProcessor
+
skip_finalize_step
+
source_catalog
+
register_source
+
process_stdin
+
process_input_stream
+
process_airbyte_messages
+ +
+
+
+
+
+
+ + class + DuckDBCacheConfig(airbyte_lib.caches.base.SQLCacheConfigBase, airbyte_lib._file_writers.parquet.ParquetWriterConfig): + + +
+ + +

Configuration for the DuckDB cache.

+ +

Also inherits config from the ParquetWriter, which is responsible for writing files to disk.

+
+ + +
+
+ db_path: pathlib.Path | str + + +
+ + +

Normally db_path is a Path object.

+ +

There are some cases, such as when connecting to MotherDuck, where it could be a string that +is not also a path, such as "md:" to connect the user's default MotherDuck DB.

+
+ + +
+
+
+ schema_name: str + + +
+ + +

The name of the schema to write to. Defaults to "main".

+
+ + +
+
+
+
@overrides
+ + def + get_sql_alchemy_url(self) -> str: + + +
+ + +

Return the SQLAlchemy URL to use.

+
+ + +
+
+
+ + def + get_database_name(self) -> str: + + +
+ + +

Return the name of the database.

+
+ + +
+
+
Inherited Members
+
+
pydantic.main.BaseModel
+
BaseModel
+
Config
+
dict
+
json
+
parse_obj
+
parse_raw
+
parse_file
+
from_orm
+
construct
+
copy
+
schema
+
schema_json
+
validate
+
update_forward_refs
+ +
+
airbyte_lib.caches.base.SQLCacheConfigBase
+
dedupe_mode
+
table_prefix
+
table_suffix
+ +
+
airbyte_lib._file_writers.base.FileWriterConfigBase
+
cache_dir
+
cleanup
+ +
+
+
+
+
+
+ + class + PostgresCache(airbyte_lib.caches.SQLCacheBase): + + +
+ + +

A Postgres implementation of the cache.

+ +

Parquet is used for local file storage before bulk loading. +Unlike the Snowflake implementation, we can't use the COPY command to load data +so we insert as values instead.

+ +

TOOD: Add optimized bulk load path for Postgres. Could use an alternate file writer +or another import method. (Relatively low priority, since for now it works fine as-is.)

+
+ + +
+
+ config_class = +<class 'PostgresCacheConfig'> + + +
+ + + + +
+
+
+ file_writer_class = +<class 'airbyte_lib._file_writers.parquet.ParquetWriter'> + + +
+ + + + +
+
+
+ supports_merge_insert = +True + + +
+ + + + +
+
+
Inherited Members
+
+ +
airbyte_lib._processors.RecordProcessor
+
skip_finalize_step
+
source_catalog
+
register_source
+
process_stdin
+
process_input_stream
+
process_airbyte_messages
+ +
+
+
+
+
+
+ + class + PostgresCacheConfig(airbyte_lib.caches.base.SQLCacheConfigBase, airbyte_lib._file_writers.parquet.ParquetWriterConfig): + + +
+ + +

Configuration for the Postgres cache.

+ +

Also inherits config from the ParquetWriter, which is responsible for writing files to disk.

+
+ + +
+
+ host: str + + +
+ + + + +
+
+
+ port: int + + +
+ + + + +
+
+
+ username: str + + +
+ + + + +
+
+
+ password: str + + +
+ + + + +
+
+
+ database: str + + +
+ + + + +
+
+
+
@overrides
+ + def + get_sql_alchemy_url(self) -> str: + + +
+ + +

Return the SQLAlchemy URL to use.

+
+ + +
+
+
+ + def + get_database_name(self) -> str: + + +
+ + +

Return the name of the database.

+
+ + +
+
+
Inherited Members
+
+
pydantic.main.BaseModel
+
BaseModel
+
Config
+
dict
+
json
+
parse_obj
+
parse_raw
+
parse_file
+
from_orm
+
construct
+
copy
+
schema
+
schema_json
+
validate
+
update_forward_refs
+ +
+
airbyte_lib.caches.base.SQLCacheConfigBase
+
dedupe_mode
+
schema_name
+
table_prefix
+
table_suffix
+ +
+
airbyte_lib._file_writers.base.FileWriterConfigBase
+
cache_dir
+
cleanup
+ +
+
+
+
+
+
+ + class + SQLCacheBase(airbyte_lib._processors.RecordProcessor): + + +
+ + +

A base class to be used for SQL Caches.

+ +

Optionally we can use a file cache to store the data in parquet files.

+
+ + +
+
+
@final
+ + SQLCacheBase( config: airbyte_lib.caches.base.SQLCacheConfigBase | None = None, file_writer: airbyte_lib._file_writers.base.FileWriterBase | None = None, **kwargs: dict[str, typing.Any]) + + +
+ + + + +
+
+
+ type_converter_class: type[airbyte_lib.types.SQLTypeConverter] = +<class 'airbyte_lib.types.SQLTypeConverter'> + + +
+ + + + +
+
+
+ config_class: type[airbyte_lib.caches.base.SQLCacheConfigBase] + + +
+ + + + +
+
+
+ file_writer_class: type[airbyte_lib._file_writers.base.FileWriterBase] + + +
+ + + + +
+
+
+ supports_merge_insert = +False + + +
+ + + + +
+
+
+ use_singleton_connection = +False + + +
+ + + + +
+
+
+ config: airbyte_lib.caches.base.SQLCacheConfigBase + + +
+ + + + +
+
+
+ file_writer + + +
+ + + + +
+
+
+ type_converter + + +
+ + + + +
+
+
+ + def + get_sql_alchemy_url(self) -> str: + + +
+ + +

Return the SQLAlchemy URL to use.

+
+ + +
+
+
+ database_name: str + + +
+ + +

Return the name of the database.

+
+ + +
+
+
+
@final
+ + def + get_sql_engine(self) -> sqlalchemy.engine.base.Engine: + + +
+ + +

Return a new SQL engine to use.

+
+ + +
+
+
+
@contextmanager
+ + def + get_sql_connection( self) -> collections.abc.Generator[sqlalchemy.engine.base.Connection, None, None]: + + +
+ + +

A context manager which returns a new SQL connection for running queries.

+ +

If the connection needs to close, it will be closed automatically.

+
+ + +
+
+
+ + def + get_sql_table_name(self, stream_name: str) -> str: + + +
+ + +

Return the name of the SQL table for the given stream.

+
+ + +
+
+
+
@final
+ + def + get_sql_table(self, stream_name: str) -> sqlalchemy.sql.schema.Table: + + +
+ + +

Return a temporary table name.

+
+ + +
+
+
+ streams: dict[str, airbyte_lib.datasets._base.DatasetBase] + + +
+ + +

Return a temporary table name.

+
+ + +
+
+
+ + def + get_records( self, stream_name: str) -> collections.abc.Iterator[collections.abc.Mapping[str, typing.Any]]: + + +
+ + +

Uses SQLAlchemy to select all rows from the table.

+ +

TODO: Refactor to return a LazyDataset here.

+
+ + +
+
+
+ + def + get_pandas_dataframe(self, stream_name: str) -> pandas.core.frame.DataFrame: + + +
+ + +

Return a Pandas data frame with the stream's data.

+
+ + +
+
+
Inherited Members
+
+
airbyte_lib._processors.RecordProcessor
+
skip_finalize_step
+
source_catalog
+
register_source
+
process_stdin
+
process_input_stream
+
process_airbyte_messages
+ +
+
+
+
diff --git a/airbyte-lib/docs/generated/airbyte_lib/datasets.html b/airbyte-lib/docs/generated/airbyte_lib/datasets.html index c0d27ca14eaa0..acd3bb5928b63 100644 --- a/airbyte-lib/docs/generated/airbyte_lib/datasets.html +++ b/airbyte-lib/docs/generated/airbyte_lib/datasets.html @@ -1,5 +1,117 @@
+
+
+ + class + CachedDataset(abc.ABC, collections.abc.Iterator[collections.abc.Mapping[str, typing.Any]]): + + +
+ + +

Base implementation for all datasets.

+
+ + +
+
+ + CachedDataset(cache: airbyte_lib.caches.base.SQLCacheBase, stream: str) + + +
+ + + + +
+
+
+ + def + to_pandas(self) -> pandas.core.frame.DataFrame: + + +
+ + +

Return a pandas DataFrame representation of the dataset.

+
+ + +
+
+
+ + def + to_sql_table(self) -> sqlalchemy.sql.schema.Table: + + +
+ + + + +
+
+
+
+ + class + DatasetBase(abc.ABC, collections.abc.Iterator[collections.abc.Mapping[str, typing.Any]]): + + +
+ + +

Base implementation for all datasets.

+
+ + +
+
+ + def + to_pandas(self) -> pandas.core.frame.DataFrame: + + +
+ + +

Return a pandas DataFrame representation of the dataset.

+
+ + +
+
+
+
+ + class + DatasetMap(collections.abc.Mapping): + + +
+ + +

A generic interface for a set of streams or datasets.

+
+ + +
+
Inherited Members
+
+
collections.abc.Mapping
+
get
+
keys
+
items
+
values
+ +
+
+
+
diff --git a/airbyte-lib/docs/generated/airbyte_lib/factories.html b/airbyte-lib/docs/generated/airbyte_lib/factories.html deleted file mode 100644 index 0a91ab3f27296..0000000000000 --- a/airbyte-lib/docs/generated/airbyte_lib/factories.html +++ /dev/null @@ -1,46 +0,0 @@ - -
-
-
- - def - get_in_memory_cache(): - - -
- - - - -
-
-
- - def - get_connector( name: str, version: str | None = None, pip_url: str | None = None, config: dict[str, typing.Any] | None = None, use_local_install: bool = False, install_if_missing: bool = True): - - -
- - -

Get a connector by name and version.

- -
Parameters
- -
    -
  • name: connector name
  • -
  • version: connector version - if not provided, the currently installed version will be used. If no version is installed, the latest available version will be used. The version can also be set to "latest" to force the use of the latest available version.
  • -
  • pip_url: connector pip URL - if not provided, the pip url will be inferred from the connector name.
  • -
  • config: connector config - if not provided, you need to set it later via the set_config method.
  • -
  • use_local_install: whether to use a virtual environment to run the connector. If True, the connector is expected to be available on the path (e.g. installed via pip). If False, the connector will be installed automatically in a virtual environment.
  • -
  • install_if_missing: whether to install the connector if it is not available locally. This parameter is ignored if use_local_install is True.
  • -
-
- - -
-
- - - - \ No newline at end of file diff --git a/airbyte-lib/docs/generated/airbyte_lib/file_writers.html b/airbyte-lib/docs/generated/airbyte_lib/file_writers.html deleted file mode 100644 index c0d27ca14eaa0..0000000000000 --- a/airbyte-lib/docs/generated/airbyte_lib/file_writers.html +++ /dev/null @@ -1,7 +0,0 @@ - -
-
- - - - \ No newline at end of file diff --git a/airbyte-lib/examples/run_faker.py b/airbyte-lib/examples/run_faker.py new file mode 100644 index 0000000000000..02df99d54e120 --- /dev/null +++ b/airbyte-lib/examples/run_faker.py @@ -0,0 +1,26 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from itertools import islice + +import airbyte_lib as ab + + +source = ab.get_connector( + "source-faker", + config={"count": 10000, "seed": 0, "parallelism": 1, "always_updated": False}, + install_if_missing=True, +) +cache = ab.new_local_cache() + +source.check() + +# TODO: Pur the real stream names here: +streams = ["stream1", "stream2", "stream3"] +# source.set_streams(["launches", "rockets", "capsules"]) + +result = source.read(cache) + +print(islice(source.get_records(streams[0]), 10)) + +for name, records in result.cache.streams.items(): + print(f"Stream {name}: {len(list(records))} records") diff --git a/airbyte-lib/examples/run_spacex.py b/airbyte-lib/examples/run_spacex.py index 2cfd46ab70f88..46c8dee411d87 100644 --- a/airbyte-lib/examples/run_spacex.py +++ b/airbyte-lib/examples/run_spacex.py @@ -4,6 +4,7 @@ import airbyte_lib as ab + # preparation (from airbyte-lib main folder): # python -m venv .venv-source-spacex-api # source .venv-source-spacex-api/bin/activate @@ -11,16 +12,20 @@ # In separate terminal: # poetry run python examples/run_spacex.py -source = ab.get_connector("source-spacex-api", config={"id": "605b4b6aaa5433645e37d03f"}) -cache = ab.get_in_memory_cache() +source = ab.get_connector( + "source-spacex-api", + config={"id": "605b4b6aaa5433645e37d03f"}, + install_if_missing=True, +) +cache = ab.new_local_cache() source.check() source.set_streams(["launches", "rockets", "capsules"]) -result = source.read_all(cache) +result = source.read(cache) -print(islice(source.read_stream("capsules"), 10)) +print(islice(source.get_records("capsules"), 10)) for name, records in result.cache.streams.items(): - print(f"Stream {name}: {len(records)} records") + print(f"Stream {name}: {len(list(records))} records") diff --git a/airbyte-lib/examples/run_test_source.py b/airbyte-lib/examples/run_test_source.py index 76baa8e771dc6..de57ca8420ff6 100644 --- a/airbyte-lib/examples/run_test_source.py +++ b/airbyte-lib/examples/run_test_source.py @@ -4,6 +4,7 @@ import airbyte_lib as ab + # preparation (from airbyte-lib main folder): # python -m venv .venv-source-test # source .venv-source-test/bin/activate @@ -14,13 +15,13 @@ os.environ["AIRBYTE_LOCAL_REGISTRY"] = "./tests/integration_tests/fixtures/registry.json" source = ab.get_connector("source-test", config={"apiKey": "test"}) -cache = ab.get_in_memory_cache() +cache = ab.new_local_cache() source.check() print(source.get_available_streams()) -result = source.read_all(cache) +result = source.read(cache) print(result.processed_records) print(list(result["stream1"])) diff --git a/airbyte-lib/poetry.lock b/airbyte-lib/poetry.lock index 4bb03217983c8..58fb0cbf88c71 100644 --- a/airbyte-lib/poetry.lock +++ b/airbyte-lib/poetry.lock @@ -1,18 +1,55 @@ # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +[[package]] +name = "airbyte-cdk" +version = "0.58.3" +description = "A framework for writing Airbyte Connectors." +optional = false +python-versions = ">=3.8" +files = [ + {file = "airbyte-cdk-0.58.3.tar.gz", hash = "sha256:da75898d1503d8bd06840cb0c10a06f6a0ebcc77858deca146de34e392b01ede"}, + {file = "airbyte_cdk-0.58.3-py3-none-any.whl", hash = "sha256:00f81ebe7d8c7be724ea2c8f364f31803de2345d1ccbf9cdcad808562e512b7b"}, +] + +[package.dependencies] +airbyte-protocol-models = "0.5.1" +backoff = "*" +cachetools = "*" +Deprecated = ">=1.2,<2.0" +dpath = ">=2.0.1,<2.1.0" +genson = "1.2.2" +isodate = ">=0.6.1,<0.7.0" +Jinja2 = ">=3.1.2,<3.2.0" +jsonref = ">=0.2,<1.0" +jsonschema = ">=3.2.0,<3.3.0" +pendulum = "<3.0.0" +pydantic = ">=1.10.8,<2.0.0" +pyrate-limiter = ">=3.1.0,<3.2.0" +python-dateutil = "*" +PyYAML = ">=6.0.1" +requests = "*" +requests-cache = "*" +wcmatch = "8.4" + +[package.extras] +dev = ["avro (>=1.11.2,<1.12.0)", "cohere (==4.21)", "fastavro (>=1.8.0,<1.9.0)", "freezegun", "langchain (==0.0.271)", "markdown", "mypy", "openai[embeddings] (==0.27.9)", "pandas (==2.0.3)", "pdf2image (==1.16.3)", "pdfminer.six (==20221105)", "pyarrow (==12.0.1)", "pytesseract (==0.3.10)", "pytest", "pytest-cov", "pytest-httpserver", "pytest-mock", "requests-mock", "tiktoken (==0.4.0)", "unstructured (==0.10.27)", "unstructured.pytesseract (>=0.3.12)", "unstructured[docx,pptx] (==0.10.27)"] +file-based = ["avro (>=1.11.2,<1.12.0)", "fastavro (>=1.8.0,<1.9.0)", "markdown", "pdf2image (==1.16.3)", "pdfminer.six (==20221105)", "pyarrow (==12.0.1)", "pytesseract (==0.3.10)", "unstructured (==0.10.27)", "unstructured.pytesseract (>=0.3.12)", "unstructured[docx,pptx] (==0.10.27)"] +sphinx-docs = ["Sphinx (>=4.2,<5.0)", "sphinx-rtd-theme (>=1.0,<2.0)"] +vector-db-based = ["cohere (==4.21)", "langchain (==0.0.271)", "openai[embeddings] (==0.27.9)", "tiktoken (==0.4.0)"] + [[package]] name = "airbyte-protocol-models" -version = "1.0.1" +version = "0.5.1" description = "Declares the Airbyte Protocol." optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" files = [ - {file = "airbyte_protocol_models-1.0.1-py3-none-any.whl", hash = "sha256:2c214fb8cb42b74aa6408beeea2cd52f094bc8a3ba0e78af20bb358e5404f4a8"}, - {file = "airbyte_protocol_models-1.0.1.tar.gz", hash = "sha256:caa860d15c9c9073df4b221f58280b9855d36de07519e010d1e610546458d0a7"}, + {file = "airbyte_protocol_models-0.5.1-py3-none-any.whl", hash = "sha256:dfe84e130e51ce2ae81a06d5aa36f6c5ce3152b9e36e6f0195fad6c3dab0927e"}, + {file = "airbyte_protocol_models-0.5.1.tar.gz", hash = "sha256:7c8b16c7c1c7956b1996052e40585a3a93b1e44cb509c4e97c1ee4fe507ea086"}, ] [package.dependencies] -pydantic = ">=1.9.2,<1.10.0" +pydantic = ">=1.9.2,<2.0.0" [[package]] name = "attrs" @@ -33,6 +70,64 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +[[package]] +name = "backoff" +version = "2.2.1" +description = "Function decoration for backoff and retry" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, + {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, +] + +[[package]] +name = "bracex" +version = "2.4" +description = "Bash style brace expander." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bracex-2.4-py3-none-any.whl", hash = "sha256:efdc71eff95eaff5e0f8cfebe7d01adf2c8637c8c92edaf63ef348c241a82418"}, + {file = "bracex-2.4.tar.gz", hash = "sha256:a27eaf1df42cf561fed58b7a8f3fdf129d1ea16a81e1fadd1d17989bc6384beb"}, +] + +[[package]] +name = "cachetools" +version = "5.3.2" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, + {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, +] + +[[package]] +name = "cattrs" +version = "23.2.3" +description = "Composable complex class support for attrs and dataclasses." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cattrs-23.2.3-py3-none-any.whl", hash = "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108"}, + {file = "cattrs-23.2.3.tar.gz", hash = "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"}, +] + +[package.dependencies] +attrs = ">=23.1.0" +exceptiongroup = {version = ">=1.1.1", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.1.0,<4.6.3 || >4.6.3", markers = "python_version < \"3.11\""} + +[package.extras] +bson = ["pymongo (>=4.4.0)"] +cbor2 = ["cbor2 (>=5.4.6)"] +msgpack = ["msgpack (>=1.0.5)"] +orjson = ["orjson (>=3.9.2)"] +pyyaml = ["pyyaml (>=6.0)"] +tomlkit = ["tomlkit (>=0.11.8)"] +ujson = ["ujson (>=5.7.0)"] + [[package]] name = "certifi" version = "2023.11.17" @@ -154,6 +249,118 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "deprecated" +version = "1.2.14" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, + {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] + +[[package]] +name = "docker" +version = "7.0.0" +description = "A Python library for the Docker Engine API." +optional = false +python-versions = ">=3.8" +files = [ + {file = "docker-7.0.0-py3-none-any.whl", hash = "sha256:12ba681f2777a0ad28ffbcc846a69c31b4dfd9752b47eb425a274ee269c5e14b"}, + {file = "docker-7.0.0.tar.gz", hash = "sha256:323736fb92cd9418fc5e7133bc953e11a9da04f4483f828b527db553f1e7e5a3"}, +] + +[package.dependencies] +packaging = ">=14.0" +pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} +requests = ">=2.26.0" +urllib3 = ">=1.26.0" + +[package.extras] +ssh = ["paramiko (>=2.4.3)"] +websockets = ["websocket-client (>=1.3.0)"] + +[[package]] +name = "dpath" +version = "2.0.8" +description = "Filesystem-like pathing and searching for dictionaries" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dpath-2.0.8-py3-none-any.whl", hash = "sha256:f92f595214dd93a00558d75d4b858beee519f4cffca87f02616ad6cd013f3436"}, + {file = "dpath-2.0.8.tar.gz", hash = "sha256:a3440157ebe80d0a3ad794f1b61c571bef125214800ffdb9afc9424e8250fe9b"}, +] + +[[package]] +name = "duckdb" +version = "0.9.2" +description = "DuckDB embedded database" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "duckdb-0.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aadcea5160c586704c03a8a796c06a8afffbefefb1986601104a60cb0bfdb5ab"}, + {file = "duckdb-0.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:08215f17147ed83cbec972175d9882387366de2ed36c21cbe4add04b39a5bcb4"}, + {file = "duckdb-0.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee6c2a8aba6850abef5e1be9dbc04b8e72a5b2c2b67f77892317a21fae868fe7"}, + {file = "duckdb-0.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ff49f3da9399900fd58b5acd0bb8bfad22c5147584ad2427a78d937e11ec9d0"}, + {file = "duckdb-0.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5ac5baf8597efd2bfa75f984654afcabcd698342d59b0e265a0bc6f267b3f0"}, + {file = "duckdb-0.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:81c6df905589a1023a27e9712edb5b724566587ef280a0c66a7ec07c8083623b"}, + {file = "duckdb-0.9.2-cp310-cp310-win32.whl", hash = "sha256:a298cd1d821c81d0dec8a60878c4b38c1adea04a9675fb6306c8f9083bbf314d"}, + {file = "duckdb-0.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:492a69cd60b6cb4f671b51893884cdc5efc4c3b2eb76057a007d2a2295427173"}, + {file = "duckdb-0.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:061a9ea809811d6e3025c5de31bc40e0302cfb08c08feefa574a6491e882e7e8"}, + {file = "duckdb-0.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a43f93be768af39f604b7b9b48891f9177c9282a408051209101ff80f7450d8f"}, + {file = "duckdb-0.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ac29c8c8f56fff5a681f7bf61711ccb9325c5329e64f23cb7ff31781d7b50773"}, + {file = "duckdb-0.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b14d98d26bab139114f62ade81350a5342f60a168d94b27ed2c706838f949eda"}, + {file = "duckdb-0.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:796a995299878913e765b28cc2b14c8e44fae2f54ab41a9ee668c18449f5f833"}, + {file = "duckdb-0.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6cb64ccfb72c11ec9c41b3cb6181b6fd33deccceda530e94e1c362af5f810ba1"}, + {file = "duckdb-0.9.2-cp311-cp311-win32.whl", hash = "sha256:930740cb7b2cd9e79946e1d3a8f66e15dc5849d4eaeff75c8788d0983b9256a5"}, + {file = "duckdb-0.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:c28f13c45006fd525001b2011cdf91fa216530e9751779651e66edc0e446be50"}, + {file = "duckdb-0.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fbce7bbcb4ba7d99fcec84cec08db40bc0dd9342c6c11930ce708817741faeeb"}, + {file = "duckdb-0.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15a82109a9e69b1891f0999749f9e3265f550032470f51432f944a37cfdc908b"}, + {file = "duckdb-0.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9490fb9a35eb74af40db5569d90df8a04a6f09ed9a8c9caa024998c40e2506aa"}, + {file = "duckdb-0.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:696d5c6dee86c1a491ea15b74aafe34ad2b62dcd46ad7e03b1d00111ca1a8c68"}, + {file = "duckdb-0.9.2-cp37-cp37m-win32.whl", hash = "sha256:4f0935300bdf8b7631ddfc838f36a858c1323696d8c8a2cecbd416bddf6b0631"}, + {file = "duckdb-0.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:0aab900f7510e4d2613263865570203ddfa2631858c7eb8cbed091af6ceb597f"}, + {file = "duckdb-0.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7d8130ed6a0c9421b135d0743705ea95b9a745852977717504e45722c112bf7a"}, + {file = "duckdb-0.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:974e5de0294f88a1a837378f1f83330395801e9246f4e88ed3bfc8ada65dcbee"}, + {file = "duckdb-0.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4fbc297b602ef17e579bb3190c94d19c5002422b55814421a0fc11299c0c1100"}, + {file = "duckdb-0.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1dd58a0d84a424924a35b3772419f8cd78a01c626be3147e4934d7a035a8ad68"}, + {file = "duckdb-0.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11a1194a582c80dfb57565daa06141727e415ff5d17e022dc5f31888a5423d33"}, + {file = "duckdb-0.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:be45d08541002a9338e568dca67ab4f20c0277f8f58a73dfc1435c5b4297c996"}, + {file = "duckdb-0.9.2-cp38-cp38-win32.whl", hash = "sha256:dd6f88aeb7fc0bfecaca633629ff5c986ac966fe3b7dcec0b2c48632fd550ba2"}, + {file = "duckdb-0.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:28100c4a6a04e69aa0f4a6670a6d3d67a65f0337246a0c1a429f3f28f3c40b9a"}, + {file = "duckdb-0.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ae5bf0b6ad4278e46e933e51473b86b4b932dbc54ff097610e5b482dd125552"}, + {file = "duckdb-0.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e5d0bb845a80aa48ed1fd1d2d285dd352e96dc97f8efced2a7429437ccd1fe1f"}, + {file = "duckdb-0.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ce262d74a52500d10888110dfd6715989926ec936918c232dcbaddb78fc55b4"}, + {file = "duckdb-0.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6935240da090a7f7d2666f6d0a5e45ff85715244171ca4e6576060a7f4a1200e"}, + {file = "duckdb-0.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5cfb93e73911696a98b9479299d19cfbc21dd05bb7ab11a923a903f86b4d06e"}, + {file = "duckdb-0.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:64e3bc01751f31e7572d2716c3e8da8fe785f1cdc5be329100818d223002213f"}, + {file = "duckdb-0.9.2-cp39-cp39-win32.whl", hash = "sha256:6e5b80f46487636368e31b61461940e3999986359a78660a50dfdd17dd72017c"}, + {file = "duckdb-0.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:e6142a220180dbeea4f341708bd5f9501c5c962ce7ef47c1cadf5e8810b4cb13"}, + {file = "duckdb-0.9.2.tar.gz", hash = "sha256:3843afeab7c3fc4a4c0b53686a4cc1d9cdbdadcbb468d60fef910355ecafd447"}, +] + +[[package]] +name = "duckdb-engine" +version = "0.9.5" +description = "SQLAlchemy driver for duckdb" +optional = false +python-versions = ">=3.7" +files = [ + {file = "duckdb_engine-0.9.5-py3-none-any.whl", hash = "sha256:bdaf9cc6b7e95bff8081921a9a2bdfa1c72b5ee60c1403c5c671de620dfebd9e"}, + {file = "duckdb_engine-0.9.5.tar.gz", hash = "sha256:17fdc13068540315b64c7d174d5a260e918b1ce4b5346897caca026401afb280"}, +] + +[package.dependencies] +duckdb = ">=0.4.0" +sqlalchemy = ">=1.3.22" + [[package]] name = "exceptiongroup" version = "1.2.0" @@ -168,6 +375,20 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "faker" +version = "21.0.1" +description = "Faker is a Python package that generates fake data for you." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Faker-21.0.1-py3-none-any.whl", hash = "sha256:0afc67ec898a2d71842a3456e9302620ebc35fab6ad4f3829693fdf151fa4a3a"}, + {file = "Faker-21.0.1.tar.gz", hash = "sha256:bb404bba449b87e6b54a8c50b4602765e9c1a42eaf48abfceb025e42fed01608"}, +] + +[package.dependencies] +python-dateutil = ">=2.4" + [[package]] name = "filelock" version = "3.13.1" @@ -184,6 +405,87 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] +[[package]] +name = "genson" +version = "1.2.2" +description = "GenSON is a powerful, user-friendly JSON Schema generator." +optional = false +python-versions = "*" +files = [ + {file = "genson-1.2.2.tar.gz", hash = "sha256:8caf69aa10af7aee0e1a1351d1d06801f4696e005f06cedef438635384346a16"}, +] + +[[package]] +name = "greenlet" +version = "3.0.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + [[package]] name = "idna" version = "3.6" @@ -206,6 +508,20 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "isodate" +version = "0.6.1" +description = "An ISO 8601 date/time/duration parser and formatter" +optional = false +python-versions = "*" +files = [ + {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, + {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, +] + +[package.dependencies] +six = "*" + [[package]] name = "jinja2" version = "3.1.2" @@ -223,6 +539,17 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jsonref" +version = "0.3.0" +description = "jsonref is a library for automatic dereferencing of JSON Reference objects for Python." +optional = false +python-versions = ">=3.3,<4.0" +files = [ + {file = "jsonref-0.3.0-py3-none-any.whl", hash = "sha256:9480ad1b500f7e795daeb0ef29f9c55ae3a9ab38fb8d6659b6f4868acb5a5bc8"}, + {file = "jsonref-0.3.0.tar.gz", hash = "sha256:68b330c6815dc0d490dbb3d65ccda265ddde9f7856fd2f3322f971d456ea7549"}, +] + [[package]] name = "jsonschema" version = "3.2.0" @@ -303,6 +630,20 @@ files = [ {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] +[[package]] +name = "mirakuru" +version = "2.5.2" +description = "Process executor (not only) for tests." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mirakuru-2.5.2-py3-none-any.whl", hash = "sha256:90c2d90a8cf14349b2f33e6db30a16acd855499811e0312e56cf80ceacf2d3e5"}, + {file = "mirakuru-2.5.2.tar.gz", hash = "sha256:41ca583d355eb7a6cfdc21c1aea549979d685c27b57239b88725434f115a7132"}, +] + +[package.dependencies] +psutil = {version = ">=4.0.0", markers = "sys_platform != \"cygwin\""} + [[package]] name = "mypy" version = "1.8.0" @@ -361,6 +702,121 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "numpy" +version = "1.26.3" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:806dd64230dbbfaca8a27faa64e2f414bf1c6622ab78cc4264f7f5f028fee3bf"}, + {file = "numpy-1.26.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f98011ba4ab17f46f80f7f8f1c291ee7d855fcef0a5a98db80767a468c85cd"}, + {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d45b3ec2faed4baca41c76617fcdcfa4f684ff7a151ce6fc78ad3b6e85af0a6"}, + {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdd2b45bf079d9ad90377048e2747a0c82351989a2165821f0c96831b4a2a54b"}, + {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:211ddd1e94817ed2d175b60b6374120244a4dd2287f4ece45d49228b4d529178"}, + {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1240f767f69d7c4c8a29adde2310b871153df9b26b5cb2b54a561ac85146485"}, + {file = "numpy-1.26.3-cp310-cp310-win32.whl", hash = "sha256:21a9484e75ad018974a2fdaa216524d64ed4212e418e0a551a2d83403b0531d3"}, + {file = "numpy-1.26.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e1591f6ae98bcfac2a4bbf9221c0b92ab49762228f38287f6eeb5f3f55905ce"}, + {file = "numpy-1.26.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b831295e5472954104ecb46cd98c08b98b49c69fdb7040483aff799a755a7374"}, + {file = "numpy-1.26.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e87562b91f68dd8b1c39149d0323b42e0082db7ddb8e934ab4c292094d575d6"}, + {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c66d6fec467e8c0f975818c1796d25c53521124b7cfb760114be0abad53a0a2"}, + {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f25e2811a9c932e43943a2615e65fc487a0b6b49218899e62e426e7f0a57eeda"}, + {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af36e0aa45e25c9f57bf684b1175e59ea05d9a7d3e8e87b7ae1a1da246f2767e"}, + {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:51c7f1b344f302067b02e0f5b5d2daa9ed4a721cf49f070280ac202738ea7f00"}, + {file = "numpy-1.26.3-cp311-cp311-win32.whl", hash = "sha256:7ca4f24341df071877849eb2034948459ce3a07915c2734f1abb4018d9c49d7b"}, + {file = "numpy-1.26.3-cp311-cp311-win_amd64.whl", hash = "sha256:39763aee6dfdd4878032361b30b2b12593fb445ddb66bbac802e2113eb8a6ac4"}, + {file = "numpy-1.26.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a7081fd19a6d573e1a05e600c82a1c421011db7935ed0d5c483e9dd96b99cf13"}, + {file = "numpy-1.26.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12c70ac274b32bc00c7f61b515126c9205323703abb99cd41836e8125ea0043e"}, + {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f784e13e598e9594750b2ef6729bcd5a47f6cfe4a12cca13def35e06d8163e3"}, + {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f24750ef94d56ce6e33e4019a8a4d68cfdb1ef661a52cdaee628a56d2437419"}, + {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:77810ef29e0fb1d289d225cabb9ee6cf4d11978a00bb99f7f8ec2132a84e0166"}, + {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8ed07a90f5450d99dad60d3799f9c03c6566709bd53b497eb9ccad9a55867f36"}, + {file = "numpy-1.26.3-cp312-cp312-win32.whl", hash = "sha256:f73497e8c38295aaa4741bdfa4fda1a5aedda5473074369eca10626835445511"}, + {file = "numpy-1.26.3-cp312-cp312-win_amd64.whl", hash = "sha256:da4b0c6c699a0ad73c810736303f7fbae483bcb012e38d7eb06a5e3b432c981b"}, + {file = "numpy-1.26.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1666f634cb3c80ccbd77ec97bc17337718f56d6658acf5d3b906ca03e90ce87f"}, + {file = "numpy-1.26.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18c3319a7d39b2c6a9e3bb75aab2304ab79a811ac0168a671a62e6346c29b03f"}, + {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b7e807d6888da0db6e7e75838444d62495e2b588b99e90dd80c3459594e857b"}, + {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4d362e17bcb0011738c2d83e0a65ea8ce627057b2fdda37678f4374a382a137"}, + {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b8c275f0ae90069496068c714387b4a0eba5d531aace269559ff2b43655edd58"}, + {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc0743f0302b94f397a4a65a660d4cd24267439eb16493fb3caad2e4389bccbb"}, + {file = "numpy-1.26.3-cp39-cp39-win32.whl", hash = "sha256:9bc6d1a7f8cedd519c4b7b1156d98e051b726bf160715b769106661d567b3f03"}, + {file = "numpy-1.26.3-cp39-cp39-win_amd64.whl", hash = "sha256:867e3644e208c8922a3be26fc6bbf112a035f50f0a86497f98f228c50c607bb2"}, + {file = "numpy-1.26.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3c67423b3703f8fbd90f5adaa37f85b5794d3366948efe9a5190a5f3a83fc34e"}, + {file = "numpy-1.26.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46f47ee566d98849323f01b349d58f2557f02167ee301e5e28809a8c0e27a2d0"}, + {file = "numpy-1.26.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a8474703bffc65ca15853d5fd4d06b18138ae90c17c8d12169968e998e448bb5"}, + {file = "numpy-1.26.3.tar.gz", hash = "sha256:697df43e2b6310ecc9d95f05d5ef20eacc09c7c4ecc9da3f235d39e71b7da1e4"}, +] + +[[package]] +name = "orjson" +version = "3.9.10" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "orjson-3.9.10-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c18a4da2f50050a03d1da5317388ef84a16013302a5281d6f64e4a3f406aabc4"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5148bab4d71f58948c7c39d12b14a9005b6ab35a0bdf317a8ade9a9e4d9d0bd5"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cf7837c3b11a2dfb589f8530b3cff2bd0307ace4c301e8997e95c7468c1378e"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c62b6fa2961a1dcc51ebe88771be5319a93fd89bd247c9ddf732bc250507bc2b"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb3922a7a804755bbe6b5be9b312e746137a03600f488290318936c1a2d4dc"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1234dc92d011d3554d929b6cf058ac4a24d188d97be5e04355f1b9223e98bbe9"}, + {file = "orjson-3.9.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:06ad5543217e0e46fd7ab7ea45d506c76f878b87b1b4e369006bdb01acc05a83"}, + {file = "orjson-3.9.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4fd72fab7bddce46c6826994ce1e7de145ae1e9e106ebb8eb9ce1393ca01444d"}, + {file = "orjson-3.9.10-cp310-none-win32.whl", hash = "sha256:b5b7d4a44cc0e6ff98da5d56cde794385bdd212a86563ac321ca64d7f80c80d1"}, + {file = "orjson-3.9.10-cp310-none-win_amd64.whl", hash = "sha256:61804231099214e2f84998316f3238c4c2c4aaec302df12b21a64d72e2a135c7"}, + {file = "orjson-3.9.10-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cff7570d492bcf4b64cc862a6e2fb77edd5e5748ad715f487628f102815165e9"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8bc367f725dfc5cabeed1ae079d00369900231fbb5a5280cf0736c30e2adf7"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c812312847867b6335cfb264772f2a7e85b3b502d3a6b0586aa35e1858528ab1"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edd2856611e5050004f4722922b7b1cd6268da34102667bd49d2a2b18bafb81"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:674eb520f02422546c40401f4efaf8207b5e29e420c17051cddf6c02783ff5ca"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0dc4310da8b5f6415949bd5ef937e60aeb0eb6b16f95041b5e43e6200821fb"}, + {file = "orjson-3.9.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e99c625b8c95d7741fe057585176b1b8783d46ed4b8932cf98ee145c4facf499"}, + {file = "orjson-3.9.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec6f18f96b47299c11203edfbdc34e1b69085070d9a3d1f302810cc23ad36bf3"}, + {file = "orjson-3.9.10-cp311-none-win32.whl", hash = "sha256:ce0a29c28dfb8eccd0f16219360530bc3cfdf6bf70ca384dacd36e6c650ef8e8"}, + {file = "orjson-3.9.10-cp311-none-win_amd64.whl", hash = "sha256:cf80b550092cc480a0cbd0750e8189247ff45457e5a023305f7ef1bcec811616"}, + {file = "orjson-3.9.10-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:602a8001bdf60e1a7d544be29c82560a7b49319a0b31d62586548835bbe2c862"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f295efcd47b6124b01255d1491f9e46f17ef40d3d7eabf7364099e463fb45f0f"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92af0d00091e744587221e79f68d617b432425a7e59328ca4c496f774a356071"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5a02360e73e7208a872bf65a7554c9f15df5fe063dc047f79738998b0506a14"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:858379cbb08d84fe7583231077d9a36a1a20eb72f8c9076a45df8b083724ad1d"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666c6fdcaac1f13eb982b649e1c311c08d7097cbda24f32612dae43648d8db8d"}, + {file = "orjson-3.9.10-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3fb205ab52a2e30354640780ce4587157a9563a68c9beaf52153e1cea9aa0921"}, + {file = "orjson-3.9.10-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7ec960b1b942ee3c69323b8721df2a3ce28ff40e7ca47873ae35bfafeb4555ca"}, + {file = "orjson-3.9.10-cp312-none-win_amd64.whl", hash = "sha256:3e892621434392199efb54e69edfff9f699f6cc36dd9553c5bf796058b14b20d"}, + {file = "orjson-3.9.10-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8b9ba0ccd5a7f4219e67fbbe25e6b4a46ceef783c42af7dbc1da548eb28b6531"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e2ecd1d349e62e3960695214f40939bbfdcaeaaa62ccc638f8e651cf0970e5f"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f433be3b3f4c66016d5a20e5b4444ef833a1f802ced13a2d852c637f69729c1"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4689270c35d4bb3102e103ac43c3f0b76b169760aff8bcf2d401a3e0e58cdb7f"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bd176f528a8151a6efc5359b853ba3cc0e82d4cd1fab9c1300c5d957dc8f48c"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a2ce5ea4f71681623f04e2b7dadede3c7435dfb5e5e2d1d0ec25b35530e277b"}, + {file = "orjson-3.9.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:49f8ad582da6e8d2cf663c4ba5bf9f83cc052570a3a767487fec6af839b0e777"}, + {file = "orjson-3.9.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2a11b4b1a8415f105d989876a19b173f6cdc89ca13855ccc67c18efbd7cbd1f8"}, + {file = "orjson-3.9.10-cp38-none-win32.whl", hash = "sha256:a353bf1f565ed27ba71a419b2cd3db9d6151da426b61b289b6ba1422a702e643"}, + {file = "orjson-3.9.10-cp38-none-win_amd64.whl", hash = "sha256:e28a50b5be854e18d54f75ef1bb13e1abf4bc650ab9d635e4258c58e71eb6ad5"}, + {file = "orjson-3.9.10-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ee5926746232f627a3be1cc175b2cfad24d0170d520361f4ce3fa2fd83f09e1d"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a73160e823151f33cdc05fe2cea557c5ef12fdf276ce29bb4f1c571c8368a60"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c338ed69ad0b8f8f8920c13f529889fe0771abbb46550013e3c3d01e5174deef"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5869e8e130e99687d9e4be835116c4ebd83ca92e52e55810962446d841aba8de"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2c1e559d96a7f94a4f581e2a32d6d610df5840881a8cba8f25e446f4d792df3"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a3a3a72c9811b56adf8bcc829b010163bb2fc308877e50e9910c9357e78521"}, + {file = "orjson-3.9.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7f8fb7f5ecf4f6355683ac6881fd64b5bb2b8a60e3ccde6ff799e48791d8f864"}, + {file = "orjson-3.9.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c943b35ecdf7123b2d81d225397efddf0bce2e81db2f3ae633ead38e85cd5ade"}, + {file = "orjson-3.9.10-cp39-none-win32.whl", hash = "sha256:fb0b361d73f6b8eeceba47cd37070b5e6c9de5beaeaa63a1cb35c7e1a73ef088"}, + {file = "orjson-3.9.10-cp39-none-win_amd64.whl", hash = "sha256:b90f340cb6397ec7a854157fac03f0c82b744abdd1c0941a024c3c29d1340aff"}, + {file = "orjson-3.9.10.tar.gz", hash = "sha256:9ebbdbd6a046c304b1845e96fbcc5559cd296b4dfd3ad2509e33c4d9ce07d6a1"}, +] + +[[package]] +name = "overrides" +version = "7.4.0" +description = "A decorator to automatically detect mismatch when overriding a method." +optional = false +python-versions = ">=3.6" +files = [ + {file = "overrides-7.4.0-py3-none-any.whl", hash = "sha256:3ad24583f86d6d7a49049695efe9933e67ba62f0c7625d53c59fa832ce4b8b7d"}, + {file = "overrides-7.4.0.tar.gz", hash = "sha256:9502a3cca51f4fac40b5feca985b6703a5c1f6ad815588a7ca9e285b9dca6757"}, +] + [[package]] name = "packaging" version = "23.2" @@ -372,6 +828,89 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "pandas" +version = "2.1.4" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bdec823dc6ec53f7a6339a0e34c68b144a7a1fd28d80c260534c39c62c5bf8c9"}, + {file = "pandas-2.1.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:294d96cfaf28d688f30c918a765ea2ae2e0e71d3536754f4b6de0ea4a496d034"}, + {file = "pandas-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b728fb8deba8905b319f96447a27033969f3ea1fea09d07d296c9030ab2ed1d"}, + {file = "pandas-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00028e6737c594feac3c2df15636d73ace46b8314d236100b57ed7e4b9ebe8d9"}, + {file = "pandas-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:426dc0f1b187523c4db06f96fb5c8d1a845e259c99bda74f7de97bd8a3bb3139"}, + {file = "pandas-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:f237e6ca6421265643608813ce9793610ad09b40154a3344a088159590469e46"}, + {file = "pandas-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b7d852d16c270e4331f6f59b3e9aa23f935f5c4b0ed2d0bc77637a8890a5d092"}, + {file = "pandas-2.1.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd7d5f2f54f78164b3d7a40f33bf79a74cdee72c31affec86bfcabe7e0789821"}, + {file = "pandas-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa6e92e639da0d6e2017d9ccff563222f4eb31e4b2c3cf32a2a392fc3103c0d"}, + {file = "pandas-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d797591b6846b9db79e65dc2d0d48e61f7db8d10b2a9480b4e3faaddc421a171"}, + {file = "pandas-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2d3e7b00f703aea3945995ee63375c61b2e6aa5aa7871c5d622870e5e137623"}, + {file = "pandas-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:dc9bf7ade01143cddc0074aa6995edd05323974e6e40d9dbde081021ded8510e"}, + {file = "pandas-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:482d5076e1791777e1571f2e2d789e940dedd927325cc3cb6d0800c6304082f6"}, + {file = "pandas-2.1.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8a706cfe7955c4ca59af8c7a0517370eafbd98593155b48f10f9811da440248b"}, + {file = "pandas-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0513a132a15977b4a5b89aabd304647919bc2169eac4c8536afb29c07c23540"}, + {file = "pandas-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9f17f2b6fc076b2a0078862547595d66244db0f41bf79fc5f64a5c4d635bead"}, + {file = "pandas-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:45d63d2a9b1b37fa6c84a68ba2422dc9ed018bdaa668c7f47566a01188ceeec1"}, + {file = "pandas-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:f69b0c9bb174a2342818d3e2778584e18c740d56857fc5cdb944ec8bbe4082cf"}, + {file = "pandas-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3f06bda01a143020bad20f7a85dd5f4a1600112145f126bc9e3e42077c24ef34"}, + {file = "pandas-2.1.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab5796839eb1fd62a39eec2916d3e979ec3130509930fea17fe6f81e18108f6a"}, + {file = "pandas-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edbaf9e8d3a63a9276d707b4d25930a262341bca9874fcb22eff5e3da5394732"}, + {file = "pandas-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ebfd771110b50055712b3b711b51bee5d50135429364d0498e1213a7adc2be8"}, + {file = "pandas-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8ea107e0be2aba1da619cc6ba3f999b2bfc9669a83554b1904ce3dd9507f0860"}, + {file = "pandas-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:d65148b14788b3758daf57bf42725caa536575da2b64df9964c563b015230984"}, + {file = "pandas-2.1.4.tar.gz", hash = "sha256:fcb68203c833cc735321512e13861358079a96c174a61f5116a1de89c58c0ef7"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""}, + {version = ">=1.22.4,<2", markers = "python_version < \"3.11\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.1" + +[package.extras] +all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"] +aws = ["s3fs (>=2022.05.0)"] +clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"] +compression = ["zstandard (>=0.17.0)"] +computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"] +feather = ["pyarrow (>=7.0.0)"] +fss = ["fsspec (>=2022.05.0)"] +gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"] +hdf5 = ["tables (>=3.7.0)"] +html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"] +mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"] +parquet = ["pyarrow (>=7.0.0)"] +performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"] +plot = ["matplotlib (>=3.6.1)"] +postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"] +spss = ["pyreadstat (>=1.1.5)"] +sql-other = ["SQLAlchemy (>=1.4.36)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.8.0)"] + +[[package]] +name = "pandas-stubs" +version = "2.1.4.231227" +description = "Type annotations for pandas" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas_stubs-2.1.4.231227-py3-none-any.whl", hash = "sha256:211fc23e6ae87073bdf41dbf362c4a4d85e1e3477cb078dbac3da6c7fdaefba8"}, + {file = "pandas_stubs-2.1.4.231227.tar.gz", hash = "sha256:3ea29ef001e9e44985f5ebde02d4413f94891ef6ec7e5056fb07d125be796c23"}, +] + +[package.dependencies] +numpy = {version = ">=1.26.0", markers = "python_version < \"3.13\""} +types-pytz = ">=2022.1.1" + [[package]] name = "pdoc" version = "14.3.0" @@ -391,6 +930,55 @@ pygments = ">=2.12.0" [package.extras] dev = ["hypothesis", "mypy", "pdoc-pyo3-sample-library (==1.0.11)", "pygments (>=2.14.0)", "pytest", "pytest-cov", "pytest-timeout", "ruff", "tox", "types-pygments"] +[[package]] +name = "pendulum" +version = "2.1.2" +description = "Python datetimes made easy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "pendulum-2.1.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:b6c352f4bd32dff1ea7066bd31ad0f71f8d8100b9ff709fb343f3b86cee43efe"}, + {file = "pendulum-2.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:318f72f62e8e23cd6660dbafe1e346950281a9aed144b5c596b2ddabc1d19739"}, + {file = "pendulum-2.1.2-cp35-cp35m-macosx_10_15_x86_64.whl", hash = "sha256:0731f0c661a3cb779d398803655494893c9f581f6488048b3fb629c2342b5394"}, + {file = "pendulum-2.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3481fad1dc3f6f6738bd575a951d3c15d4b4ce7c82dce37cf8ac1483fde6e8b0"}, + {file = "pendulum-2.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9702069c694306297ed362ce7e3c1ef8404ac8ede39f9b28b7c1a7ad8c3959e3"}, + {file = "pendulum-2.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:fb53ffa0085002ddd43b6ca61a7b34f2d4d7c3ed66f931fe599e1a531b42af9b"}, + {file = "pendulum-2.1.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:c501749fdd3d6f9e726086bf0cd4437281ed47e7bca132ddb522f86a1645d360"}, + {file = "pendulum-2.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c807a578a532eeb226150d5006f156632df2cc8c5693d778324b43ff8c515dd0"}, + {file = "pendulum-2.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2d1619a721df661e506eff8db8614016f0720ac171fe80dda1333ee44e684087"}, + {file = "pendulum-2.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f888f2d2909a414680a29ae74d0592758f2b9fcdee3549887779cd4055e975db"}, + {file = "pendulum-2.1.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:e95d329384717c7bf627bf27e204bc3b15c8238fa8d9d9781d93712776c14002"}, + {file = "pendulum-2.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4c9c689747f39d0d02a9f94fcee737b34a5773803a64a5fdb046ee9cac7442c5"}, + {file = "pendulum-2.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1245cd0075a3c6d889f581f6325dd8404aca5884dea7223a5566c38aab94642b"}, + {file = "pendulum-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:db0a40d8bcd27b4fb46676e8eb3c732c67a5a5e6bfab8927028224fbced0b40b"}, + {file = "pendulum-2.1.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f5e236e7730cab1644e1b87aca3d2ff3e375a608542e90fe25685dae46310116"}, + {file = "pendulum-2.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:de42ea3e2943171a9e95141f2eecf972480636e8e484ccffaf1e833929e9e052"}, + {file = "pendulum-2.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7c5ec650cb4bec4c63a89a0242cc8c3cebcec92fcfe937c417ba18277d8560be"}, + {file = "pendulum-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:33fb61601083f3eb1d15edeb45274f73c63b3c44a8524703dc143f4212bf3269"}, + {file = "pendulum-2.1.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:29c40a6f2942376185728c9a0347d7c0f07905638c83007e1d262781f1e6953a"}, + {file = "pendulum-2.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:94b1fc947bfe38579b28e1cccb36f7e28a15e841f30384b5ad6c5e31055c85d7"}, + {file = "pendulum-2.1.2.tar.gz", hash = "sha256:b06a0ca1bfe41c990bbf0c029f0b6501a7f2ec4e38bfec730712015e8860f207"}, +] + +[package.dependencies] +python-dateutil = ">=2.6,<3.0" +pytzdata = ">=2020.1" + +[[package]] +name = "platformdirs" +version = "4.1.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + [[package]] name = "pluggy" version = "1.3.0" @@ -406,52 +994,263 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "port-for" +version = "0.7.2" +description = "Utility that helps with local TCP ports management. It can find an unused TCP localhost port and remember the association." +optional = false +python-versions = ">=3.8" +files = [ + {file = "port-for-0.7.2.tar.gz", hash = "sha256:074f29335130578aa42fef3726985e57d01c15189e509633a8a1b0b7f9226349"}, + {file = "port_for-0.7.2-py3-none-any.whl", hash = "sha256:16b279ab4f210bad33515c45bd9af0c6e048ab24c3b6bbd9cfc7e451782617df"}, +] + +[[package]] +name = "psutil" +version = "5.9.7" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "psutil-5.9.7-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0bd41bf2d1463dfa535942b2a8f0e958acf6607ac0be52265ab31f7923bcd5e6"}, + {file = "psutil-5.9.7-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:5794944462509e49d4d458f4dbfb92c47539e7d8d15c796f141f474010084056"}, + {file = "psutil-5.9.7-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:fe361f743cb3389b8efda21980d93eb55c1f1e3898269bc9a2a1d0bb7b1f6508"}, + {file = "psutil-5.9.7-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:e469990e28f1ad738f65a42dcfc17adaed9d0f325d55047593cb9033a0ab63df"}, + {file = "psutil-5.9.7-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:3c4747a3e2ead1589e647e64aad601981f01b68f9398ddf94d01e3dc0d1e57c7"}, + {file = "psutil-5.9.7-cp27-none-win32.whl", hash = "sha256:1d4bc4a0148fdd7fd8f38e0498639ae128e64538faa507df25a20f8f7fb2341c"}, + {file = "psutil-5.9.7-cp27-none-win_amd64.whl", hash = "sha256:4c03362e280d06bbbfcd52f29acd79c733e0af33d707c54255d21029b8b32ba6"}, + {file = "psutil-5.9.7-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ea36cc62e69a13ec52b2f625c27527f6e4479bca2b340b7a452af55b34fcbe2e"}, + {file = "psutil-5.9.7-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1132704b876e58d277168cd729d64750633d5ff0183acf5b3c986b8466cd0284"}, + {file = "psutil-5.9.7-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8b7f07948f1304497ce4f4684881250cd859b16d06a1dc4d7941eeb6233bfe"}, + {file = "psutil-5.9.7-cp36-cp36m-win32.whl", hash = "sha256:b27f8fdb190c8c03914f908a4555159327d7481dac2f01008d483137ef3311a9"}, + {file = "psutil-5.9.7-cp36-cp36m-win_amd64.whl", hash = "sha256:44969859757f4d8f2a9bd5b76eba8c3099a2c8cf3992ff62144061e39ba8568e"}, + {file = "psutil-5.9.7-cp37-abi3-win32.whl", hash = "sha256:c727ca5a9b2dd5193b8644b9f0c883d54f1248310023b5ad3e92036c5e2ada68"}, + {file = "psutil-5.9.7-cp37-abi3-win_amd64.whl", hash = "sha256:f37f87e4d73b79e6c5e749440c3113b81d1ee7d26f21c19c47371ddea834f414"}, + {file = "psutil-5.9.7-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:032f4f2c909818c86cea4fe2cc407f1c0f0cde8e6c6d702b28b8ce0c0d143340"}, + {file = "psutil-5.9.7.tar.gz", hash = "sha256:3f02134e82cfb5d089fddf20bb2e03fd5cd52395321d1c8458a9e58500ff417c"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "psycopg" +version = "3.1.17" +description = "PostgreSQL database adapter for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "psycopg-3.1.17-py3-none-any.whl", hash = "sha256:96b7b13af6d5a514118b759a66b2799a8a4aa78675fa6bb0d3f7d52d67eff002"}, + {file = "psycopg-3.1.17.tar.gz", hash = "sha256:437e7d7925459f21de570383e2e10542aceb3b9cb972ce957fdd3826ca47edc6"}, +] + +[package.dependencies] +psycopg-binary = {version = "3.1.17", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} +psycopg-pool = {version = "*", optional = true, markers = "extra == \"pool\""} +typing-extensions = ">=4.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +binary = ["psycopg-binary (==3.1.17)"] +c = ["psycopg-c (==3.1.17)"] +dev = ["black (>=23.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.4.1)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] +docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] +pool = ["psycopg-pool"] +test = ["anyio (>=3.6.2,<4.0)", "mypy (>=1.4.1)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + +[[package]] +name = "psycopg-binary" +version = "3.1.17" +description = "PostgreSQL database adapter for Python -- C optimisation distribution" +optional = false +python-versions = ">=3.7" +files = [ + {file = "psycopg_binary-3.1.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9ba559eabb0ba1afd4e0504fa0b10e00a212cac0c4028b8a1c3b087b5c1e5de"}, + {file = "psycopg_binary-3.1.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2b2a689eaede08cf91a36b10b0da6568dd6e4669200f201e082639816737992b"}, + {file = "psycopg_binary-3.1.17-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a16abab0c1abc58feb6ab11d78d0f8178a67c3586bd70628ec7c0218ec04c4ef"}, + {file = "psycopg_binary-3.1.17-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73e7097b81cad9ae358334e3cec625246bb3b8013ae6bb287758dd6435e12f65"}, + {file = "psycopg_binary-3.1.17-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:67a5b93101bc85a95a189c0a23d02a29cf06c1080a695a0dedfdd50dd734662a"}, + {file = "psycopg_binary-3.1.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:751b31c2faae0348f87f22b45ef58f704bdcfc2abdd680fa0c743c124071157e"}, + {file = "psycopg_binary-3.1.17-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b447ea765e71bc33a82cf070bba814b1efa77967442d116b95ccef8ce5da7631"}, + {file = "psycopg_binary-3.1.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d2e9ed88d9a6a475c67bf70fc8285e88ccece0391727c7701e5a512e0eafbb05"}, + {file = "psycopg_binary-3.1.17-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a89f36bf7b612ff6ed3e789bd987cbd0787cf0d66c49386fa3bad816dd7bee87"}, + {file = "psycopg_binary-3.1.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ccbe8b2ec444763a51ecb1213befcbb75defc1ef36e7dd5dff501a23d7ce8cf"}, + {file = "psycopg_binary-3.1.17-cp310-cp310-win_amd64.whl", hash = "sha256:adb670031b27949c9dc5cf585c4a5a6b4469d3879fd2fb9d39b6d53e5f66b9bc"}, + {file = "psycopg_binary-3.1.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0227885686c2cc0104ceb22d6eebc732766e9ad48710408cb0123237432e5435"}, + {file = "psycopg_binary-3.1.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9124b6db07e8d8b11f4512b8b56cbe136bf1b7d0417d1280e62291a9dcad4408"}, + {file = "psycopg_binary-3.1.17-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a46f77ba0ca7c5a5449b777170a518fa7820e1710edb40e777c9798f00d033"}, + {file = "psycopg_binary-3.1.17-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f5f5bcbb772d8c243d605fc7151beec760dd27532d42145a58fb74ef9c5fbf2"}, + {file = "psycopg_binary-3.1.17-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:267a82548c21476120e43dc72b961f1af52c380c0b4c951bdb34cf14cb26bd35"}, + {file = "psycopg_binary-3.1.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b20013051f1fd7d02b8d0766cfe8d009e8078babc00a6d39bc7e2d50a7b96af"}, + {file = "psycopg_binary-3.1.17-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c5c38129cc79d7e3ba553035b9962a442171e9f97bb1b8795c0885213f206f3"}, + {file = "psycopg_binary-3.1.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d01c4faae66de60fcd3afd3720dcc8ffa03bc2087f898106da127774db12aac5"}, + {file = "psycopg_binary-3.1.17-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e6ae27b0617ad3809449964b5e901b21acff8e306abacb8ba71d5ee7c8c47eeb"}, + {file = "psycopg_binary-3.1.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:40af298b209dd77ca2f3e7eb3fbcfb87a25999fc015fcd14140bde030a164c7e"}, + {file = "psycopg_binary-3.1.17-cp311-cp311-win_amd64.whl", hash = "sha256:7b4e4c2b05f3b431e9026e82590b217e87696e7a7548f512ae8059d59fa8af3b"}, + {file = "psycopg_binary-3.1.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ea425a8dcd808a7232a5417d2633bfa543da583a2701b5228e9e29989a50deda"}, + {file = "psycopg_binary-3.1.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3f1196d76860e72d338fab0d2b6722e8d47e2285d693e366ae36011c4a5898a"}, + {file = "psycopg_binary-3.1.17-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1e867c2a729348df218a14ba1b862e627177fd57c7b4f3db0b4c708f6d03696"}, + {file = "psycopg_binary-3.1.17-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0711e46361ea3047cd049868419d030c8236a9dea7e9ed1f053cbd61a853ec9"}, + {file = "psycopg_binary-3.1.17-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1c0115bdf80cf6c8c9109cb10cf6f650fd1a8d841f884925e8cb12f34eb5371"}, + {file = "psycopg_binary-3.1.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d0d154c780cc7b28a3a0886e8a4b18689202a1dbb522b3c771eb3a1289cf7c3"}, + {file = "psycopg_binary-3.1.17-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f4028443bf25c1e04ecffdc552c0a98d826903dec76a1568dfddf5ebbbb03db7"}, + {file = "psycopg_binary-3.1.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf424d92dd7e94705b31625b02d396297a7c8fab4b6f7de8dba6388323a7b71c"}, + {file = "psycopg_binary-3.1.17-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:00377f6963ee7e4bf71cab17c2c235ef0624df9483f3b615d86aa24cde889d42"}, + {file = "psycopg_binary-3.1.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9690a535d9ccd361bbc3590bfce7fe679e847f44fa7cc97f3b885f4744ca8a2c"}, + {file = "psycopg_binary-3.1.17-cp312-cp312-win_amd64.whl", hash = "sha256:6b2ae342d69684555bfe77aed5546d125b4a99012e0b83a8b3da68c8829f0935"}, + {file = "psycopg_binary-3.1.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:86bb3656c8d744cc1e42003414cd6c765117d70aa23da6c0f4ff2b826e0fd0fd"}, + {file = "psycopg_binary-3.1.17-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c10b7713e3ed31df7319c2a72d5fea5a2536476d7695a3e1d18a1f289060997c"}, + {file = "psycopg_binary-3.1.17-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12eab8bc91b4ba01b2ecee3b5b80501934b198f6e1f8d4b13596f3f38ba6e762"}, + {file = "psycopg_binary-3.1.17-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a728beefd89b430ebe2729d04ba10e05036b5e9d01648da60436000d2fcd242"}, + {file = "psycopg_binary-3.1.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61104b8e7a43babf2bbaa36c08e31a12023e2f967166e99d6b052b11a4c7db06"}, + {file = "psycopg_binary-3.1.17-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:02cd2eb62ffc56f8c847d68765cbf461b3d11b438fe48951e44b6c563ec27d18"}, + {file = "psycopg_binary-3.1.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ca1757a6e080086f7234dc45684e81a47a66a6dd492a37d6ce38c58a1a93e9ff"}, + {file = "psycopg_binary-3.1.17-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:6e3543edc18553e31a3884af3cd7eea43d6c44532d8b9b16f3e743cdf6cfe6c5"}, + {file = "psycopg_binary-3.1.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:914254849486e14aa931b0b3382cd16887f1507068ffba775cbdc5a55fe9ef19"}, + {file = "psycopg_binary-3.1.17-cp37-cp37m-win_amd64.whl", hash = "sha256:92fad8f1aa80a5ab316c0493dc6d1b54c1dba21937e43eea7296ff4a0ccc071e"}, + {file = "psycopg_binary-3.1.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6d4f2e15d33ed4f9776fdf23683512d76f4e7825c4b80677e9e3ce6c1b193ff2"}, + {file = "psycopg_binary-3.1.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4fa26836ce074a1104249378727e1f239a01530f36bae16e77cf6c50968599b4"}, + {file = "psycopg_binary-3.1.17-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d54bcf2dfc0880bf13f38512d44b194c092794e4ee9e01d804bc6cd3eed9bfb7"}, + {file = "psycopg_binary-3.1.17-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e28024204dc0c61094268c682041d2becfedfea2e3b46bed5f6138239304d98"}, + {file = "psycopg_binary-3.1.17-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b1ec6895cab887b92c303565617f994c9b9db53befda81fa2a31b76fe8a3ab1"}, + {file = "psycopg_binary-3.1.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:420c1eb1626539c261cf3fbe099998da73eb990f9ce1a34da7feda414012ea5f"}, + {file = "psycopg_binary-3.1.17-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:83404a353240fdff5cfe9080665fdfdcaa2d4d0c5112e15b0a2fe2e59200ed57"}, + {file = "psycopg_binary-3.1.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a0c4ba73f9e7721dd6cc3e6953016652dbac206f654229b7a1a8ac182b16e689"}, + {file = "psycopg_binary-3.1.17-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f6898bf1ca5aa01115807643138e3e20ec603b17a811026bc4a49d43055720a7"}, + {file = "psycopg_binary-3.1.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6b40fa54a02825d3d6a8009d9a82a2b4fad80387acf2b8fd6d398fd2813cb2d9"}, + {file = "psycopg_binary-3.1.17-cp38-cp38-win_amd64.whl", hash = "sha256:78ebb43dca7d5b41eee543cd005ee5a0256cecc74d84acf0fab4f025997b837e"}, + {file = "psycopg_binary-3.1.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:02ac573f5a6e79bb6df512b3a6279f01f033bbd45c47186e8872fee45f6681d0"}, + {file = "psycopg_binary-3.1.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:704f6393d758b12a4369887fe956b2a8c99e4aced839d9084de8e3f056015d40"}, + {file = "psycopg_binary-3.1.17-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0340ef87a888fd940796c909e038426f4901046f61856598582a817162c64984"}, + {file = "psycopg_binary-3.1.17-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a880e4113af3ab84d6a0991e3f85a2424924c8a182733ab8d964421df8b5190a"}, + {file = "psycopg_binary-3.1.17-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93921178b9a40c60c26e47eb44970f88c49fe484aaa3bb7ec02bb8b514eab3d9"}, + {file = "psycopg_binary-3.1.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a05400e9314fc30bc1364865ba9f6eaa2def42b5e7e67f71f9a4430f870023e"}, + {file = "psycopg_binary-3.1.17-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3e2cc2bbf37ff1cf11e8b871c294e3532636a3cf7f0c82518b7537158923d77b"}, + {file = "psycopg_binary-3.1.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a343261701a8f63f0d8268f7fd32be40ffe28d24b65d905404ca03e7281f7bb5"}, + {file = "psycopg_binary-3.1.17-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:dceb3930ec426623c0cacc78e447a90882981e8c49d6fea8d1e48850e24a0170"}, + {file = "psycopg_binary-3.1.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d613a23f8928f30acb2b6b2398cb7775ba9852e8968e15df13807ba0d3ebd565"}, + {file = "psycopg_binary-3.1.17-cp39-cp39-win_amd64.whl", hash = "sha256:d90c0531e9d591bde8cea04e75107fcddcc56811b638a34853436b23c9a3cb7d"}, +] + +[[package]] +name = "psycopg-pool" +version = "3.2.1" +description = "Connection Pool for Psycopg" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg-pool-3.2.1.tar.gz", hash = "sha256:6509a75c073590952915eddbba7ce8b8332a440a31e77bba69561483492829ad"}, + {file = "psycopg_pool-3.2.1-py3-none-any.whl", hash = "sha256:060b551d1b97a8d358c668be58b637780b884de14d861f4f5ecc48b7563aafb7"}, +] + +[package.dependencies] +typing-extensions = ">=4.4" + +[[package]] +name = "pyarrow" +version = "14.0.2" +description = "Python library for Apache Arrow" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyarrow-14.0.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:ba9fe808596c5dbd08b3aeffe901e5f81095baaa28e7d5118e01354c64f22807"}, + {file = "pyarrow-14.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:22a768987a16bb46220cef490c56c671993fbee8fd0475febac0b3e16b00a10e"}, + {file = "pyarrow-14.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dbba05e98f247f17e64303eb876f4a80fcd32f73c7e9ad975a83834d81f3fda"}, + {file = "pyarrow-14.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a898d134d00b1eca04998e9d286e19653f9d0fcb99587310cd10270907452a6b"}, + {file = "pyarrow-14.0.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:87e879323f256cb04267bb365add7208f302df942eb943c93a9dfeb8f44840b1"}, + {file = "pyarrow-14.0.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:76fc257559404ea5f1306ea9a3ff0541bf996ff3f7b9209fc517b5e83811fa8e"}, + {file = "pyarrow-14.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0c4a18e00f3a32398a7f31da47fefcd7a927545b396e1f15d0c85c2f2c778cd"}, + {file = "pyarrow-14.0.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:87482af32e5a0c0cce2d12eb3c039dd1d853bd905b04f3f953f147c7a196915b"}, + {file = "pyarrow-14.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:059bd8f12a70519e46cd64e1ba40e97eae55e0cbe1695edd95384653d7626b23"}, + {file = "pyarrow-14.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f16111f9ab27e60b391c5f6d197510e3ad6654e73857b4e394861fc79c37200"}, + {file = "pyarrow-14.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06ff1264fe4448e8d02073f5ce45a9f934c0f3db0a04460d0b01ff28befc3696"}, + {file = "pyarrow-14.0.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6dd4f4b472ccf4042f1eab77e6c8bce574543f54d2135c7e396f413046397d5a"}, + {file = "pyarrow-14.0.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:32356bfb58b36059773f49e4e214996888eeea3a08893e7dbde44753799b2a02"}, + {file = "pyarrow-14.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:52809ee69d4dbf2241c0e4366d949ba035cbcf48409bf404f071f624ed313a2b"}, + {file = "pyarrow-14.0.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:c87824a5ac52be210d32906c715f4ed7053d0180c1060ae3ff9b7e560f53f944"}, + {file = "pyarrow-14.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a25eb2421a58e861f6ca91f43339d215476f4fe159eca603c55950c14f378cc5"}, + {file = "pyarrow-14.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c1da70d668af5620b8ba0a23f229030a4cd6c5f24a616a146f30d2386fec422"}, + {file = "pyarrow-14.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cc61593c8e66194c7cdfae594503e91b926a228fba40b5cf25cc593563bcd07"}, + {file = "pyarrow-14.0.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:78ea56f62fb7c0ae8ecb9afdd7893e3a7dbeb0b04106f5c08dbb23f9c0157591"}, + {file = "pyarrow-14.0.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:37c233ddbce0c67a76c0985612fef27c0c92aef9413cf5aa56952f359fcb7379"}, + {file = "pyarrow-14.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:e4b123ad0f6add92de898214d404e488167b87b5dd86e9a434126bc2b7a5578d"}, + {file = "pyarrow-14.0.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e354fba8490de258be7687f341bc04aba181fc8aa1f71e4584f9890d9cb2dec2"}, + {file = "pyarrow-14.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:20e003a23a13da963f43e2b432483fdd8c38dc8882cd145f09f21792e1cf22a1"}, + {file = "pyarrow-14.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc0de7575e841f1595ac07e5bc631084fd06ca8b03c0f2ecece733d23cd5102a"}, + {file = "pyarrow-14.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66e986dc859712acb0bd45601229021f3ffcdfc49044b64c6d071aaf4fa49e98"}, + {file = "pyarrow-14.0.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:f7d029f20ef56673a9730766023459ece397a05001f4e4d13805111d7c2108c0"}, + {file = "pyarrow-14.0.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:209bac546942b0d8edc8debda248364f7f668e4aad4741bae58e67d40e5fcf75"}, + {file = "pyarrow-14.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1e6987c5274fb87d66bb36816afb6f65707546b3c45c44c28e3c4133c010a881"}, + {file = "pyarrow-14.0.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a01d0052d2a294a5f56cc1862933014e696aa08cc7b620e8c0cce5a5d362e976"}, + {file = "pyarrow-14.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a51fee3a7db4d37f8cda3ea96f32530620d43b0489d169b285d774da48ca9785"}, + {file = "pyarrow-14.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64df2bf1ef2ef14cee531e2dfe03dd924017650ffaa6f9513d7a1bb291e59c15"}, + {file = "pyarrow-14.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c0fa3bfdb0305ffe09810f9d3e2e50a2787e3a07063001dcd7adae0cee3601a"}, + {file = "pyarrow-14.0.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c65bf4fd06584f058420238bc47a316e80dda01ec0dfb3044594128a6c2db794"}, + {file = "pyarrow-14.0.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:63ac901baec9369d6aae1cbe6cca11178fb018a8d45068aaf5bb54f94804a866"}, + {file = "pyarrow-14.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:75ee0efe7a87a687ae303d63037d08a48ef9ea0127064df18267252cfe2e9541"}, + {file = "pyarrow-14.0.2.tar.gz", hash = "sha256:36cef6ba12b499d864d1def3e990f97949e0b79400d08b7cf74504ffbd3eb025"}, +] + +[package.dependencies] +numpy = ">=1.16.6" + +[[package]] +name = "pyarrow-stubs" +version = "10.0.1.7" +description = "Type annotations for pyarrow" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "pyarrow_stubs-10.0.1.7-py3-none-any.whl", hash = "sha256:cccc7a46eddeea4e3cb85330eb8972c116a615da6188b8ae1f7a44cb724b21ac"}, +] + [[package]] name = "pydantic" -version = "1.9.2" +version = "1.10.13" description = "Data validation and settings management using python type hints" optional = false -python-versions = ">=3.6.1" -files = [ - {file = "pydantic-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c9e04a6cdb7a363d7cb3ccf0efea51e0abb48e180c0d31dca8d247967d85c6e"}, - {file = "pydantic-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fafe841be1103f340a24977f61dee76172e4ae5f647ab9e7fd1e1fca51524f08"}, - {file = "pydantic-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afacf6d2a41ed91fc631bade88b1d319c51ab5418870802cedb590b709c5ae3c"}, - {file = "pydantic-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ee0d69b2a5b341fc7927e92cae7ddcfd95e624dfc4870b32a85568bd65e6131"}, - {file = "pydantic-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ff68fc85355532ea77559ede81f35fff79a6a5543477e168ab3a381887caea76"}, - {file = "pydantic-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c0f5e142ef8217019e3eef6ae1b6b55f09a7a15972958d44fbd228214cede567"}, - {file = "pydantic-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:615661bfc37e82ac677543704437ff737418e4ea04bef9cf11c6d27346606044"}, - {file = "pydantic-1.9.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:328558c9f2eed77bd8fffad3cef39dbbe3edc7044517f4625a769d45d4cf7555"}, - {file = "pydantic-1.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bd446bdb7755c3a94e56d7bdfd3ee92396070efa8ef3a34fab9579fe6aa1d84"}, - {file = "pydantic-1.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0b214e57623a535936005797567231a12d0da0c29711eb3514bc2b3cd008d0f"}, - {file = "pydantic-1.9.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d8ce3fb0841763a89322ea0432f1f59a2d3feae07a63ea2c958b2315e1ae8adb"}, - {file = "pydantic-1.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b34ba24f3e2d0b39b43f0ca62008f7ba962cff51efa56e64ee25c4af6eed987b"}, - {file = "pydantic-1.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:84d76ecc908d917f4684b354a39fd885d69dd0491be175f3465fe4b59811c001"}, - {file = "pydantic-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4de71c718c9756d679420c69f216776c2e977459f77e8f679a4a961dc7304a56"}, - {file = "pydantic-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5803ad846cdd1ed0d97eb00292b870c29c1f03732a010e66908ff48a762f20e4"}, - {file = "pydantic-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8c5360a0297a713b4123608a7909e6869e1b56d0e96eb0d792c27585d40757f"}, - {file = "pydantic-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:cdb4272678db803ddf94caa4f94f8672e9a46bae4a44f167095e4d06fec12979"}, - {file = "pydantic-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19b5686387ea0d1ea52ecc4cffb71abb21702c5e5b2ac626fd4dbaa0834aa49d"}, - {file = "pydantic-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:32e0b4fb13ad4db4058a7c3c80e2569adbd810c25e6ca3bbd8b2a9cc2cc871d7"}, - {file = "pydantic-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91089b2e281713f3893cd01d8e576771cd5bfdfbff5d0ed95969f47ef6d676c3"}, - {file = "pydantic-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e631c70c9280e3129f071635b81207cad85e6c08e253539467e4ead0e5b219aa"}, - {file = "pydantic-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b3946f87e5cef3ba2e7bd3a4eb5a20385fe36521d6cc1ebf3c08a6697c6cfb3"}, - {file = "pydantic-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5565a49effe38d51882cb7bac18bda013cdb34d80ac336428e8908f0b72499b0"}, - {file = "pydantic-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:bd67cb2c2d9602ad159389c29e4ca964b86fa2f35c2faef54c3eb28b4efd36c8"}, - {file = "pydantic-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4aafd4e55e8ad5bd1b19572ea2df546ccace7945853832bb99422a79c70ce9b8"}, - {file = "pydantic-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:d70916235d478404a3fa8c997b003b5f33aeac4686ac1baa767234a0f8ac2326"}, - {file = "pydantic-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ca86b525264daa5f6b192f216a0d1e860b7383e3da1c65a1908f9c02f42801"}, - {file = "pydantic-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1061c6ee6204f4f5a27133126854948e3b3d51fcc16ead2e5d04378c199b2f44"}, - {file = "pydantic-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e78578f0c7481c850d1c969aca9a65405887003484d24f6110458fb02cca7747"}, - {file = "pydantic-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5da164119602212a3fe7e3bc08911a89db4710ae51444b4224c2382fd09ad453"}, - {file = "pydantic-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ead3cd020d526f75b4188e0a8d71c0dbbe1b4b6b5dc0ea775a93aca16256aeb"}, - {file = "pydantic-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7d0f183b305629765910eaad707800d2f47c6ac5bcfb8c6397abdc30b69eeb15"}, - {file = "pydantic-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f1a68f4f65a9ee64b6ccccb5bf7e17db07caebd2730109cb8a95863cfa9c4e55"}, - {file = "pydantic-1.9.2-py3-none-any.whl", hash = "sha256:78a4d6bdfd116a559aeec9a4cfe77dda62acc6233f8b56a716edad2651023e5e"}, - {file = "pydantic-1.9.2.tar.gz", hash = "sha256:8cb0bc509bfb71305d7a59d00163d5f9fc4530f0881ea32c74ff4f74c85f3d3d"}, -] - -[package.dependencies] -typing-extensions = ">=3.7.4.3" +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, + {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, + {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, + {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, + {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, + {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, + {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, + {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, + {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, + {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] @@ -472,6 +1271,21 @@ files = [ plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyrate-limiter" +version = "3.1.1" +description = "Python Rate-Limiter using Leaky-Bucket Algorithm" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "pyrate_limiter-3.1.1-py3-none-any.whl", hash = "sha256:c51906f1d51d56dc992ff6c26e8300e32151bc6cfa3e6559792e31971dfd4e2b"}, + {file = "pyrate_limiter-3.1.1.tar.gz", hash = "sha256:2f57eda712687e6eccddf6afe8f8a15b409b97ed675fe64a626058f12863b7b7"}, +] + +[package.extras] +all = ["filelock (>=3.0)", "redis (>=5.0.0,<6.0.0)"] +docs = ["furo (>=2022.3.4,<2023.0.0)", "myst-parser (>=0.17)", "sphinx (>=4.3.0,<5.0.0)", "sphinx-autodoc-typehints (>=1.17,<2.0)", "sphinx-copybutton (>=0.5)", "sphinxcontrib-apidoc (>=0.3,<0.4)"] + [[package]] name = "pyrsistent" version = "0.20.0" @@ -535,6 +1349,25 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-docker" +version = "2.0.1" +description = "Simple pytest fixtures for Docker and Docker Compose based tests" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-docker-2.0.1.tar.gz", hash = "sha256:1c17e9202a566f85ed5ef269fe2815bd4899e90eb639622e5d14277372ca7524"}, + {file = "pytest_docker-2.0.1-py3-none-any.whl", hash = "sha256:7103f97b8c479c826b63d73cfb83383dc1970d35105ed1ce78a722c90c7fe650"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +pytest = ">=4.0,<8.0" + +[package.extras] +docker-compose-v1 = ["docker-compose (>=1.27.3,<2.0)"] +tests = ["pytest-pycodestyle (>=2.0.0,<3.0)", "pytest-pylint (>=0.14.1,<1.0)", "requests (>=2.22.0,<3.0)"] + [[package]] name = "pytest-mypy" version = "0.10.3" @@ -555,6 +1388,143 @@ mypy = [ ] pytest = {version = ">=6.2", markers = "python_version >= \"3.10\""} +[[package]] +name = "pytest-postgresql" +version = "5.0.0" +description = "Postgresql fixtures and fixture factories for Pytest." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-postgresql-5.0.0.tar.gz", hash = "sha256:22edcbafab8995ee85b8d948ddfaad4f70c2c7462303d7477ecd2f77fc9d15bd"}, + {file = "pytest_postgresql-5.0.0-py3-none-any.whl", hash = "sha256:6e8f0773b57c9b8975b6392c241b7b81b7018f32079a533f368f2fbda732ecd3"}, +] + +[package.dependencies] +mirakuru = "*" +port-for = ">=0.6.0" +psycopg = ">=3.0.0" +pytest = ">=6.2" +setuptools = "*" + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-ulid" +version = "2.2.0" +description = "Universally unique lexicographically sortable identifier" +optional = false +python-versions = ">=3.9" +files = [ + {file = "python_ulid-2.2.0-py3-none-any.whl", hash = "sha256:ec2e69292c0b7c338a07df5e15b05270be6823675c103383e74d1d531945eab5"}, + {file = "python_ulid-2.2.0.tar.gz", hash = "sha256:9ec777177d396880d94be49ac7eb4ae2cd4a7474448bfdbfe911537add970aeb"}, +] + +[[package]] +name = "pytz" +version = "2023.3.post1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, +] + +[[package]] +name = "pytzdata" +version = "2020.1" +description = "The Olson timezone database for Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"}, + {file = "pytzdata-2020.1.tar.gz", hash = "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540"}, +] + +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + [[package]] name = "referencing" version = "0.32.1" @@ -591,6 +1561,36 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-cache" +version = "1.1.1" +description = "A persistent cache for python requests" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "requests_cache-1.1.1-py3-none-any.whl", hash = "sha256:c8420cf096f3aafde13c374979c21844752e2694ffd8710e6764685bb577ac90"}, + {file = "requests_cache-1.1.1.tar.gz", hash = "sha256:764f93d3fa860be72125a568c2cc8eafb151cf29b4dc2515433a56ee657e1c60"}, +] + +[package.dependencies] +attrs = ">=21.2" +cattrs = ">=22.2" +platformdirs = ">=2.5" +requests = ">=2.22" +url-normalize = ">=1.4" +urllib3 = ">=1.25.5" + +[package.extras] +all = ["boto3 (>=1.15)", "botocore (>=1.18)", "itsdangerous (>=2.0)", "pymongo (>=3)", "pyyaml (>=5.4)", "redis (>=3)", "ujson (>=5.4)"] +bson = ["bson (>=0.5)"] +docs = ["furo (>=2023.3,<2024.0)", "linkify-it-py (>=2.0,<3.0)", "myst-parser (>=1.0,<2.0)", "sphinx (>=5.0.2,<6.0.0)", "sphinx-autodoc-typehints (>=1.19)", "sphinx-automodapi (>=0.14)", "sphinx-copybutton (>=0.5)", "sphinx-design (>=0.2)", "sphinx-notfound-page (>=0.8)", "sphinxcontrib-apidoc (>=0.3)", "sphinxext-opengraph (>=0.6)"] +dynamodb = ["boto3 (>=1.15)", "botocore (>=1.18)"] +json = ["ujson (>=5.4)"] +mongodb = ["pymongo (>=3)"] +redis = ["redis (>=3)"] +security = ["itsdangerous (>=2.0)"] +yaml = ["pyyaml (>=5.4)"] + [[package]] name = "rpds-py" version = "0.16.2" @@ -699,6 +1699,32 @@ files = [ {file = "rpds_py-0.16.2.tar.gz", hash = "sha256:781ef8bfc091b19960fc0142a23aedadafa826bc32b433fdfe6fd7f964d7ef44"}, ] +[[package]] +name = "ruff" +version = "0.1.11" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a7f772696b4cdc0a3b2e527fc3c7ccc41cdcb98f5c80fdd4f2b8c50eb1458196"}, + {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:934832f6ed9b34a7d5feea58972635c2039c7a3b434fe5ba2ce015064cb6e955"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea0d3e950e394c4b332bcdd112aa566010a9f9c95814844a7468325290aabfd9"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bd4025b9c5b429a48280785a2b71d479798a69f5c2919e7d274c5f4b32c3607"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ad00662305dcb1e987f5ec214d31f7d6a062cae3e74c1cbccef15afd96611d"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4b077ce83f47dd6bea1991af08b140e8b8339f0ba8cb9b7a484c30ebab18a23f"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a88efecec23c37b11076fe676e15c6cdb1271a38f2b415e381e87fe4517f18"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b25093dad3b055667730a9b491129c42d45e11cdb7043b702e97125bcec48a1"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231d8fb11b2cc7c0366a326a66dafc6ad449d7fcdbc268497ee47e1334f66f77"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:09c415716884950080921dd6237767e52e227e397e2008e2bed410117679975b"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f58948c6d212a6b8d41cd59e349751018797ce1727f961c2fa755ad6208ba45"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:190a566c8f766c37074d99640cd9ca3da11d8deae2deae7c9505e68a4a30f740"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6464289bd67b2344d2a5d9158d5eb81025258f169e69a46b741b396ffb0cda95"}, + {file = "ruff-0.1.11-py3-none-win32.whl", hash = "sha256:9b8f397902f92bc2e70fb6bebfa2139008dc72ae5177e66c383fa5426cb0bf2c"}, + {file = "ruff-0.1.11-py3-none-win_amd64.whl", hash = "sha256:eb85ee287b11f901037a6683b2374bb0ec82928c5cbc984f575d0437979c521a"}, + {file = "ruff-0.1.11-py3-none-win_arm64.whl", hash = "sha256:97ce4d752f964ba559c7023a86e5f8e97f026d511e48013987623915431c7ea9"}, + {file = "ruff-0.1.11.tar.gz", hash = "sha256:f9d4d88cb6eeb4dfe20f9f0519bd2eaba8119bde87c3d5065c541dbae2b5a2cb"}, +] + [[package]] name = "setuptools" version = "69.0.3" @@ -726,6 +1752,81 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "sqlalchemy" +version = "2.0.25" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-win32.whl", hash = "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-win_amd64.whl", hash = "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-win32.whl", hash = "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-win32.whl", hash = "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-win_amd64.whl", hash = "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bb209a73b8307f8fe4fe46f6ad5979649be01607f11af1eb94aa9e8a3aaf77f0"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd402169aa00df3142149940b3bf9ce7dde075928c1886d9a1df63d4b8de62"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:74b080c897563f81062b74e44f5a72fa44c2b373741a9ade701d5f789a10ba23"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-win32.whl", hash = "sha256:87d91043ea0dc65ee583026cb18e1b458d8ec5fc0a93637126b5fc0bc3ea68c4"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-win_amd64.whl", hash = "sha256:75f99202324383d613ddd1f7455ac908dca9c2dd729ec8584c9541dd41822a2c"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-win32.whl", hash = "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-win_amd64.whl", hash = "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-win32.whl", hash = "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-win_amd64.whl", hash = "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7"}, + {file = "SQLAlchemy-2.0.25-py3-none-any.whl", hash = "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3"}, + {file = "SQLAlchemy-2.0.25.tar.gz", hash = "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + [[package]] name = "tomli" version = "2.0.1" @@ -751,6 +1852,17 @@ files = [ [package.dependencies] referencing = "*" +[[package]] +name = "types-pytz" +version = "2023.3.1.1" +description = "Typing stubs for pytz" +optional = false +python-versions = "*" +files = [ + {file = "types-pytz-2023.3.1.1.tar.gz", hash = "sha256:cc23d0192cd49c8f6bba44ee0c81e4586a8f30204970fc0894d209a6b08dab9a"}, + {file = "types_pytz-2023.3.1.1-py3-none-any.whl", hash = "sha256:1999a123a3dc0e39a2ef6d19f3f8584211de9e6a77fe7a0259f04a524e90a5cf"}, +] + [[package]] name = "types-pyyaml" version = "6.0.12.12" @@ -787,6 +1899,41 @@ files = [ {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] +[[package]] +name = "tzdata" +version = "2023.4" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, + {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, +] + +[[package]] +name = "ulid" +version = "1.1" +description = "Pyhton version of this: https://github.com/alizain/ulid" +optional = false +python-versions = "*" +files = [ + {file = "ulid-1.1.tar.gz", hash = "sha256:0943e8a751ec10dfcdb4df2758f96dffbbfbc055d0b49288caf2f92125900d49"}, +] + +[[package]] +name = "url-normalize" +version = "1.4.3" +description = "URL normalization for Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "url-normalize-1.4.3.tar.gz", hash = "sha256:d23d3a070ac52a67b83a1c59a0e68f8608d1cd538783b401bc9de2c0fac999b2"}, + {file = "url_normalize-1.4.3-py2.py3-none-any.whl", hash = "sha256:ec3c301f04e5bb676d333a7fa162fa977ad2ca04b7e652bfc9fac4e405728eed"}, +] + +[package.dependencies] +six = "*" + [[package]] name = "urllib3" version = "2.1.0" @@ -803,7 +1950,100 @@ brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "wcmatch" +version = "8.4" +description = "Wildcard/glob file name matcher." +optional = false +python-versions = ">=3.7" +files = [ + {file = "wcmatch-8.4-py3-none-any.whl", hash = "sha256:dc7351e5a7f8bbf4c6828d51ad20c1770113f5f3fd3dfe2a03cfde2a63f03f98"}, + {file = "wcmatch-8.4.tar.gz", hash = "sha256:ba4fc5558f8946bf1ffc7034b05b814d825d694112499c86035e0e4d398b6a67"}, +] + +[package.dependencies] +bracex = ">=2.1.1" + +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "d1e2a54bb61025323657af12ba1e9d4c43c3eac85e706a2e70ac55ec2854c44d" +content-hash = "a349c7b28e4ed7cb240906e522e373f6b00a5f2ee4ed4138567a0ed094491a30" diff --git a/airbyte-lib/pyproject.toml b/airbyte-lib/pyproject.toml index 8868b5ae376fa..84f492f0bba31 100644 --- a/airbyte-lib/pyproject.toml +++ b/airbyte-lib/pyproject.toml @@ -1,34 +1,163 @@ [tool.poetry] name = "airbyte-lib" +description = "AirbyteLib" version = "0.1.0" -description = "" -authors = ["Joe Reuter "] +authors = ["Airbyte "] readme = "README.md" +packages = [{include = "airbyte_lib"}] [tool.poetry.dependencies] python = "^3.10" + +airbyte-cdk = "^0.58.3" +# airbyte-protocol-models = "^1.0.1" # Conflicts with airbyte-cdk # TODO: delete or resolve +duckdb-engine = "^0.9.5" jsonschema = "3.2.0" +orjson = "^3.9.10" +overrides = "^7.4.0" +pandas = "^2.1.4" +psycopg = {extras = ["binary", "pool"], version = "^3.1.16"} +pyarrow = "^14.0.1" +python-ulid = "^2.2.0" requests = "^2.31.0" -airbyte-protocol-models = "^1.0.1" +SQLAlchemy = "^2.0.23" types-pyyaml = "^6.0.12.12" +ulid = "^1.1" [tool.poetry.group.dev.dependencies] -pytest = "^7.4.3" +docker = "^7.0.0" +faker = "^21.0.0" mypy = "^1.7.1" -types-requests = "^2.31.0.10" -types-jsonschema = "^4.20.0.0" -pytest-mypy = "^0.10.3" +pandas-stubs = "^2.1.4.231218" pdoc = "^14.3.0" +pyarrow-stubs = "^10.0.1.7" +pytest = "^7.4.3" +pytest-docker = "^2.0.1" +pytest-mypy = "^0.10.3" +pytest-postgresql = "^5.0.0" +ruff = "^0.1.11" +types-jsonschema = "^4.20.0.0" +types-requests = "^2.31.0.10" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" +[tool.pytest.ini_options] +# addopts = "--mypy" # FIXME: This sometimes blocks test discovery and execution + +[tool.ruff.pylint] +max-args = 8 # Relaxed from default of 5 + +[tool.ruff] +target-version = "py310" +select = ["F", "E"] +extend-select = [ + "W", "C90", "I", "N", "UP", "YTT", "ANN", "ASYNC", "BLE", "B", "A", "COM", "C4", "EXE", "FA", "ISC", "ICN", "INP", "PIE", "PYI", "PT", "Q", "RSE", "RET", "SLF", "SLOT", "SIM", "TID", "TCH", "INT", "ARG", "PTH", "TD", "FIX", "PD", "PL", "TRY", "FLY", "NPY", "PERF", "RUF" +] +ignore = [ + # For rules reference, see https://docs.astral.sh/ruff/rules/ + # "I001", # Sorted imports + # "ANN401", # Any-typed expressions + "ANN003", # kwargs missing type annotations + # "SIM300", # 'yoda' conditions + "PERF203", # exception handling in loop + "ANN101", # Type annotations for 'self' args + "TD002", # Require author for TODOs + "TD003", # Require links for TODOs + "B019", # lru_cache on class methods keep instance from getting garbage collected + "COM812", # Conflicts with ruff auto-format + "ISC001", # Conflicts with ruff auto-format + "TRY003" # Raising exceptions with too-long string descriptions # TODO: re-evaluate once we have our own exception classes +] +fixable = ["ALL"] +unfixable = [] +line-length = 100 +extend-exclude = ["docs", "test", "tests"] +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.isort] +force-sort-within-sections = false +lines-after-imports = 2 +known-first-party = ["airbyte_cdk", "airbyte_protocol"] +known-local-folder = ["airbyte_lib"] +known-third-party = [] +section-order = [ + "future", + "standard-library", + "third-party", + "first-party", + "local-folder" +] + +[tool.ruff.mccabe] +max-complexity = 24 + +[tool.ruff.pycodestyle] +ignore-overlong-task-comments = true + +[tool.ruff.pydocstyle] +convention = "numpy" + +[tool.ruff.flake8-annotations] +allow-star-arg-any = false +ignore-fully-untyped = false + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +preview = false +docstring-code-format = true + [tool.mypy] +# Platform configuration +python_version = "3.10" +# imports related ignore_missing_imports = true +follow_imports = "silent" +# None and Optional handling +no_implicit_optional = true +strict_optional = true +# Configuring warnings +warn_unused_configs = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true +warn_return_any = false +# Untyped definitions and calls +check_untyped_defs = true +disallow_untyped_calls = false +disallow_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_decorators = false +# Disallow dynamic typing +disallow_subclassing_any = true +disallow_any_unimported = false +disallow_any_expr = false +disallow_any_decorated = false +disallow_any_explicit = false +disallow_any_generics = false +# Miscellaneous strictness flags +allow_untyped_globals = false +allow_redefinition = false +local_partial_types = false +implicit_reexport = true +strict_equality = true +# Configuring error messages +show_error_context = false +show_column_numbers = false +show_error_codes = true +exclude = ["docs", "test", "tests"] -[tool.pytest.ini_options] -addopts = "--mypy" +[[tool.mypy.overrides]] +module = [ + "airbyte_protocol", + "airbyte_protocol.models" +] +ignore_missing_imports = true # No stubs yet (😢) [tool.poetry.scripts] generate-docs = "docs:run" diff --git a/airbyte-lib/tests/conftest.py b/airbyte-lib/tests/conftest.py new file mode 100644 index 0000000000000..f45441d791de2 --- /dev/null +++ b/airbyte-lib/tests/conftest.py @@ -0,0 +1,91 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +"""Global pytest fixtures.""" + +import logging +import socket +import time + +import docker +import psycopg +import pytest + +from airbyte_lib.caches import PostgresCacheConfig + +logger = logging.getLogger(__name__) + + +PYTEST_POSTGRES_IMAGE = "postgres:13" +PYTEST_POSTGRES_CONTAINER = "postgres_pytest_container" +PYTEST_POSTGRES_PORT = 5432 + + +def is_port_in_use(port): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex(("localhost", port)) == 0 + + +@pytest.fixture(scope="session", autouse=True) +def remove_postgres_container(): + client = docker.from_env() + if is_port_in_use(PYTEST_POSTGRES_PORT): + try: + container = client.containers.get( + PYTEST_POSTGRES_CONTAINER, + ) + container.stop() + container.remove() + except docker.errors.NotFound: + pass # Container not found, nothing to do. + + +@pytest.fixture(scope="session") +def pg_dsn(): + client = docker.from_env() + try: + client.images.get(PYTEST_POSTGRES_IMAGE) + except docker.errors.ImageNotFound: + # Pull the image if it doesn't exist, to avoid failing our sleep timer + # if the image needs to download on-demand. + client.images.pull(PYTEST_POSTGRES_IMAGE) + + postgres = client.containers.run( + image=PYTEST_POSTGRES_IMAGE, + name=PYTEST_POSTGRES_CONTAINER, + environment={"POSTGRES_USER": "postgres", "POSTGRES_PASSWORD": "postgres", "POSTGRES_DB": "postgres"}, + ports={"5432/tcp": PYTEST_POSTGRES_PORT}, + detach=True, + ) + # Wait for the database to start (assumes image is already downloaded) + pg_url = f"postgresql://postgres:postgres@localhost:{PYTEST_POSTGRES_PORT}/postgres" + + max_attempts = 10 + for attempt in range(max_attempts): + try: + conn = psycopg.connect(pg_url) + conn.close() + break + except psycopg.OperationalError: + logger.info(f"Waiting for postgres to start (attempt {attempt + 1}/{max_attempts})") + time.sleep(1.0) + + else: + raise Exception("Failed to connect to the PostgreSQL database.") + + yield pg_url + # Stop and remove the container after the tests are done + postgres.stop() + postgres.remove() + + +@pytest.fixture +def new_pg_cache_config(pg_dsn): + config = PostgresCacheConfig( + host="localhost", + port=PYTEST_POSTGRES_PORT, + username="postgres", + password="postgres", + database="postgres", + schema_name="public", + ) + yield config diff --git a/airbyte-lib/tests/integration_tests/test_integration.py b/airbyte-lib/tests/integration_tests/test_integration.py index 02347de6dfa88..2be56ece60a97 100644 --- a/airbyte-lib/tests/integration_tests/test_integration.py +++ b/airbyte-lib/tests/integration_tests/test_integration.py @@ -2,10 +2,16 @@ import os import shutil +import tempfile +from pathlib import Path import airbyte_lib as ab +import pandas as pd import pytest + +from airbyte_lib.caches import PostgresCache, PostgresCacheConfig from airbyte_lib.registry import _update_cache +from airbyte_lib.results import ReadResult @pytest.fixture(scope="module", autouse=True) @@ -24,11 +30,22 @@ def prepare_test_env(): shutil.rmtree(".venv-source-test") - -def test_list_streams(): +@pytest.fixture +def expected_test_stream_data() -> dict[str, list[dict[str, str | int]]]: + return { + "stream1": [ + {"column1": "value1", "column2": 1}, + {"column1": "value2", "column2": 2}, + ], + "stream2": [ + {"column1": "value1", "column2": 1}, + ], + } + +def test_list_streams(expected_test_stream_data: dict[str, list[dict[str, str | int]]]): source = ab.get_connector("source-test", config={"apiKey": "test"}) - assert source.get_available_streams() == ["stream1", "stream2"] + assert source.get_available_streams() == list(expected_test_stream_data.keys()) def test_invalid_config(): @@ -86,12 +103,88 @@ def test_check_fail(): source.check() +def test_file_write_and_cleanup() -> None: + """Ensure files are written to the correct location and cleaned up afterwards.""" + with tempfile.TemporaryDirectory() as temp_dir_1, tempfile.TemporaryDirectory() as temp_dir_2: + cache_w_cleanup = ab.new_local_cache(cache_dir=temp_dir_1, cleanup=True) + cache_wo_cleanup = ab.new_local_cache(cache_dir=temp_dir_2, cleanup=False) + + source = ab.get_connector("source-test", config={"apiKey": "test"}) + + _ = source.read(cache_w_cleanup) + _ = source.read(cache_wo_cleanup) + + assert len(list(Path(temp_dir_1).glob("*.parquet"))) == 0, "Expected files to be cleaned up" + assert len(list(Path(temp_dir_2).glob("*.parquet"))) == 2, "Expected files to exist" + + +def test_sync_to_duckdb(expected_test_stream_data: dict[str, list[dict[str, str | int]]]): + source = ab.get_connector("source-test", config={"apiKey": "test"}) + cache = ab.new_local_cache() + + result: ReadResult = source.read(cache) + + assert result.processed_records == 3 + for stream_name, expected_data in expected_test_stream_data.items(): + pd.testing.assert_frame_equal( + result[stream_name].to_pandas(), + pd.DataFrame(expected_data), + check_dtype=False, + ) + + +def test_read_result_as_list(expected_test_stream_data: dict[str, list[dict[str, str | int]]]): + source = ab.get_connector("source-test", config={"apiKey": "test"}) + cache = ab.new_local_cache() + + result: ReadResult = source.read(cache) + stream_1_list = list(result["stream1"]) + stream_2_list = list(result["stream2"]) + assert stream_1_list == expected_test_stream_data["stream1"] + assert stream_2_list == expected_test_stream_data["stream2"] + + +def test_get_records_result_as_list(expected_test_stream_data: dict[str, list[dict[str, str | int]]]): + source = ab.get_connector("source-test", config={"apiKey": "test"}) + cache = ab.new_local_cache() + + stream_1_list = list(source.get_records("stream1")) + stream_2_list = list(source.get_records("stream2")) + assert stream_1_list == expected_test_stream_data["stream1"] + assert stream_2_list == expected_test_stream_data["stream2"] + + + +def test_sync_with_merge_to_duckdb(expected_test_stream_data: dict[str, list[dict[str, str | int]]]): + """Test that the merge strategy works as expected. + + In this test, we sync the same data twice. If the data is not duplicated, we assume + the merge was successful. + + # TODO: Add a check with a primary key to ensure that the merge strategy works as expected. + """ + source = ab.get_connector("source-test", config={"apiKey": "test"}) + cache = ab.new_local_cache() + + # Read twice to test merge strategy + result: ReadResult = source.read(cache) + result: ReadResult = source.read(cache) + + assert result.processed_records == 3 + for stream_name, expected_data in expected_test_stream_data.items(): + pd.testing.assert_frame_equal( + result[stream_name].to_pandas(), + pd.DataFrame(expected_data), + check_dtype=False, + ) + + @pytest.mark.parametrize( "method_call", [ pytest.param(lambda source: source.check(), id="check"), - pytest.param(lambda source: list(source.read_stream("stream1")), id="read_stream"), - pytest.param(lambda source: source.read_all(), id="read_all"), + pytest.param(lambda source: list(source.get_records("stream1")), id="read_stream"), + pytest.param(lambda source: source.read(), id="read"), ], ) def test_check_fail_on_missing_config(method_call): @@ -100,41 +193,72 @@ def test_check_fail_on_missing_config(method_call): with pytest.raises(Exception, match="Config is not set, either set in get_connector or via source.set_config"): method_call(source) +def test_sync_with_merge_to_postgres(new_pg_cache_config: PostgresCacheConfig, expected_test_stream_data: dict[str, list[dict[str, str | int]]]): + """Test that the merge strategy works as expected. + + In this test, we sync the same data twice. If the data is not duplicated, we assume + the merge was successful. + + # TODO: Add a check with a primary key to ensure that the merge strategy works as expected. + """ + source = ab.get_connector("source-test", config={"apiKey": "test"}) + cache = PostgresCache(config=new_pg_cache_config) + + # Read twice to test merge strategy + result: ReadResult = source.read(cache) + result: ReadResult = source.read(cache) + + assert result.processed_records == 3 + for stream_name, expected_data in expected_test_stream_data.items(): + pd.testing.assert_frame_equal( + result[stream_name].to_pandas(), + pd.DataFrame(expected_data), + check_dtype=False, + ) + -def test_sync(): +def test_sync_to_postgres(new_pg_cache_config: PostgresCacheConfig, expected_test_stream_data: dict[str, list[dict[str, str | int]]]): source = ab.get_connector("source-test", config={"apiKey": "test"}) - cache = ab.get_in_memory_cache() + cache = PostgresCache(config=new_pg_cache_config) - result = source.read_all(cache) + result: ReadResult = source.read(cache) assert result.processed_records == 3 - assert list(result["stream1"]) == [{"column1": "value1", "column2": 1}, {"column1": "value2", "column2": 2}] - assert list(result["stream2"]) == [{"column1": "value1", "column2": 1}] + for stream_name, expected_data in expected_test_stream_data.items(): + pd.testing.assert_frame_equal( + result[stream_name].to_pandas(), + pd.DataFrame(expected_data), + check_dtype=False, + ) -def test_sync_limited_streams(): +def test_sync_limited_streams(expected_test_stream_data): source = ab.get_connector("source-test", config={"apiKey": "test"}) - cache = ab.get_in_memory_cache() + cache = ab.new_local_cache() source.set_streams(["stream2"]) - result = source.read_all(cache) + result = source.read(cache) assert result.processed_records == 1 - assert list(result["stream2"]) == [{"column1": "value1", "column2": 1}] + pd.testing.assert_frame_equal( + result["stream2"].to_pandas(), + pd.DataFrame(expected_test_stream_data["stream2"]), + check_dtype=False, + ) def test_read_stream(): source = ab.get_connector("source-test", config={"apiKey": "test"}) - assert list(source.read_stream("stream1")) == [{"column1": "value1", "column2": 1}, {"column1": "value2", "column2": 2}] + assert list(source.get_records("stream1")) == [{"column1": "value1", "column2": 1}, {"column1": "value2", "column2": 2}] def test_read_stream_nonexisting(): source = ab.get_connector("source-test", config={"apiKey": "test"}) with pytest.raises(Exception): - list(source.read_stream("non-existing")) + list(source.get_records("non-existing")) def test_failing_path_connector(): with pytest.raises(Exception): diff --git a/airbyte-lib/tests/lint_tests/__init__.py b/airbyte-lib/tests/lint_tests/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/airbyte-lib/tests/lint_tests/test_mypy.py b/airbyte-lib/tests/lint_tests/test_mypy.py new file mode 100644 index 0000000000000..df09978280792 --- /dev/null +++ b/airbyte-lib/tests/lint_tests/test_mypy.py @@ -0,0 +1,21 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import subprocess + +import pytest + + +def test_mypy_typing(): + # Run the check command + check_result = subprocess.run( + ["poetry", "run", "mypy", "."], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + # Assert that the Ruff command exited without errors (exit code 0) + assert check_result.returncode == 0, ( + "MyPy checks failed:\n" + + f"{check_result.stdout.decode()}\n{check_result.stderr.decode()}\n\n" + + "Run `poetry run mypy .` to see all failures." + ) diff --git a/airbyte-lib/tests/lint_tests/test_ruff.py b/airbyte-lib/tests/lint_tests/test_ruff.py new file mode 100644 index 0000000000000..5f654d7b11e4d --- /dev/null +++ b/airbyte-lib/tests/lint_tests/test_ruff.py @@ -0,0 +1,60 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import subprocess + +import pytest + +XFAIL = True # Toggle to set if the test is expected to fail or not + + +@pytest.mark.xfail( + condition=XFAIL, + reason=( + "This is expected to fail until Ruff cleanup is completed.\n" + "In the meanwhile, use `poetry run ruff check --fix .` to find and fix issues." + ), +) +def test_ruff_linting(): + # Run the check command + check_result = subprocess.run( + ["poetry", "run", "ruff", "check", "."], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + # Assert that the Ruff command exited without errors (exit code 0) + assert check_result.returncode == 0, ( + "Ruff checks failed:\n\n" + + f"{check_result.stdout.decode()}\n{check_result.stderr.decode()}\n\n" + + "Run `poetry run ruff check .` to view all issues." + ) + + +def test_ruff_linting_fixable(): + # Run the check command + fix_diff_result = subprocess.run( + ["poetry", "run", "ruff", "check", "--fix", "--diff", "."], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + # Assert that the Ruff command exited without errors (exit code 0) + assert fix_diff_result.returncode == 0, ( + "Ruff checks revealed fixable issues:\n\n" + + f"{fix_diff_result.stdout.decode()}\n{fix_diff_result.stderr.decode()}\n\n" + + "Run `poetry run ruff check --fix .` to attempt automatic fixes." + ) + + +def test_ruff_format(): + # Define the command to run Ruff + command = ["poetry", "run", "ruff", "format", "--check", "--diff"] + + # Run the command + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + # Assert that the Ruff command exited without errors (exit code 0) + assert result.returncode == 0, ( + f"Ruff checks failed:\n\n{result.stdout.decode()}\n{result.stderr.decode()}\n\n" + + "Run `poetry run ruff format .` to attempt automatic fixes." + ) diff --git a/airbyte-lib/tests/unit_tests/__init__.py b/airbyte-lib/tests/unit_tests/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/airbyte-lib/tests/unit_tests/test_caches.py b/airbyte-lib/tests/unit_tests/test_caches.py new file mode 100644 index 0000000000000..5bc2ba4186cd8 --- /dev/null +++ b/airbyte-lib/tests/unit_tests/test_caches.py @@ -0,0 +1,60 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from pathlib import Path + +import pytest + +from airbyte_lib._file_writers import ParquetWriterConfig +from airbyte_lib.caches.base import SQLCacheBase, SQLCacheConfigBase +from airbyte_lib.caches.duckdb import DuckDBCacheBase, DuckDBCacheConfig + + +def test_duck_db_cache_config_initialization(): + config = DuckDBCacheConfig(db_path='test_path', schema_name='test_schema') + assert config.db_path == Path('test_path') + assert config.schema_name == 'test_schema' + +def test_duck_db_cache_config_default_schema_name(): + config = DuckDBCacheConfig(db_path='test_path') + assert config.schema_name == 'main' + +def test_get_sql_alchemy_url(): + config = DuckDBCacheConfig(db_path='test_path', schema_name='test_schema') + assert config.get_sql_alchemy_url() == 'duckdb:///test_path' + +def test_get_sql_alchemy_url_with_default_schema_name(): + config = DuckDBCacheConfig(db_path='test_path') + assert config.get_sql_alchemy_url() == 'duckdb:///test_path' + +def test_duck_db_cache_config_inheritance(): + assert issubclass(DuckDBCacheConfig, SQLCacheConfigBase) + assert issubclass(DuckDBCacheConfig, ParquetWriterConfig) + +def test_duck_db_cache_config_get_sql_alchemy_url(): + config = DuckDBCacheConfig(db_path='test_path', schema_name='test_schema') + assert config.get_sql_alchemy_url() == 'duckdb:///test_path' + +def test_duck_db_cache_config_get_database_name(): + config = DuckDBCacheConfig(db_path='test_path/test_db.duckdb', schema_name='test_schema') + assert config.get_database_name() == 'test_db' + +def test_duck_db_cache_base_inheritance(): + assert issubclass(DuckDBCacheBase, SQLCacheBase) + +def test_duck_db_cache_config_default_schema_name(): + config = DuckDBCacheConfig(db_path='test_path') + assert config.schema_name == 'main' + +def test_duck_db_cache_config_get_sql_alchemy_url_with_default_schema_name(): + config = DuckDBCacheConfig(db_path='test_path') + assert config.get_sql_alchemy_url() == 'duckdb:///test_path' + +def test_duck_db_cache_config_get_database_name_with_default_schema_name(): + config = DuckDBCacheConfig(db_path='test_path/test_db.duckdb') + assert config.get_database_name() == 'test_db' + +def test_duck_db_cache_config_inheritance_from_sql_cache_config_base(): + assert issubclass(DuckDBCacheConfig, SQLCacheConfigBase) + +def test_duck_db_cache_config_inheritance_from_parquet_writer_config(): + assert issubclass(DuckDBCacheConfig, ParquetWriterConfig) diff --git a/airbyte-lib/tests/unit_tests/test_type_translation.py b/airbyte-lib/tests/unit_tests/test_type_translation.py new file mode 100644 index 0000000000000..80c1e611c6620 --- /dev/null +++ b/airbyte-lib/tests/unit_tests/test_type_translation.py @@ -0,0 +1,27 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import pytest +from sqlalchemy import types +from airbyte_lib.types import SQLTypeConverter + +@pytest.mark.parametrize( + "json_schema_property_def, expected_sql_type", + [ + ({"type": "string"}, types.VARCHAR), + ({"type": "boolean"}, types.BOOLEAN), + ({"type": "string", "format": "date"}, types.DATE), + ({"type": "string", "format": "date-time", "airbyte_type": "timestamp_without_timezone"}, types.TIMESTAMP), + ({"type": "string", "format": "date-time", "airbyte_type": "timestamp_with_timezone"}, types.TIMESTAMP), + ({"type": "string", "format": "time", "airbyte_type": "time_without_timezone"}, types.TIME), + ({"type": "string", "format": "time", "airbyte_type": "time_with_timezone"}, types.TIME), + ({"type": "integer"}, types.BIGINT), + ({"type": "number", "airbyte_type": "integer"}, types.BIGINT), + ({"type": "number"}, types.DECIMAL), + ({"type": "array"}, types.VARCHAR), + ({"type": "object"}, types.VARCHAR), + ], +) +def test_to_sql_type(json_schema_property_def, expected_sql_type): + converter = SQLTypeConverter() + sql_type = converter.to_sql_type(json_schema_property_def) + assert isinstance(sql_type, expected_sql_type) diff --git a/airbyte-lib/tests/unit_tests/test_writers.py b/airbyte-lib/tests/unit_tests/test_writers.py new file mode 100644 index 0000000000000..5d0432606b136 --- /dev/null +++ b/airbyte-lib/tests/unit_tests/test_writers.py @@ -0,0 +1,38 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from pathlib import Path +import pytest +from airbyte_lib._file_writers.base import FileWriterBase, FileWriterBatchHandle, FileWriterConfigBase +from airbyte_lib._file_writers.parquet import ParquetWriter, ParquetWriterConfig +from numpy import source + + +def test_parquet_writer_config_initialization(): + config = ParquetWriterConfig(cache_dir='test_path') + assert config.cache_dir == Path('test_path') + +def test_parquet_writer_config_inheritance(): + assert issubclass(ParquetWriterConfig, FileWriterConfigBase) + +def test_parquet_writer_initialization(): + config = ParquetWriterConfig(cache_dir='test_path') + writer = ParquetWriter(config) + assert writer.config == config + +def test_parquet_writer_inheritance(): + assert issubclass(ParquetWriter, FileWriterBase) + +def test_parquet_writer_has_config(): + config = ParquetWriterConfig(cache_dir='test_path') + writer = ParquetWriter(config) + assert hasattr(writer, 'config') + +def test_parquet_writer_has_source_catalog(): + config = ParquetWriterConfig(cache_dir='test_path') + writer = ParquetWriter(config) + assert hasattr(writer, 'source_catalog') + +def test_parquet_writer_source_catalog_is_none(): + config = ParquetWriterConfig(cache_dir='test_path') + writer = ParquetWriter(config) + assert writer.source_catalog is None diff --git a/pyproject.toml b/pyproject.toml index fb3ac81754469..1977450d14a5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ black = "~22.3.0" [tool.black] line-length = 140 target-version = ["py37"] -extend-exclude = "(build|integration_tests|unit_tests|generated)" +extend-exclude = "(build|integration_tests|unit_tests|generated|airbyte-lib)" [tool.coverage.report] fail_under = 0 @@ -61,7 +61,12 @@ multi_line_output = 3 include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true -skip_glob = ["**/connector_builder/generated/**"] +skip_glob = [ + "**/connector_builder/generated/**", + "airbyte-lib" # Handled by Ruff, with some conflicting rules + # TODO: Remove this after we move to Ruff. Ruff is mono-repo-aware and + # correctly handles first-party imports in subdirectories. +] [tool.mypy] @@ -96,5 +101,3 @@ error_summary = true [tool.pytest.ini_options] minversion = "6.2.5" addopts ="-r a --capture=no -vv --color=yes" - - From de4ca2d84892dcda3a1bf06a1981e509dc82a7ae Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 12 Jan 2024 18:59:21 +0100 Subject: [PATCH 081/574] Vectara Destination: Add info box (#34159) --- docs/integrations/destinations/vectara.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/integrations/destinations/vectara.md b/docs/integrations/destinations/vectara.md index f6e871de13cd9..19d19baa74024 100644 --- a/docs/integrations/destinations/vectara.md +++ b/docs/integrations/destinations/vectara.md @@ -2,10 +2,18 @@ This page contains the setup guide and reference information for the Vectara destination connector. -[Vectara](https://vectara.com/) is the trusted GenAI platform that provides a managed service for Retrieval Augmented Generation or [RAG](https://vectara.com/grounded-generation/). +[Vectara](https://vectara.com/) is the trusted GenAI platform that provides Retrieval Augmented Generation or [RAG](https://vectara.com/grounded-generation/) as a service. The Vectara destination connector allows you to connect any Airbyte source to Vectara and ingest data into Vectara for your RAG pipeline. +:::info +In case of issues, the following public channels are available for support: + +* For Airbyte related issues such as data source or processing: [Open a Github issue](https://github.com/airbytehq/airbyte/issues/new?assignees=&labels=type%2Fbug%2Carea%2Fconnectors%2Cneeds-triage&projects=&template=1-issue-connector.yaml) +* For Vectara related issues such as data indexing or RAG: Create a post in the [Vectara forum](https://discuss.vectara.com/) or reach out on [Vectara's Discord server](https://discord.gg/GFb8gMz6UH) + +::: + ## Overview The Vectara destination connector supports Full Refresh Overwrite, Full Refresh Append, and Incremental Append. From 25f52e423f4d95db6a5d2949c2e789af0179669b Mon Sep 17 00:00:00 2001 From: Ben Church Date: Fri, 12 Jan 2024 10:59:37 -0800 Subject: [PATCH 082/574] CI: Fix linting issue (#34224) --- airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py b/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py index cb5226728929a..c7a463a160c91 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py +++ b/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py @@ -6,8 +6,9 @@ from __future__ import annotations -# Important: This import and function call must be the first import in this file +# HACK! IMPORTANT! This import and function call must be the first import in this file # This is needed to ensure that the working directory is the root of the airbyte repo +# ruff: noqa: E402 from pipelines.cli.ensure_repo_root import set_working_directory_to_root set_working_directory_to_root() From 632b1bfb8bd6b161dee4a62a88e987104a91ccbb Mon Sep 17 00:00:00 2001 From: Gireesh Sreepathi Date: Fri, 12 Jan 2024 11:24:24 -0800 Subject: [PATCH 083/574] Destination Redshift: Use cdk for TD dependency (#34194) Signed-off-by: Gireesh Sreepathi --- .../connectors/destination-redshift/build.gradle | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/airbyte-integrations/connectors/destination-redshift/build.gradle b/airbyte-integrations/connectors/destination-redshift/build.gradle index 7b137ee3a8bbe..773a75a9ba127 100644 --- a/airbyte-integrations/connectors/destination-redshift/build.gradle +++ b/airbyte-integrations/connectors/destination-redshift/build.gradle @@ -5,7 +5,7 @@ plugins { airbyteJavaConnector { cdkVersionRequired = '0.12.0' - features = ['db-destinations', 's3-destinations'] + features = ['db-destinations', 's3-destinations', 'typing-deduping'] useLocalCdk = false } @@ -39,10 +39,6 @@ dependencies { testImplementation 'org.apache.commons:commons-dbcp2:2.7.0' testImplementation "org.mockito:mockito-inline:4.1.0" - // TODO: declare typing-deduping as a CDK feature instead of importing from source. - implementation project(':airbyte-cdk:java:airbyte-cdk:typing-deduping') - testImplementation testFixtures(project(':airbyte-cdk:java:airbyte-cdk:typing-deduping')) - integrationTestJavaImplementation testFixtures(project(':airbyte-cdk:java:airbyte-cdk:typing-deduping')) } configurations.all { From 59df0cf30224004f7efebf6e640f88ea26de2446 Mon Sep 17 00:00:00 2001 From: Gireesh Sreepathi Date: Fri, 12 Jan 2024 13:04:45 -0800 Subject: [PATCH 084/574] Destination Bigquery: Clean up dependencies with TD/CDK (#34226) Signed-off-by: Gireesh Sreepathi --- .../connectors/destination-bigquery/build.gradle | 12 ++++-------- .../connectors/destination-bigquery/metadata.yaml | 2 +- .../bigquery/uploader/BigQueryUploaderFactory.java | 8 ++++---- docs/integrations/destinations/bigquery.md | 5 +++-- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/airbyte-integrations/connectors/destination-bigquery/build.gradle b/airbyte-integrations/connectors/destination-bigquery/build.gradle index 9bbf9824d47b9..9d4c49a4163df 100644 --- a/airbyte-integrations/connectors/destination-bigquery/build.gradle +++ b/airbyte-integrations/connectors/destination-bigquery/build.gradle @@ -4,8 +4,8 @@ plugins { } airbyteJavaConnector { - cdkVersionRequired = '0.11.0' - features = ['db-destinations', 's3-destinations'] + cdkVersionRequired = '0.12.0' + features = ['db-destinations', 's3-destinations', 'typing-deduping'] useLocalCdk = false } @@ -29,6 +29,7 @@ application { airbyteJavaConnector.addCdkDependencies() dependencies { + // TODO: Pull out common classes into CDK instead of depending on another destination implementation project(':airbyte-integrations:connectors:destination-gcs') implementation 'com.google.cloud:google-cloud-bigquery:2.31.1' @@ -46,13 +47,8 @@ dependencies { integrationTestJavaImplementation project(':airbyte-integrations:connectors:destination-bigquery') - // TODO: declare typing-deduping as a CDK feature instead of importing from source. - implementation project(':airbyte-cdk:java:airbyte-cdk:typing-deduping') - integrationTestJavaImplementation testFixtures(project(':airbyte-cdk:java:airbyte-cdk:typing-deduping')) - - // TODO: remove these dependencies (what's S3 doing here???) + // This dependency is required because GCSOperaitons is leaking S3Client interface to the BigQueryDestination. implementation libs.aws.java.sdk.s3 - implementation libs.s3 } configurations.all { diff --git a/airbyte-integrations/connectors/destination-bigquery/metadata.yaml b/airbyte-integrations/connectors/destination-bigquery/metadata.yaml index 5d5abb06054c9..13878dc432770 100644 --- a/airbyte-integrations/connectors/destination-bigquery/metadata.yaml +++ b/airbyte-integrations/connectors/destination-bigquery/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: database connectorType: destination definitionId: 22f6c74f-5699-40ff-833c-4a879ea40133 - dockerImageTag: 2.3.29 + dockerImageTag: 2.3.30 dockerRepository: airbyte/destination-bigquery documentationUrl: https://docs.airbyte.com/integrations/destinations/bigquery githubIssueLabel: destination-bigquery diff --git a/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/uploader/BigQueryUploaderFactory.java b/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/uploader/BigQueryUploaderFactory.java index f79388b8cf09f..6eca8c9f947e5 100644 --- a/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/uploader/BigQueryUploaderFactory.java +++ b/airbyte-integrations/connectors/destination-bigquery/src/main/java/io/airbyte/integrations/destination/bigquery/uploader/BigQueryUploaderFactory.java @@ -4,9 +4,6 @@ package io.airbyte.integrations.destination.bigquery.uploader; -import static software.amazon.awssdk.http.HttpStatusCode.FORBIDDEN; -import static software.amazon.awssdk.http.HttpStatusCode.NOT_FOUND; - import com.fasterxml.jackson.databind.JsonNode; import com.google.cloud.bigquery.BigQuery; import com.google.cloud.bigquery.BigQueryException; @@ -33,6 +30,9 @@ public class BigQueryUploaderFactory { private static final Logger LOGGER = LoggerFactory.getLogger(BigQueryUploaderFactory.class); + private static final int HTTP_STATUS_CODE_FORBIDDEN = 403; + private static final int HTTP_STATUS_CODE_NOT_FOUND = 404; + private static final String CONFIG_ERROR_MSG = """ Failed to write to destination schema. @@ -104,7 +104,7 @@ private static BigQueryDirectUploader getBigQueryDirectUploader( try { writer = bigQuery.writer(job, writeChannelConfiguration); } catch (final BigQueryException e) { - if (e.getCode() == FORBIDDEN || e.getCode() == NOT_FOUND) { + if (e.getCode() == HTTP_STATUS_CODE_FORBIDDEN || e.getCode() == HTTP_STATUS_CODE_NOT_FOUND) { throw new ConfigErrorException(CONFIG_ERROR_MSG + e); } else { throw new BigQueryException(e.getCode(), e.getMessage()); diff --git a/docs/integrations/destinations/bigquery.md b/docs/integrations/destinations/bigquery.md index 646ffcc7128f7..09fc5de2a0f12 100644 --- a/docs/integrations/destinations/bigquery.md +++ b/docs/integrations/destinations/bigquery.md @@ -209,7 +209,8 @@ tutorials: ## Changelog | Version | Date | Pull Request | Subject | -| :------ | :--------- | :--------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|:--------|:-----------|:-----------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 2.3.30 | 2024-01-12 | [34226](https://github.com/airbytehq/airbyte/pull/34226) | Upgrade CDK to 0.12.0; Cleanup dependencies | | 2.3.29 | 2024-01-09 | [34003](https://github.com/airbytehq/airbyte/pull/34003) | Fix loading credentials from GCP Env | | 2.3.28 | 2024-01-08 | [34021](https://github.com/airbytehq/airbyte/pull/34021) | Add idempotency ids in dummy insert for check call | | 2.3.27 | 2024-01-05 | [\#33948](https://github.com/airbytehq/airbyte/pull/33948) | Skip retrieving initial table state when setup fails | @@ -371,4 +372,4 @@ tutorials: | 0.3.10 | 2021-07-28 | [\#3549](https://github.com/airbytehq/airbyte/issues/3549) | Add extended logs and made JobId filled with region and projectId | | 0.3.9 | 2021-07-28 | [\#5026](https://github.com/airbytehq/airbyte/pull/5026) | Add sanitized json fields in raw tables to handle quotes in column names | | 0.3.6 | 2021-06-18 | [\#3947](https://github.com/airbytehq/airbyte/issues/3947) | Service account credentials are now optional. | -| 0.3.4 | 2021-06-07 | [\#3277](https://github.com/airbytehq/airbyte/issues/3277) | Add dataset location option | +| 0.3.4 | 2021-06-07 | [\#3277](https://github.com/airbytehq/airbyte/issues/3277) | Add dataset location option | \ No newline at end of file From 026f5a7dddc025c54a32ddce913845a694b3f6ab Mon Sep 17 00:00:00 2001 From: Gireesh Sreepathi Date: Fri, 12 Jan 2024 13:15:27 -0800 Subject: [PATCH 085/574] Destination Snowflake: Cleanup dependencies and upgrade CDK (#34227) Signed-off-by: Gireesh Sreepathi --- .../connectors/destination-snowflake/build.gradle | 14 ++------------ .../connectors/destination-snowflake/metadata.yaml | 2 +- docs/integrations/destinations/snowflake.md | 5 +++-- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/airbyte-integrations/connectors/destination-snowflake/build.gradle b/airbyte-integrations/connectors/destination-snowflake/build.gradle index 2a6e6a00f2a06..4ce8698d75d8f 100644 --- a/airbyte-integrations/connectors/destination-snowflake/build.gradle +++ b/airbyte-integrations/connectors/destination-snowflake/build.gradle @@ -4,8 +4,8 @@ plugins { } airbyteJavaConnector { - cdkVersionRequired = '0.11.2' - features = ['db-destinations', 's3-destinations'] + cdkVersionRequired = '0.12.0' + features = ['db-destinations', 's3-destinations', 'typing-deduping'] useLocalCdk = false } @@ -38,24 +38,14 @@ integrationTestJava { } dependencies { - implementation 'com.google.cloud:google-cloud-storage:1.113.16' - implementation 'com.google.auth:google-auth-library-oauth2-http:0.25.5' implementation 'net.snowflake:snowflake-jdbc:3.14.1' implementation 'org.apache.commons:commons-csv:1.4' implementation 'org.apache.commons:commons-text:1.10.0' - implementation 'com.github.alexmojaki:s3-stream-upload:2.2.2' implementation "io.aesy:datasize:1.0.0" implementation 'com.zaxxer:HikariCP:5.0.1' - implementation project(':airbyte-integrations:connectors:destination-gcs') - // this is a configuration to make mockito work with final classes testImplementation 'org.mockito:mockito-inline:2.13.0' integrationTestJavaImplementation 'org.apache.commons:commons-lang3:3.11' - - // TODO: declare typing-deduping as a CDK feature instead of importing from source. - implementation project(':airbyte-cdk:java:airbyte-cdk:typing-deduping') - testImplementation testFixtures(project(':airbyte-cdk:java:airbyte-cdk:typing-deduping')) - integrationTestJavaImplementation testFixtures(project(':airbyte-cdk:java:airbyte-cdk:typing-deduping')) } diff --git a/airbyte-integrations/connectors/destination-snowflake/metadata.yaml b/airbyte-integrations/connectors/destination-snowflake/metadata.yaml index fa5975df1ccfa..b5d7311d68df2 100644 --- a/airbyte-integrations/connectors/destination-snowflake/metadata.yaml +++ b/airbyte-integrations/connectors/destination-snowflake/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: database connectorType: destination definitionId: 424892c4-daac-4491-b35d-c6688ba547ba - dockerImageTag: 3.4.21 + dockerImageTag: 3.4.22 dockerRepository: airbyte/destination-snowflake documentationUrl: https://docs.airbyte.com/integrations/destinations/snowflake githubIssueLabel: destination-snowflake diff --git a/docs/integrations/destinations/snowflake.md b/docs/integrations/destinations/snowflake.md index 5ccaf068410a7..aad1612b10c82 100644 --- a/docs/integrations/destinations/snowflake.md +++ b/docs/integrations/destinations/snowflake.md @@ -246,7 +246,8 @@ Otherwise, make sure to grant the role the required permissions in the desired n | Version | Date | Pull Request | Subject | |:----------------|:-----------|:-----------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 3.4.21 | 2024-01-10 | [\#34083](https://github.com/airbytehq/airbte/pull/34083) | Emit destination stats as part of the state message | +| 3.4.22 | 2024-01-12 | [34227](https://github.com/airbytehq/airbyte/pull/34227) | Upgrade CDK to 0.12.0; Cleanup unused dependencies | +| 3.4.21 | 2024-01-10 | [\#34083](https://github.com/airbytehq/airbte/pull/34083) | Emit destination stats as part of the state message | | 3.4.20 | 2024-01-05 | [\#33948](https://github.com/airbytehq/airbyte/pull/33948) | Skip retrieving initial table state when setup fails | | 3.4.19 | 2024-01-04 | [\#33730](https://github.com/airbytehq/airbyte/pull/33730) | Internal code structure changes | | 3.4.18 | 2024-01-02 | [\#33728](https://github.com/airbytehq/airbyte/pull/33728) | Add option to only type and dedupe at the end of the sync | @@ -402,4 +403,4 @@ Otherwise, make sure to grant the role the required permissions in the desired n | 0.3.13 | 2021-09-01 | [\#5784](https://github.com/airbytehq/airbyte/pull/5784) | Updated query timeout from 30 minutes to 3 hours | | 0.3.12 | 2021-07-30 | [\#5125](https://github.com/airbytehq/airbyte/pull/5125) | Enable `additionalPropertities` in spec.json | | 0.3.11 | 2021-07-21 | [\#3555](https://github.com/airbytehq/airbyte/pull/3555) | Partial Success in BufferedStreamConsumer | -| 0.3.10 | 2021-07-12 | [\#4713](https://github.com/airbytehq/airbyte/pull/4713) | Tag traffic with `airbyte` label to enable optimization opportunities from Snowflake | +| 0.3.10 | 2021-07-12 | [\#4713](https://github.com/airbytehq/airbyte/pull/4713) | Tag traffic with `airbyte` label to enable optimization opportunities from Snowflake | \ No newline at end of file From 0fc363cf5f15c3428c09290c0ae04395bc3740c9 Mon Sep 17 00:00:00 2001 From: midavadim Date: Sat, 13 Jan 2024 00:28:49 +0200 Subject: [PATCH 086/574] =?UTF-8?q?=F0=9F=9A=A8=F0=9F=9A=A8=20Source=20Mon?= =?UTF-8?q?day=20migration=20to=20new=20api=20version=202024-01=20(#34108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: maxi297 --- .../source-monday/acceptance-test-config.yml | 6 +- .../integration_tests/expected_records.jsonl | 25 ++---- .../connectors/source-monday/metadata.yaml | 19 ++++- .../source-monday/source_monday/extractor.py | 11 ++- .../source_monday/graphql_requester.py | 38 ++++++++- .../source_monday/item_pagination_strategy.py | 54 +++++++++++++ .../source-monday/source_monday/manifest.yaml | 19 ++++- .../source_monday/schemas/boards.json | 13 ++-- .../source_monday/schemas/items.json | 11 +-- .../source_monday/schemas/tags.json | 2 +- .../source_monday/schemas/teams.json | 2 +- .../source_monday/schemas/updates.json | 2 +- .../source_monday/schemas/users.json | 2 +- .../source_monday/schemas/workspaces.json | 12 +-- .../unit_tests/test_extractor.py | 29 ++++++- .../unit_tests/test_graphql_requester.py | 11 ++- .../test_item_pagination_strategy.py | 41 +++++++++- .../integrations/sources/monday-migrations.md | 77 +++++++++++++++++++ docs/integrations/sources/monday.md | 1 + 19 files changed, 314 insertions(+), 61 deletions(-) create mode 100644 docs/integrations/sources/monday-migrations.md diff --git a/airbyte-integrations/connectors/source-monday/acceptance-test-config.yml b/airbyte-integrations/connectors/source-monday/acceptance-test-config.yml index 5f312963ecd66..23f80f6c22ceb 100644 --- a/airbyte-integrations/connectors/source-monday/acceptance-test-config.yml +++ b/airbyte-integrations/connectors/source-monday/acceptance-test-config.yml @@ -22,13 +22,13 @@ acceptance_tests: # `boards`, `items`, `updates` streams schemas were modified. PR: https://github.com/airbytehq/airbyte/pull/27410 # Changes applies to all configs backward_compatibility_tests_config: - disable_for_version: "0.2.6" + disable_for_version: "2.0.0" - config_path: "secrets/config_api_token.json" backward_compatibility_tests_config: - disable_for_version: "0.2.6" + disable_for_version: "2.0.0" - config_path: "secrets/config_oauth.json" backward_compatibility_tests_config: - disable_for_version: "0.2.6" + disable_for_version: "2.0.0" basic_read: tests: - config_path: "secrets/config_api_token.json" diff --git a/airbyte-integrations/connectors/source-monday/integration_tests/expected_records.jsonl b/airbyte-integrations/connectors/source-monday/integration_tests/expected_records.jsonl index 05a950303148d..8c9370bc4be57 100644 --- a/airbyte-integrations/connectors/source-monday/integration_tests/expected_records.jsonl +++ b/airbyte-integrations/connectors/source-monday/integration_tests/expected_records.jsonl @@ -1,17 +1,8 @@ -{"stream": "items", "data": {"assets": [], "board": {"id": "4635211873"}, "column_values": [{"additional_info": null, "description": null, "id": "person", "text": "", "title": "Person", "type": "multiple-person", "value": null}, {"additional_info": "{\"label\":\"Working on it\",\"color\":\"#fdab3d\",\"changed_at\":\"2019-03-01T17:24:57.321Z\"}", "description": null, "id": "status", "text": "Working on it", "title": "Status", "type": "color", "value": "{\"index\":0,\"post_id\":null,\"changed_at\":\"2019-03-01T17:24:57.321Z\"}"}, {"additional_info": null, "description": null, "id": "date4", "text": "2023-06-11", "title": "Date", "type": "date", "value": "{\"date\":\"2023-06-11\",\"icon\":null,\"changed_at\":\"2023-06-13T13:58:25.871Z\"}"}, {"additional_info": null, "description": null, "id": "tags", "text": "open", "title": "Tags", "type": "tag", "value": "{\"tag_ids\":[19038090]}"}], "created_at": "2023-06-13T13:58:24Z", "creator_id": "36694549", "group": {"id": "topics"}, "id": "4635211945", "name": "Item 1", "parent_item": null, "state": "active", "subscribers": [{"id": 36694549}], "updated_at": "2023-06-15T16:19:37Z", "updates": [{"id": "2223820299"}, {"id": "2223818363"}], "updated_at_int": 1686845977}, "emitted_at": 1690884054247} -{"stream": "items", "data": {"assets": [], "board": {"id": "4635211873"}, "column_values": [{"additional_info": null, "description": null, "id": "person", "text": "", "title": "Person", "type": "multiple-person", "value": null}, {"additional_info": "{\"label\":\"Done\",\"color\":\"#00c875\",\"changed_at\":\"2019-03-01T17:28:23.178Z\"}", "description": null, "id": "status", "text": "Done", "title": "Status", "type": "color", "value": "{\"index\":1,\"post_id\":null,\"changed_at\":\"2019-03-01T17:28:23.178Z\"}"}, {"additional_info": null, "description": null, "id": "date4", "text": "2023-06-11", "title": "Date", "type": "date", "value": "{\"date\":\"2023-06-11\",\"icon\":null,\"changed_at\":\"2023-06-13T13:58:25.871Z\"}"}, {"additional_info": null, "description": null, "id": "tags", "text": "closed", "title": "Tags", "type": "tag", "value": "{\"tag_ids\":[19038091]}"}], "created_at": "2023-06-13T13:58:24Z", "creator_id": "36694549", "group": {"id": "topics"}, "id": "4635211964", "name": "Item 2", "parent_item": null, "state": "active", "subscribers": [{"id": 36694549}], "updated_at": "2023-06-13T13:59:36Z", "updates": [], "updated_at_int": 1686664776}, "emitted_at": 1690884054254} -{"stream": "items", "data": {"assets": [], "board": {"id": "4635211873"}, "column_values": [{"additional_info": null, "description": null, "id": "person", "text": "", "title": "Person", "type": "multiple-person", "value": null}, {"additional_info": "{\"label\":null,\"color\":\"#c4c4c4\",\"changed_at\":\"2019-03-01T17:25:02.248Z\"}", "description": null, "id": "status", "text": null, "title": "Status", "type": "color", "value": "{\"index\":5,\"post_id\":null,\"changed_at\":\"2019-03-01T17:25:02.248Z\"}"}, {"additional_info": null, "description": null, "id": "date4", "text": "2023-06-13", "title": "Date", "type": "date", "value": "{\"date\":\"2023-06-13\",\"icon\":null,\"changed_at\":\"2023-06-13T13:58:26.291Z\"}"}, {"additional_info": null, "description": null, "id": "tags", "text": "", "title": "Tags", "type": "tag", "value": null}], "created_at": "2023-06-13T13:58:24Z", "creator_id": "36694549", "group": {"id": "topics"}, "id": "4635211977", "name": "Item 3", "parent_item": null, "state": "active", "subscribers": [{"id": 36694549}], "updated_at": "2023-06-13T13:58:26Z", "updates": [], "updated_at_int": 1686664706}, "emitted_at": 1690884054258} -{"stream": "boards", "data": {"board_kind": "public", "type": "board", "columns": [{"archived": false, "description": null, "id": "name", "settings_str": "{}", "title": "Name", "type": "name", "width": 400}, {"archived": false, "description": null, "id": "person", "settings_str": "{}", "title": "Person", "type": "multiple-person", "width": null}, {"archived": false, "description": null, "id": "status", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"Working on it\",\"1\":\"Done\",\"2\":\"Stuck\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "Status", "type": "color", "width": null}, {"archived": false, "description": null, "id": "date4", "settings_str": "{}", "title": "Date", "type": "date", "width": null}, {"archived": false, "description": null, "id": "tags", "settings_str": "{\"hide_footer\":false}", "title": "Tags", "type": "tag", "width": null}], "communication": null, "description": null, "groups": [{"archived": false, "color": "#579bfc", "deleted": false, "id": "topics", "position": "65536", "title": "Group Title"}, {"archived": false, "color": "#a25ddc", "deleted": false, "id": "group_title", "position": "98304", "title": "Group Title"}, {"archived": false, "color": "#808080", "deleted": false, "id": "new_group", "position": "163840.0", "title": "New Group unit board"}], "id": "4635211873", "name": "New Board", "owners": [{"id": 36694549}], "creator": {"id": 36694549}, "permissions": "everyone", "pos": null, "state": "active", "subscribers": [{"id": 36694549}], "tags": [], "top_group": {"id": "topics"}, "updated_at": "2023-06-20T12:12:46Z", "updates": [{"id": "2223820299"}, {"id": "2223818363"}], "views": [], "workspace": {"id": 2845647, "name": "Test workspace", "kind": "open", "description": null}, "updated_at_int": 1687263166}, "emitted_at": 1702496562635} -{"stream": "boards", "data": {"board_kind": "public", "type": "board", "columns": [{"archived": false, "description": null, "id": "name", "settings_str": "{}", "title": "Name", "type": "name", "width": 380}, {"archived": false, "description": null, "id": "manager1", "settings_str": "{}", "title": "Owner", "type": "multiple-person", "width": 80}, {"archived": false, "description": null, "id": "date4", "settings_str": "{}", "title": "Request date", "type": "date", "width": null}, {"archived": false, "description": null, "id": "status1", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"Evaluating\",\"1\":\"Done\",\"2\":\"Denied\",\"3\":\"Waiting for legal\",\"6\":\"Approved for POC\",\"11\":\"On hold\",\"14\":\"Waiting for vendor\",\"15\":\"Negotiation\",\"108\":\"Approved for use\"},\"labels_positions_v2\":{\"0\":0,\"1\":1,\"2\":7,\"3\":8,\"5\":9,\"6\":3,\"11\":6,\"14\":5,\"15\":4,\"108\":2},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"},\"3\":{\"color\":\"#0086c0\",\"border\":\"#3DB0DF\",\"var_name\":\"blue-links\"},\"6\":{\"color\":\"#037f4c\",\"border\":\"#006B38\",\"var_name\":\"grass-green\"},\"11\":{\"color\":\"#BB3354\",\"border\":\"#A42D4A\",\"var_name\":\"dark-red\"},\"14\":{\"color\":\"#784BD1\",\"border\":\"#8F4DC4\",\"var_name\":\"dark-purple\"},\"15\":{\"color\":\"#9CD326\",\"border\":\"#89B921\",\"var_name\":\"lime-green\"},\"108\":{\"color\":\"#4eccc6\",\"border\":\"#4eccc6\",\"var_name\":\"australia\"}}}", "title": "Procurement status", "type": "color", "width": null}, {"archived": false, "description": null, "id": "person", "settings_str": "{}", "title": "Manager", "type": "multiple-person", "width": 80}, {"archived": false, "description": null, "id": "status", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"On Hold\",\"1\":\"Approved\",\"2\":\"Declined\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "Manager approval", "type": "color", "width": null}, {"archived": false, "description": null, "id": "budget_owner", "settings_str": "{}", "title": "POC owner", "type": "multiple-person", "width": 80}, {"archived": false, "description": null, "id": "budget_owner_approval4", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"On Hold\",\"1\":\"Approved\",\"2\":\"Declined\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "POC status", "type": "color", "width": null}, {"archived": false, "description": null, "id": "manager", "settings_str": "{}", "title": "Budget owner", "type": "multiple-person", "width": 80}, {"archived": false, "description": null, "id": "status4", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"On Hold\",\"1\":\"Approved\",\"2\":\"Declined\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "Budget owner approval", "type": "color", "width": 185}, {"archived": false, "description": null, "id": "people", "settings_str": "{}", "title": "Procurement team", "type": "multiple-person", "width": null}, {"archived": false, "description": null, "id": "budget_owner_approval", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"On Hold\",\"1\":\"Approved\",\"2\":\"Declined\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "Procurement approval", "type": "color", "width": null}, {"archived": false, "description": null, "id": "procurement_team", "settings_str": "{}", "title": "Finance", "type": "multiple-person", "width": null}, {"archived": false, "description": null, "id": "procurement_approval", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"On Hold\",\"1\":\"Approved\",\"2\":\"Declined\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "Finance approval", "type": "color", "width": null}, {"archived": false, "description": null, "id": "finance", "settings_str": "{}", "title": "Legal", "type": "multiple-person", "width": null}, {"archived": false, "description": null, "id": "finance_approval", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"On Hold\",\"1\":\"Approved\",\"2\":\"Redlines\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "Legal approval", "type": "color", "width": null}, {"archived": false, "description": null, "id": "file", "settings_str": "{}", "title": "File", "type": "file", "width": null}, {"archived": false, "description": null, "id": "legal", "settings_str": "{}", "title": "Security", "type": "multiple-person", "width": null}, {"archived": false, "description": null, "id": "legal_approval", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"On Hold\",\"1\":\"Approved\",\"2\":\"Declined\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "Security approval", "type": "color", "width": null}, {"archived": false, "description": null, "id": "date", "settings_str": "{\"hide_footer\":false}", "title": "Renewal date", "type": "date", "width": null}, {"archived": false, "description": null, "id": "last_updated", "settings_str": "{}", "title": "Last updated", "type": "pulse-updated", "width": 129}], "communication": null, "description": "Many IT departments need to handle the procurement process for new services. The essence of this board is to streamline this process by providing an intuitive structure that supports collaboration and efficiency.", "groups": [{"archived": false, "color": "#579bfc", "deleted": false, "id": "topics", "position": "65536", "title": "Reviewing"}, {"archived": false, "color": "#FF642E", "deleted": false, "id": "new_group", "position": "98304.0", "title": "Corporate IT"}, {"archived": false, "color": "#037f4c", "deleted": false, "id": "new_group2816", "position": "114688.0", "title": "Finance"}], "id": "3555407826", "name": "Procurement process", "owners": [{"id": 36694549}], "creator": {"id": 36694549}, "permissions": "everyone", "pos": null, "state": "active", "subscribers": [{"id": 36694549}], "tags": [], "top_group": {"id": "topics"}, "updated_at": "2022-11-21T14:36:50Z", "updates": [], "views": [], "workspace": null, "updated_at_int": 1669041410}, "emitted_at": 1702496562643} -{"stream": "boards", "data": {"board_kind": "public", "type": "board", "columns": [{"archived": false, "description": null, "id": "name", "settings_str": "{}", "title": "Name", "type": "name", "width": 523}, {"archived": false, "description": null, "id": "text4", "settings_str": "{}", "title": "SN", "type": "text", "width": null}, {"archived": false, "description": null, "id": "status", "settings_str": "{\"done_colors\":[1],\"hide_footer\":true,\"labels\":{\"0\":\"Out for repair\",\"1\":\"Working well\",\"2\":\"Needs replacement\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "Status", "type": "color", "width": null}, {"archived": false, "description": null, "id": "date4", "settings_str": "{}", "title": "Date given to current owner", "type": "date", "width": 204}, {"archived": false, "description": null, "id": "text", "settings_str": "{}", "title": "Current owner", "type": "text", "width": null}, {"archived": false, "description": null, "id": "date_given_to_current_owner", "settings_str": "{}", "title": "Last checked", "type": "date", "width": 129}], "communication": null, "description": "Welcome to your inventory management board. This is the place to track and manage all of your IT equipment inventory.", "groups": [{"archived": false, "color": "#BB3354", "deleted": false, "id": "duplicate_of_tvs___projectors", "position": "65408", "title": "Out of service"}, {"archived": false, "color": "#579bfc", "deleted": false, "id": "topics", "position": "65536", "title": "Laptops"}, {"archived": false, "color": "#a25ddc", "deleted": false, "id": "group_title", "position": "98304", "title": "Monitors"}, {"archived": false, "color": "#037f4c", "deleted": false, "id": "new_group", "position": "163840.0", "title": "TVs & projectors"}], "id": "3555407785", "name": "Inventory management", "owners": [{"id": 36694549}], "creator": {"id": 36694549}, "permissions": "everyone", "pos": null, "state": "active", "subscribers": [{"id": 36694549}], "tags": [], "top_group": {"id": "duplicate_of_tvs___projectors"}, "updated_at": "2022-11-21T14:36:49Z", "updates": [], "views": [], "workspace": null, "updated_at_int": 1669041409}, "emitted_at": 1702496562649} -{"stream": "tags", "data": {"color": "#00c875", "id": 19038090, "name": "open"}, "emitted_at": 1690884065804} -{"stream": "tags", "data": {"color": "#fdab3d", "id": 19038091, "name": "closed"}, "emitted_at": 1690884065806} -{"stream": "updates", "data": {"assets": [{"created_at": "2023-06-15T16:19:31Z", "file_extension": ".jpg", "file_size": 116107, "id": "919077184", "name": "black_cat.jpg", "original_geometry": "473x600", "public_url": "https://files-monday-com.s3.amazonaws.com/14202902/resources/919077184/black_cat.jpg?response-content-disposition=attachment&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA4MPVJMFXGWGLJTLY%2F20230801%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20230801T100107Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=5d2d3ca95375589e620f89630d58ff0f7417f1ddd8968ceb57af854657718564", "uploaded_by": {"id": 36694549}, "url": "https://airbyte-unit.monday.com/protected_static/14202902/resources/919077184/black_cat.jpg", "url_thumbnail": "https://airbyte-unit.monday.com/protected_static/14202902/resources/919077184/thumb_small-black_cat.jpg"}], "body": "", "created_at": "2023-06-15T16:19:36Z", "creator_id": "36694549", "id": "2223820299", "item_id": "4635211945", "replies": [], "text_body": "", "updated_at": "2023-06-15T16:19:36Z"}, "emitted_at": 1690884067025} -{"stream": "updates", "data": {"assets": [], "body": "



", "created_at": "2023-06-15T16:18:50Z", "creator_id": "36694549", "id": "2223818363", "item_id": "4635211945", "replies": [], "text_body": "", "updated_at": "2023-06-15T16:18:50Z"}, "emitted_at": 1690884067027} -{"stream": "updates", "data": {"assets": [], "body": "

\ufeffTest

", "created_at": "2022-11-21T14:41:21Z", "creator_id": "36694549", "id": "1825302913", "item_id": "3555437747", "replies": [{"id": "1825303266", "creator_id": "36694549", "created_at": "2022-11-21T14:41:29Z", "text_body": "Test test", "updated_at": "2022-11-21T14:41:29Z", "body": "

\ufeffTest test

"}, {"id": "2223806079", "creator_id": "36694549", "created_at": "2023-06-15T16:14:13Z", "text_body": "", "updated_at": "2023-06-15T16:14:13Z", "body": "



"}], "text_body": "Test", "updated_at": "2023-06-15T16:14:13Z"}, "emitted_at": 1690884067029} -{"stream": "users", "data": {"birthday": null, "country_code": "UA", "created_at": "2022-11-21T14:03:00Z", "join_date": null, "email": "integration-test@airbyte.io", "enabled": true, "id": 36694549, "is_admin": true, "is_guest": false, "is_pending": false, "is_view_only": false, "is_verified": true, "location": null, "mobile_phone": null, "name": "Airbyte Team", "phone": "", "photo_original": "https://files.monday.com/use1/photos/36694549/original/36694549-user_photo_2022_11_21_14_10_42.png?1669039842", "photo_small": "https://files.monday.com/use1/photos/36694549/small/36694549-user_photo_2022_11_21_14_10_42.png?1669039842", "photo_thumb": "https://files.monday.com/use1/photos/36694549/thumb/36694549-user_photo_2022_11_21_14_10_42.png?1669039842", "photo_thumb_small": "https://files.monday.com/use1/photos/36694549/thumb_small/36694549-user_photo_2022_11_21_14_10_42.png?1669039842", "photo_tiny": "https://files.monday.com/use1/photos/36694549/tiny/36694549-user_photo_2022_11_21_14_10_42.png?1669039842", "time_zone_identifier": "Europe/Kiev", "title": "Airbyte Developer Account", "url": "https://airbyte-unit.monday.com/users/36694549", "utc_hours_diff": 2}, "emitted_at": 1702496564648} -{"stream": "users", "data": {"birthday": null, "country_code": "UA", "created_at": "2022-11-21T14:33:18Z", "join_date": null, "email": "iryna.grankova@airbyte.io", "enabled": true, "id": 36695702, "is_admin": false, "is_guest": false, "is_pending": false, "is_view_only": false, "is_verified": true, "location": null, "mobile_phone": null, "name": "Iryna Grankova", "phone": null, "photo_original": "https://files.monday.com/use1/photos/36695702/original/36695702-user_photo_initials_2022_11_21_14_34_12.png?1669041252", "photo_small": "https://files.monday.com/use1/photos/36695702/small/36695702-user_photo_initials_2022_11_21_14_34_12.png?1669041252", "photo_thumb": "https://files.monday.com/use1/photos/36695702/thumb/36695702-user_photo_initials_2022_11_21_14_34_12.png?1669041252", "photo_thumb_small": "https://files.monday.com/use1/photos/36695702/thumb_small/36695702-user_photo_initials_2022_11_21_14_34_12.png?1669041252", "photo_tiny": "https://files.monday.com/use1/photos/36695702/tiny/36695702-user_photo_initials_2022_11_21_14_34_12.png?1669041252", "time_zone_identifier": "Europe/Athens", "title": null, "url": "https://airbyte-unit.monday.com/users/36695702", "utc_hours_diff": 2}, "emitted_at": 1702496564650} -{"stream": "workspaces", "data": {"created_at": "2023-06-08T11:26:44Z", "description": null, "id": 2845647, "kind": "open", "name": "Test workspace", "state": "active", "account_product": {"id": 2248222, "kind": "core"}, "owners_subscribers": [{"id": 36694549}], "settings": {"icon": {"color": "#FDAB3D", "image": null}}, "team_owners_subscribers": [], "teams_subscribers": [], "users_subscribers": [{"id": 36694549}]}, "emitted_at": 1690884067856} -{"stream": "activity_logs", "data": {"id": "81d07d4d-414d-458e-b44c-fef36e44c424", "event": "create_pulse", "data": "{\"board_id\":4635211873,\"group_id\":\"new_group\",\"group_name\":\"New Group unit board\",\"group_color\":\"#808080\",\"is_top_group\":false,\"pulse_id\":4672924165,\"pulse_name\":\"Item 7\",\"column_values_json\":\"{}\"}", "entity": "pulse", "created_at": "16872631837419768", "created_at_int": 1687263183, "pulse_id": 4672924165}, "emitted_at": 1690884068262} -{"stream": "activity_logs", "data": {"id": "c0aa4bab-d3a4-4f13-8942-934178c0238a", "event": "update_column_value", "data": "{\"board_id\":4635211873,\"group_id\":\"group_title\",\"is_top_group\":false,\"pulse_id\":4672922929,\"pulse_name\":\"Item 6\",\"column_id\":\"status\",\"column_type\":\"color\",\"column_title\":\"Status\",\"value\":{\"label\":{\"index\":1,\"text\":\"Done\",\"style\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"is_done\":true},\"post_id\":null},\"previous_value\":null,\"is_column_with_hide_permissions\":false}", "entity": "pulse", "created_at": "16872631743009674", "created_at_int": 1687263174, "pulse_id": 4672922929}, "emitted_at": 1690884068266} -{"stream": "activity_logs", "data": {"id": "4e8f926c-1b4e-43d8-a3de-09af876ccb9e", "event": "create_pulse", "data": "{\"board_id\":4635211873,\"group_id\":\"group_title\",\"group_name\":\"Group Title\",\"group_color\":\"#a25ddc\",\"is_top_group\":false,\"pulse_id\":4672922929,\"pulse_name\":\"Item 6\",\"column_values_json\":\"{}\"}", "entity": "pulse", "created_at": "16872631712788820", "created_at_int": 1687263171, "pulse_id": 4672922929}, "emitted_at": 1690884068269} +{"stream": "items", "data": {"id": "4635211945", "name": "Item 1", "assets": [], "board": {"id": "4635211873"}, "column_values": [{"id": "person", "text": "", "type": "people", "value": null}, {"id": "status", "text": "Working on it", "type": "status", "value": "{\"index\":0,\"post_id\":null,\"changed_at\":\"2019-03-01T17:24:57.321Z\"}"}, {"id": "date4", "text": "2023-06-11", "type": "date", "value": "{\"date\":\"2023-06-11\",\"icon\":null,\"changed_at\":\"2023-06-13T13:58:25.871Z\"}"}, {"id": "tags", "text": "open", "type": "tags", "value": "{\"tag_ids\":[19038090]}"}], "created_at": "2023-06-13T13:58:24Z", "creator_id": "36694549", "group": {"id": "topics"}, "parent_item": null, "state": "active", "subscribers": [{"id": "36694549"}], "updated_at": "2023-06-15T16:19:37Z", "updates": [{"id": "2223820299"}, {"id": "2223818363"}], "updated_at_int": 1686845977}, "emitted_at": 1705072697006} +{"stream": "boards", "data": {"id": "3555407826", "name": "Procurement process", "board_kind": "public", "type": "board", "columns": [{"archived": false, "description": null, "id": "name", "settings_str": "{}", "title": "Name", "type": "name", "width": 380}, {"archived": false, "description": null, "id": "manager1", "settings_str": "{}", "title": "Owner", "type": "people", "width": 80}, {"archived": false, "description": null, "id": "date4", "settings_str": "{}", "title": "Request date", "type": "date", "width": null}, {"archived": false, "description": null, "id": "status1", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"Evaluating\",\"1\":\"Done\",\"2\":\"Denied\",\"3\":\"Waiting for legal\",\"6\":\"Approved for POC\",\"11\":\"On hold\",\"14\":\"Waiting for vendor\",\"15\":\"Negotiation\",\"108\":\"Approved for use\"},\"labels_positions_v2\":{\"0\":0,\"1\":1,\"2\":7,\"3\":8,\"5\":9,\"6\":3,\"11\":6,\"14\":5,\"15\":4,\"108\":2},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"},\"3\":{\"color\":\"#0086c0\",\"border\":\"#3DB0DF\",\"var_name\":\"blue-links\"},\"6\":{\"color\":\"#037f4c\",\"border\":\"#006B38\",\"var_name\":\"grass-green\"},\"11\":{\"color\":\"#BB3354\",\"border\":\"#A42D4A\",\"var_name\":\"dark-red\"},\"14\":{\"color\":\"#784BD1\",\"border\":\"#8F4DC4\",\"var_name\":\"dark-purple\"},\"15\":{\"color\":\"#9CD326\",\"border\":\"#89B921\",\"var_name\":\"lime-green\"},\"108\":{\"color\":\"#4eccc6\",\"border\":\"#4eccc6\",\"var_name\":\"australia\"}}}", "title": "Procurement status", "type": "status", "width": null}, {"archived": false, "description": null, "id": "person", "settings_str": "{}", "title": "Manager", "type": "people", "width": 80}, {"archived": false, "description": null, "id": "status", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"On Hold\",\"1\":\"Approved\",\"2\":\"Declined\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "Manager approval", "type": "status", "width": null}, {"archived": false, "description": null, "id": "budget_owner", "settings_str": "{}", "title": "POC owner", "type": "people", "width": 80}, {"archived": false, "description": null, "id": "budget_owner_approval4", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"On Hold\",\"1\":\"Approved\",\"2\":\"Declined\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "POC status", "type": "status", "width": null}, {"archived": false, "description": null, "id": "manager", "settings_str": "{}", "title": "Budget owner", "type": "people", "width": 80}, {"archived": false, "description": null, "id": "status4", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"On Hold\",\"1\":\"Approved\",\"2\":\"Declined\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "Budget owner approval", "type": "status", "width": 185}, {"archived": false, "description": null, "id": "people", "settings_str": "{}", "title": "Procurement team", "type": "people", "width": null}, {"archived": false, "description": null, "id": "budget_owner_approval", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"On Hold\",\"1\":\"Approved\",\"2\":\"Declined\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "Procurement approval", "type": "status", "width": null}, {"archived": false, "description": null, "id": "procurement_team", "settings_str": "{}", "title": "Finance", "type": "people", "width": null}, {"archived": false, "description": null, "id": "procurement_approval", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"On Hold\",\"1\":\"Approved\",\"2\":\"Declined\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "Finance approval", "type": "status", "width": null}, {"archived": false, "description": null, "id": "finance", "settings_str": "{}", "title": "Legal", "type": "people", "width": null}, {"archived": false, "description": null, "id": "finance_approval", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"On Hold\",\"1\":\"Approved\",\"2\":\"Redlines\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "Legal approval", "type": "status", "width": null}, {"archived": false, "description": null, "id": "file", "settings_str": "{}", "title": "File", "type": "file", "width": null}, {"archived": false, "description": null, "id": "legal", "settings_str": "{}", "title": "Security", "type": "people", "width": null}, {"archived": false, "description": null, "id": "legal_approval", "settings_str": "{\"done_colors\":[1],\"labels\":{\"0\":\"On Hold\",\"1\":\"Approved\",\"2\":\"Declined\"},\"labels_positions_v2\":{\"0\":0,\"1\":2,\"2\":1,\"5\":3},\"labels_colors\":{\"0\":{\"color\":\"#fdab3d\",\"border\":\"#E99729\",\"var_name\":\"orange\"},\"1\":{\"color\":\"#00c875\",\"border\":\"#00B461\",\"var_name\":\"green-shadow\"},\"2\":{\"color\":\"#e2445c\",\"border\":\"#CE3048\",\"var_name\":\"red-shadow\"}}}", "title": "Security approval", "type": "status", "width": null}, {"archived": false, "description": null, "id": "date", "settings_str": "{\"hide_footer\":false}", "title": "Renewal date", "type": "date", "width": null}, {"archived": false, "description": null, "id": "last_updated", "settings_str": "{}", "title": "Last updated", "type": "last_updated", "width": 129}], "communication": null, "description": "Many IT departments need to handle the procurement process for new services. The essence of this board is to streamline this process by providing an intuitive structure that supports collaboration and efficiency.", "groups": [{"archived": false, "color": "#579bfc", "deleted": false, "id": "topics", "position": "65536", "title": "Reviewing"}, {"archived": false, "color": "#FF642E", "deleted": false, "id": "new_group", "position": "98304.0", "title": "Corporate IT"}, {"archived": false, "color": "#037f4c", "deleted": false, "id": "new_group2816", "position": "114688.0", "title": "Finance"}], "owners": [{"id": "36694549"}], "creator": {"id": "36694549"}, "permissions": "everyone", "state": "active", "subscribers": [{"id": "36694549"}], "tags": [], "top_group": {"id": "topics"}, "updated_at": "2022-11-21T14:36:50Z", "updates": [], "views": [], "workspace": null, "updated_at_int": 1669041410}, "emitted_at": 1705073472066} +{"stream": "tags", "data": {"color": "#00c875", "id": "19038090", "name": "open"}, "emitted_at": 1690884065804} +{"stream": "tags", "data": {"color": "#fdab3d", "id": "19038091", "name": "closed"}, "emitted_at": 1690884065806} +{"stream": "updates", "data": {"assets": [{"created_at": "2023-06-15T16:19:31Z", "file_extension": ".jpg", "file_size": 116107, "id": "919077184", "name": "black_cat.jpg", "original_geometry": "473x600", "public_url": "https://files-monday-com.s3.amazonaws.com/14202902/resources/919077184/black_cat.jpg?response-content-disposition=attachment&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA4MPVJMFXILAOBJXD%2F20240112%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240112T154009Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=b4f14a9dd800d70520f428ff7f4a29aa1b6a259d761f3b073fe83c41010c729a", "uploaded_by": {"id": "36694549"}, "url": "https://airbyte-unit.monday.com/protected_static/14202902/resources/919077184/black_cat.jpg", "url_thumbnail": "https://airbyte-unit.monday.com/protected_static/14202902/resources/919077184/thumb_small-black_cat.jpg"}], "body": "", "created_at": "2023-06-15T16:19:36Z", "creator_id": "36694549", "id": "2223820299", "item_id": "4635211945", "replies": [], "text_body": "", "updated_at": "2023-06-15T16:19:36Z"}, "emitted_at": 1705074009909} +{"stream": "users", "data": {"birthday": null, "country_code": "UA", "created_at": "2022-11-21T14:03:00Z", "join_date": null, "email": "integration-test@airbyte.io", "enabled": true, "id": "36694549", "is_admin": true, "is_guest": false, "is_pending": false, "is_view_only": false, "is_verified": true, "location": null, "mobile_phone": null, "name": "Airbyte Team", "phone": "", "photo_original": "https://files.monday.com/use1/photos/36694549/original/36694549-user_photo_2022_11_21_14_10_42.png?1669039842", "photo_small": "https://files.monday.com/use1/photos/36694549/small/36694549-user_photo_2022_11_21_14_10_42.png?1669039842", "photo_thumb": "https://files.monday.com/use1/photos/36694549/thumb/36694549-user_photo_2022_11_21_14_10_42.png?1669039842", "photo_thumb_small": "https://files.monday.com/use1/photos/36694549/thumb_small/36694549-user_photo_2022_11_21_14_10_42.png?1669039842", "photo_tiny": "https://files.monday.com/use1/photos/36694549/tiny/36694549-user_photo_2022_11_21_14_10_42.png?1669039842", "time_zone_identifier": "Europe/Kiev", "title": "Airbyte Developer Account", "url": "https://airbyte-unit.monday.com/users/36694549", "utc_hours_diff": 2}, "emitted_at": 1702496564648} +{"stream": "workspaces", "data": {"created_at": "2023-06-08T11:26:44Z", "description": null, "id": "2845647", "kind": "open", "name": "Test workspace", "state": "active", "account_product": {"id": "2248222", "kind": "core"}, "owners_subscribers": [{"id": "36694549"}], "settings": {"icon": {"color": "#FDAB3D", "image": null}}, "team_owners_subscribers": [], "teams_subscribers": [], "users_subscribers": [{"id": "36694549"}]}, "emitted_at": 1705074164892} +{"stream": "activity_logs", "data": {"id": "81d07d4d-414d-458e-b44c-fef36e44c424", "event": "create_pulse", "data": "{\"board_id\":4635211873,\"group_id\":\"new_group\",\"group_name\":\"New Group unit board\",\"group_color\":\"#808080\",\"is_top_group\":false,\"pulse_id\":4672924165,\"pulse_name\":\"Item 7\",\"column_values_json\":\"{}\"}", "entity": "pulse", "created_at": "16872631837419768", "created_at_int": 1687263183, "pulse_id": 4672924165}, "emitted_at": 1705074202226} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-monday/metadata.yaml b/airbyte-integrations/connectors/source-monday/metadata.yaml index 3ec648c41fe19..d5a3a9f0f41c4 100644 --- a/airbyte-integrations/connectors/source-monday/metadata.yaml +++ b/airbyte-integrations/connectors/source-monday/metadata.yaml @@ -10,7 +10,24 @@ data: connectorSubtype: api connectorType: source definitionId: 80a54ea2-9959-4040-aac1-eee42423ec9b - dockerImageTag: 1.1.4 + dockerImageTag: 2.0.0 + releases: + breakingChanges: + 2.0.0: + message: "Source Monday has deprecated API version 2023-07. We have upgraded the connector to the latest API version 2024-01. In this new version, the Id field has changed from an integer to a string in the streams Boards, Items, Tags, Teams, Updates, Users and Workspaces. Please reset affected streams." + upgradeDeadline: "2024-01-15" + scopedImpact: + - scopeType: stream + impactedScopes: + [ + "boards", + "items", + "tags", + "teams", + "updates", + "users", + "workspaces", + ] dockerRepository: airbyte/source-monday documentationUrl: https://docs.airbyte.com/integrations/sources/monday githubIssueLabel: source-monday diff --git a/airbyte-integrations/connectors/source-monday/source_monday/extractor.py b/airbyte-integrations/connectors/source-monday/source_monday/extractor.py index bd8524044024c..830aafc5cf9a8 100644 --- a/airbyte-integrations/connectors/source-monday/source_monday/extractor.py +++ b/airbyte-integrations/connectors/source-monday/source_monday/extractor.py @@ -72,11 +72,12 @@ class MondayIncrementalItemsExtractor(RecordExtractor): field_path: List[Union[InterpolatedString, str]] config: Config parameters: InitVar[Mapping[str, Any]] - additional_field_path: List[Union[InterpolatedString, str]] = field(default_factory=list) + field_path_pagination: List[Union[InterpolatedString, str]] = field(default_factory=list) + field_path_incremental: List[Union[InterpolatedString, str]] = field(default_factory=list) decoder: Decoder = JsonDecoder(parameters={}) def __post_init__(self, parameters: Mapping[str, Any]): - for field_list in (self.field_path, self.additional_field_path): + for field_list in (self.field_path, self.field_path_pagination, self.field_path_incremental): for path_index in range(len(field_list)): if isinstance(field_list[path_index], str): field_list[path_index] = InterpolatedString.create(field_list[path_index], parameters=parameters) @@ -100,8 +101,10 @@ def try_extract_records(self, response: requests.Response, field_path: List[Unio def extract_records(self, response: requests.Response) -> List[Record]: result = self.try_extract_records(response, field_path=self.field_path) - if not result and self.additional_field_path: - result = self.try_extract_records(response, self.additional_field_path) + if not result and self.field_path_pagination: + result = self.try_extract_records(response, self.field_path_pagination) + if not result and self.field_path_incremental: + result = self.try_extract_records(response, self.field_path_incremental) for item_index in range(len(result)): if "updated_at" in result[item_index]: diff --git a/airbyte-integrations/connectors/source-monday/source_monday/graphql_requester.py b/airbyte-integrations/connectors/source-monday/source_monday/graphql_requester.py index 0e5fd049583c6..fec0d06cbc3e5 100644 --- a/airbyte-integrations/connectors/source-monday/source_monday/graphql_requester.py +++ b/airbyte-integrations/connectors/source-monday/source_monday/graphql_requester.py @@ -79,18 +79,37 @@ def _build_query(self, object_name: str, field_schema: dict, **object_arguments) arguments = f"({arguments})" if arguments else "" fields = ",".join(fields) - return f"{object_name}{arguments}{{{fields}}}" + if object_name in ["items_page", "next_items_page"]: + query = f"{object_name}{arguments}{{cursor,items{{{fields}}}}}" + else: + query = f"{object_name}{arguments}{{{fields}}}" + return query def _build_items_query(self, object_name: str, field_schema: dict, sub_page: Optional[int], **object_arguments) -> str: """ Special optimization needed for items stream. Starting October 3rd, 2022 items can only be reached through boards. See https://developer.monday.com/api-reference/docs/items-queries#items-queries + + Comparison of different APIs queries: + 2023-07: + boards(limit: 1) { items(limit: 20) { field1, field2, ... }} + boards(limit: 1, page:2) { items(limit: 20, page:2) { field1, field2, ... }} boards and items paginations + 2024_01: + boards(limit: 1) { items_page(limit: 20) {cursor, items{field1, field2, ...} }} + boards(limit: 1, page:2) { items_page(limit: 20) {cursor, items{field1, field2, ...} }} - boards pagination + next_items_page(limit: 20, cursor: "blaa") {cursor, items{field1, field2, ...} } - items pagination + """ nested_limit = self.nested_limit.eval(self.config) - query = self._build_query("items", field_schema, limit=nested_limit, page=sub_page) - arguments = self._get_object_arguments(**object_arguments) - return f"boards({arguments}){{{query}}}" + if sub_page: + query = self._build_query("next_items_page", field_schema, limit=nested_limit, cursor=f'"{sub_page}"') + else: + query = self._build_query("items_page", field_schema, limit=nested_limit) + arguments = self._get_object_arguments(**object_arguments) + query = f"boards({arguments}){{{query}}}" + + return query def _build_items_incremental_query(self, object_name: str, field_schema: dict, stream_slice: dict, **object_arguments) -> str: """ @@ -133,6 +152,17 @@ def _build_activity_query(self, object_name: str, field_schema: dict, sub_page: arguments = self._get_object_arguments(**object_arguments) return f"boards({arguments}){{{query}}}" + def get_request_headers( + self, + *, + stream_state: Optional[StreamState] = None, + stream_slice: Optional[StreamSlice] = None, + next_page_token: Optional[Mapping[str, Any]] = None, + ) -> Mapping[str, Any]: + headers = super().get_request_headers(stream_state=stream_state, stream_slice=stream_slice, next_page_token=next_page_token) + headers["API-Version"] = "2024-01" + return headers + def get_request_params( self, *, diff --git a/airbyte-integrations/connectors/source-monday/source_monday/item_pagination_strategy.py b/airbyte-integrations/connectors/source-monday/source_monday/item_pagination_strategy.py index 5b18cb4b37b78..a6276416d2e5d 100644 --- a/airbyte-integrations/connectors/source-monday/source_monday/item_pagination_strategy.py +++ b/airbyte-integrations/connectors/source-monday/source_monday/item_pagination_strategy.py @@ -6,6 +6,10 @@ from airbyte_cdk.sources.declarative.requesters.paginators.strategies.page_increment import PageIncrement +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + class ItemPaginationStrategy(PageIncrement): """ @@ -45,3 +49,53 @@ def next_page_token(self, response, last_records: List[Mapping[str, Any]]) -> Op return None return self._page, self._sub_page + + +class ItemCursorPaginationStrategy(PageIncrement): + """ + Page increment strategy with subpages for the `items` stream. + + From the `items` documentation https://developer.monday.com/api-reference/docs/items: + Please note that you cannot return more than 100 items per query when using items at the root. + To adjust your query, try only returning items on a specific board, nesting items inside a boards query, + looping through the boards on your account, or querying less than 100 items at a time. + + This pagination strategy supports nested loop through `boards` on the top level and `items` on the second. + See boards documentation for more details: https://developer.monday.com/api-reference/docs/boards#queries. + """ + + def __post_init__(self, parameters: Mapping[str, Any]): + # `self._page` corresponds to board page number + # `self._sub_page` corresponds to item page number within its board + self.start_from_page = 1 + self._page: Optional[int] = self.start_from_page + self._sub_page: Optional[int] = self.start_from_page + + def next_page_token(self, response, last_records: List[Mapping[str, Any]]) -> Optional[Tuple[Optional[int], Optional[int]]]: + """ + `items` stream use a separate 2 level pagination strategy where: + 1st level `boards` - incremental pagination + 2nd level `items_page` - cursor pagination + + Attributes: + response: Contains `boards` and corresponding lists of `items` for each `board` + last_records: Parsed `items` from the response + """ + data = response.json()["data"] + boards = data.get("boards", []) + next_items_page = data.get("next_items_page", {}) + if boards: + # there is always only one board due to limit=1, so in one request we extract all 'items_page' for one board only + board = boards[0] + cursor = board.get("items_page", {}).get("cursor", None) + elif next_items_page: + cursor = next_items_page.get("cursor", None) + else: + # Finish pagination if there is no more data + return None + + if cursor: + return self._page, cursor + else: + self._page += 1 + return self._page, None diff --git a/airbyte-integrations/connectors/source-monday/source_monday/manifest.yaml b/airbyte-integrations/connectors/source-monday/source_monday/manifest.yaml index dc482bae0f681..658c635cf2061 100644 --- a/airbyte-integrations/connectors/source-monday/source_monday/manifest.yaml +++ b/airbyte-integrations/connectors/source-monday/source_monday/manifest.yaml @@ -11,11 +11,11 @@ definitions: field_path: - "data" - "{{ parameters['name'] }}" + requester: type: CustomRequester class_name: "source_monday.MondayGraphqlRequester" url_base: "https://api.monday.com/v2" - http_method: "GET" authenticator: type: BearerAuthenticator api_token: "{{ config.get('credentials', {}).get('api_token') if config.get('credentials', {}).get('auth_type') == 'api_token' else config.get('credentials', {}).get('access_token') if config.get('credentials', {}).get('auth_type') == 'oauth2.0' else config.get('api_token', '') }}" @@ -49,6 +49,7 @@ definitions: action: RETRY backoff_strategies: - type: ExponentialBackoffStrategy + default_paginator: type: "DefaultPaginator" pagination_strategy: @@ -62,17 +63,20 @@ definitions: $ref: "#/definitions/requester" paginator: $ref: "#/definitions/default_paginator" + base_stream: retriever: $ref: "#/definitions/retriever" schema_loader: $ref: "#/definitions/schema_loader" primary_key: "id" + base_nopagination_stream: retriever: $ref: "#/definitions/retriever" paginator: type: NoPagination + tags_stream: $ref: "#/definitions/base_nopagination_stream" $parameters: @@ -105,6 +109,12 @@ definitions: class_name: "source_monday.item_pagination_strategy.ItemPaginationStrategy" type: "CustomPaginationStrategy" + cursor_paginator: + $ref: "#/definitions/default_paginator" + pagination_strategy: + class_name: "source_monday.item_pagination_strategy.ItemCursorPaginationStrategy" + type: "CustomPaginationStrategy" + activity_logs_stream: description: "https://developers.intercom.com/intercom-api-reference/reference/scroll-over-all-companies" incremental_sync: @@ -173,12 +183,13 @@ definitions: page_size: 20 nested_items_per_page: 20 parent_key: "pulse_id" - field_path: ["data", "items", "*"] - additional_field_path: ["data", "boards", "*", "items", "*"] + field_path: ["data", "boards", "*", "items_page", "items", "*"] # for first and further incremental pagination responses + field_path_pagination: ["data", "next_items_page", "items", "*"] # for cursor pagination responses + field_path_incremental: ["data", "items", "*"] # for incremental sync responses retriever: $ref: "#/definitions/base_stream/retriever" paginator: - $ref: "#/definitions/double_paginator" + $ref: "#/definitions/cursor_paginator" record_selector: $ref: "#/definitions/selector" extractor: diff --git a/airbyte-integrations/connectors/source-monday/source_monday/schemas/boards.json b/airbyte-integrations/connectors/source-monday/source_monday/schemas/boards.json index da156dbb7f61c..992636621db79 100644 --- a/airbyte-integrations/connectors/source-monday/source_monday/schemas/boards.json +++ b/airbyte-integrations/connectors/source-monday/source_monday/schemas/boards.json @@ -2,6 +2,8 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { + "id": { "type": ["null", "string"] }, + "name": { "type": ["null", "string"] }, "board_kind": { "type": ["null", "string"] }, "type": { "type": ["null", "string"] }, "columns": { @@ -37,26 +39,23 @@ } } }, - "id": { "type": ["null", "string"] }, - "name": { "type": ["null", "string"] }, "owners": { "type": ["null", "array"], "items": { "type": ["null", "object"], "additionalProperties": true, "properties": { - "id": { "type": ["null", "integer"] } + "id": { "type": ["null", "string"] } } } }, "creator": { "type": ["null", "object"], "properties": { - "id": { "type": ["null", "integer"] } + "id": { "type": ["null", "string"] } } }, "permissions": { "type": ["null", "string"] }, - "pos": { "type": ["null", "string"] }, "state": { "type": ["null", "string"] }, "subscribers": { "type": ["null", "array"], @@ -64,7 +63,7 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "id": { "type": ["null", "integer"] } + "id": { "type": ["null", "string"] } } } }, @@ -113,7 +112,7 @@ "workspace": { "type": ["null", "object"], "properties": { - "id": { "type": ["null", "integer"] }, + "id": { "type": ["null", "string"] }, "name": { "type": ["null", "string"] }, "kind": { "type": ["null", "string"] }, "description": { "type": ["null", "string"] } diff --git a/airbyte-integrations/connectors/source-monday/source_monday/schemas/items.json b/airbyte-integrations/connectors/source-monday/source_monday/schemas/items.json index 27f02d52354fe..a793a5b281b30 100644 --- a/airbyte-integrations/connectors/source-monday/source_monday/schemas/items.json +++ b/airbyte-integrations/connectors/source-monday/source_monday/schemas/items.json @@ -2,6 +2,8 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { + "id": { "type": ["null", "string"] }, + "name": { "type": ["null", "string"] }, "assets": { "type": ["null", "array"], "items": { @@ -18,7 +20,7 @@ "uploaded_by": { "type": ["null", "object"], "properties": { - "id": { "type": ["null", "integer"] } + "id": { "type": ["null", "string"] } } }, "url": { "type": ["null", "string"] }, @@ -38,11 +40,8 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "additional_info": { "type": ["null", "string"] }, - "description": { "type": ["null", "string"] }, "id": { "type": ["null", "string"] }, "text": { "type": ["null", "string"] }, - "title": { "type": ["null", "string"] }, "type": { "type": ["null", "string"] }, "value": { "type": ["null", "string"] } } @@ -56,8 +55,6 @@ "id": { "type": ["null", "string"] } } }, - "id": { "type": ["null", "string"] }, - "name": { "type": ["null", "string"] }, "parent_item": { "type": ["null", "object"], "properties": { @@ -71,7 +68,7 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "id": { "type": ["null", "integer"] } + "id": { "type": ["null", "string"] } } } }, diff --git a/airbyte-integrations/connectors/source-monday/source_monday/schemas/tags.json b/airbyte-integrations/connectors/source-monday/source_monday/schemas/tags.json index c96a58d1d42af..e1a4faeb63db9 100644 --- a/airbyte-integrations/connectors/source-monday/source_monday/schemas/tags.json +++ b/airbyte-integrations/connectors/source-monday/source_monday/schemas/tags.json @@ -3,7 +3,7 @@ "type": "object", "properties": { "color": { "type": ["null", "string"] }, - "id": { "type": ["null", "integer"] }, + "id": { "type": ["null", "string"] }, "name": { "type": ["null", "string"] } } } diff --git a/airbyte-integrations/connectors/source-monday/source_monday/schemas/teams.json b/airbyte-integrations/connectors/source-monday/source_monday/schemas/teams.json index 16cb865fcc92a..0bccac5fd4fa7 100644 --- a/airbyte-integrations/connectors/source-monday/source_monday/schemas/teams.json +++ b/airbyte-integrations/connectors/source-monday/source_monday/schemas/teams.json @@ -11,7 +11,7 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "id": { "type": ["null", "integer"] } + "id": { "type": ["null", "string"] } } } } diff --git a/airbyte-integrations/connectors/source-monday/source_monday/schemas/updates.json b/airbyte-integrations/connectors/source-monday/source_monday/schemas/updates.json index d0e004a69982c..8dc809329358f 100644 --- a/airbyte-integrations/connectors/source-monday/source_monday/schemas/updates.json +++ b/airbyte-integrations/connectors/source-monday/source_monday/schemas/updates.json @@ -18,7 +18,7 @@ "uploaded_by": { "type": ["null", "object"], "properties": { - "id": { "type": ["null", "integer"] } + "id": { "type": ["null", "string"] } } }, "url": { "type": ["null", "string"] }, diff --git a/airbyte-integrations/connectors/source-monday/source_monday/schemas/users.json b/airbyte-integrations/connectors/source-monday/source_monday/schemas/users.json index a064bdc3f4bca..bd2347a4fc2ba 100644 --- a/airbyte-integrations/connectors/source-monday/source_monday/schemas/users.json +++ b/airbyte-integrations/connectors/source-monday/source_monday/schemas/users.json @@ -8,7 +8,7 @@ "join_date": { "type": ["null", "string"], "format": "date" }, "email": { "type": ["null", "string"] }, "enabled": { "type": ["null", "boolean"] }, - "id": { "type": ["null", "integer"] }, + "id": { "type": ["null", "string"] }, "is_admin": { "type": ["null", "boolean"] }, "is_guest": { "type": ["null", "boolean"] }, "is_pending": { "type": ["null", "boolean"] }, diff --git a/airbyte-integrations/connectors/source-monday/source_monday/schemas/workspaces.json b/airbyte-integrations/connectors/source-monday/source_monday/schemas/workspaces.json index af7bf79b2d97f..3f8439055873f 100644 --- a/airbyte-integrations/connectors/source-monday/source_monday/schemas/workspaces.json +++ b/airbyte-integrations/connectors/source-monday/source_monday/schemas/workspaces.json @@ -4,14 +4,14 @@ "properties": { "created_at": { "type": ["null", "string"], "format": "date-time" }, "description": { "type": ["null", "string"] }, - "id": { "type": ["null", "integer"] }, + "id": { "type": ["null", "string"] }, "kind": { "type": ["null", "string"] }, "name": { "type": ["null", "string"] }, "state": { "type": ["null", "string"] }, "account_product": { "type": ["null", "object"], "properties": { - "id": { "type": ["null", "integer"] }, + "id": { "type": ["null", "string"] }, "kind": { "type": ["null", "string"] } } }, @@ -21,7 +21,7 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "id": { "type": ["null", "integer"] } + "id": { "type": ["null", "string"] } } } }, @@ -43,7 +43,7 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "id": { "type": ["null", "integer"] }, + "id": { "type": ["null", "string"] }, "name": { "type": ["null", "string"] } } } @@ -54,7 +54,7 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "id": { "type": ["null", "integer"] }, + "id": { "type": ["null", "string"] }, "name": { "type": ["null", "string"] } } } @@ -65,7 +65,7 @@ "type": ["null", "object"], "additionalProperties": true, "properties": { - "id": { "type": ["null", "integer"] } + "id": { "type": ["null", "string"] } } } } diff --git a/airbyte-integrations/connectors/source-monday/unit_tests/test_extractor.py b/airbyte-integrations/connectors/source-monday/unit_tests/test_extractor.py index cf6a4bfc871ce..869c7ab7bbcad 100644 --- a/airbyte-integrations/connectors/source-monday/unit_tests/test_extractor.py +++ b/airbyte-integrations/connectors/source-monday/unit_tests/test_extractor.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock -from source_monday.extractor import MondayActivityExtractor +from source_monday.extractor import MondayActivityExtractor, MondayIncrementalItemsExtractor def test_extract_records(): @@ -34,3 +34,30 @@ def test_extract_records(): assert len(records) == 1 assert records[0]["pulse_id"] == 123 assert records[0]["created_at_int"] == 1636738688 + + +def test_extract_records_incremental(): + # Mock the response + response = MagicMock() + response_body = { + "data": { + "boards": [ + { + "id": 1 + } + ] + } + } + + response.json.return_value = response_body + extractor = MondayIncrementalItemsExtractor( + parameters={}, + field_path=["data", "ccccc"], + config=MagicMock(), + field_path_pagination=["data", "bbbb"], + field_path_incremental=["data", "boards", "*"] + ) + records = extractor.extract_records(response) + + # Assertions + assert records == [{'id': 1}] diff --git a/airbyte-integrations/connectors/source-monday/unit_tests/test_graphql_requester.py b/airbyte-integrations/connectors/source-monday/unit_tests/test_graphql_requester.py index f080b8c085b49..b4f46146b6bc1 100644 --- a/airbyte-integrations/connectors/source-monday/unit_tests/test_graphql_requester.py +++ b/airbyte-integrations/connectors/source-monday/unit_tests/test_graphql_requester.py @@ -53,8 +53,8 @@ nested_array_schema, "items", {}, - {"query": "query{boards(limit:100,page:2){items(limit:100,page:1){root{nested{nested_of_nested}},sibling}}}"}, - {"next_page_token": (2, 1)}, + {"query": 'query{next_items_page(limit:100,cursor:"cursor_bla"){cursor,items{root{nested{nested_of_nested}},sibling}}}'}, + {"next_page_token": (2, "cursor_bla")}, id="test_get_request_params_produces_graphql_query_for_items_stream", ), pytest.param( @@ -150,3 +150,10 @@ def test_build_items_incremental_query(monday_requester): built_query = monday_requester._build_items_incremental_query(object_name, field_schema, stream_slice) assert built_query == 'items(limit:100,ids:[1, 2, 3]){id,name}' + + +def test_get_request_headers(monday_requester): + + headers = monday_requester.get_request_headers() + + assert headers == {'API-Version': '2024-01'} diff --git a/airbyte-integrations/connectors/source-monday/unit_tests/test_item_pagination_strategy.py b/airbyte-integrations/connectors/source-monday/unit_tests/test_item_pagination_strategy.py index 16988efc620df..979c72284713b 100644 --- a/airbyte-integrations/connectors/source-monday/unit_tests/test_item_pagination_strategy.py +++ b/airbyte-integrations/connectors/source-monday/unit_tests/test_item_pagination_strategy.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import pytest -from source_monday.item_pagination_strategy import ItemPaginationStrategy +from source_monday.item_pagination_strategy import ItemCursorPaginationStrategy, ItemPaginationStrategy @pytest.mark.parametrize( @@ -40,3 +40,42 @@ def test_item_pagination_strategy(response_json, last_records, expected): response.json.return_value = response_json assert strategy.next_page_token(response, last_records) == expected + +@pytest.mark.parametrize( + ("response_json", "last_records", "expected"), + [ + pytest.param( + {"data": {"boards": [{"items_page": {"cursor": "bla", "items":[{"id": "1"}]}}]}}, + [], + (1, 'bla'), + id="test_cursor_in_first_request", + ), + pytest.param( + {"data": {"next_items_page": {"cursor": "bla2", "items":[{"id": "1"}]}}}, + [], + (1, 'bla2'), + id="test_cursor_in_next_page", + ), + pytest.param( + {"data": {"next_items_page": {"items": [{"id": "1"}]}}}, + [], + (2, None), + id="test_next_board_page", + ), + pytest.param( + {"data": {"boards": []}}, + [], + None, + id="test_end_pagination", + ), + ], +) +def test_item_cursor_pagination_strategy(response_json, last_records, expected): + strategy = ItemCursorPaginationStrategy( + page_size=1, + parameters={"items_per_page": 1}, + ) + response = MagicMock() + response.json.return_value = response_json + + assert strategy.next_page_token(response, last_records) == expected diff --git a/docs/integrations/sources/monday-migrations.md b/docs/integrations/sources/monday-migrations.md new file mode 100644 index 0000000000000..9d095b9e127f7 --- /dev/null +++ b/docs/integrations/sources/monday-migrations.md @@ -0,0 +1,77 @@ +# Monday Migration Guide + +## Upgrading to 2.0.0 + +Source Monday has deprecated API version 2023-07. We have upgraded the connector to the latest API version 2024-01. In this new version, the Id field has changed from an integer to a string in the streams Boards, Items, Tags, Teams, Updates, Users and Workspaces. Please reset affected streams. + +## Connector Upgrade Guide + +### For Airbyte Open Source: Update the local connector image + +Airbyte Open Source users must manually update the connector image in their local registry before proceeding with the migration. To do so: + +1. Select **Settings** in the main navbar. + 1. Select **Sources**. +2. Find Monday in the list of connectors. + +:::note +You will see two versions listed, the current in-use version and the latest version available. +::: + +3. Select **Change** to update your OSS version to the latest available version. + +### Update the connector version + +1. Select **Sources** in the main navbar. +2. Select the instance of the connector you wish to upgrade. + +:::note +Each instance of the connector must be updated separately. If you have created multiple instances of a connector, updating one will not affect the others. +::: + +3. Select **Upgrade** + 1. Follow the prompt to confirm you are ready to upgrade to the new version. + + +### Refresh schemas and reset data + +1. Select **Connections** in the main navbar. +2. Select the connection(s) affected by the update. +3. Select the **Replication** tab. + 1. Select **Refresh source schema**. + 2. Select **OK**. +:::note +Any detected schema changes will be listed for your review. +::: +4. Select **Save changes** at the bottom of the page. + 1. Ensure the **Reset all streams** option is checked. +5. Select **Save connection**. +:::note +This will reset the data in your destination and initiate a fresh sync. +::: + +For more information on resetting your data in Airbyte, see [this page](https://docs.airbyte.com/operator-guides/reset). + + +### Refresh affected schemas and reset data + +1. Select **Connections** in the main navb nar. + 1. Select the connection(s) affected by the update. +2. Select the **Replication** tab. + 1. Select **Refresh source schema**. + 2. Select **OK**. +:::note +Any detected schema changes will be listed for your review. +::: +3. Select **Save changes** at the bottom of the page. + 1. Ensure the **Reset affected streams** option is checked. +:::note +Depending on destination type you may not be prompted to reset your data. +::: +4. Select **Save connection**. +:::note +This will reset the data in your destination and initiate a fresh sync. +::: + +For more information on resetting your data in Airbyte, see [this page](https://docs.airbyte.com/operator-guides/reset). + diff --git a/docs/integrations/sources/monday.md b/docs/integrations/sources/monday.md index 596d6ae1c6867..f3c8ab44f4fdc 100644 --- a/docs/integrations/sources/monday.md +++ b/docs/integrations/sources/monday.md @@ -74,6 +74,7 @@ The Monday connector should not run into Monday API limitations under normal usa | Version | Date | Pull Request | Subject | |:--------|:-----------|:----------------------------------------------------------|:------------------------------------------------------------------------| +| 2.0.0 | 2024-01-12 | [34108](https://github.com/airbytehq/airbyte/pull/34108) | Migrated to the latest API version: 2024-01 | | 1.1.4 | 2023-12-13 | [33448](https://github.com/airbytehq/airbyte/pull/33448) | Increase test coverage and migrate to base image | | 1.1.3 | 2023-09-23 | [30248](https://github.com/airbytehq/airbyte/pull/30248) | Add new field "type" to board stream | | 1.1.2 | 2023-08-23 | [29777](https://github.com/airbytehq/airbyte/pull/29777) | Add retry for `502` error | From 3787582fa8764b6aa80377961aacf58a4785d795 Mon Sep 17 00:00:00 2001 From: Artem Inzhyyants <36314070+artem1205@users.noreply.github.com> Date: Mon, 15 Jan 2024 11:34:07 +0100 Subject: [PATCH 087/574] =?UTF-8?q?=F0=9F=90=9B=20Source=20Linkedin=20Ads:?= =?UTF-8?q?=20Use=20stream=20slices=20for=20Analytics=20streams=20(#34222)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../source-linkedin-ads/metadata.yaml | 4 +- .../connectors/source-linkedin-ads/setup.py | 4 +- .../source_linkedin_ads/analytics.py | 207 ---------- .../source_linkedin_ads/analytics_streams.py | 373 ++++++++++++++++++ .../source_linkedin_ads/source.py | 9 +- .../source_linkedin_ads/streams.py | 183 +-------- .../source_linkedin_ads/utils.py | 15 +- .../unit_tests/__init__.py | 0 .../samples/test_data_for_analytics.py | 178 --------- .../test_chunk_analytics_fields.py | 39 -- .../test_make_analytics_slices.py | 18 - .../analytics_tests/test_make_date_slices.py | 24 -- .../analytics_tests/test_merge_chunks.py | 13 - .../unit_tests/output_slices.json | 126 ++++++ .../response_1.json | 71 ++++ .../response_2.json | 61 +++ .../response_3.json | 37 ++ .../unit_tests/test_analytics_streams.py | 109 +++++ .../{source_tests => }/test_source.py | 0 docs/integrations/sources/linkedin-ads.md | 1 + 20 files changed, 800 insertions(+), 672 deletions(-) delete mode 100644 airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/analytics.py create mode 100644 airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/analytics_streams.py create mode 100644 airbyte-integrations/connectors/source-linkedin-ads/unit_tests/__init__.py delete mode 100644 airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/samples/test_data_for_analytics.py delete mode 100644 airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_chunk_analytics_fields.py delete mode 100644 airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_make_analytics_slices.py delete mode 100644 airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_make_date_slices.py delete mode 100644 airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_merge_chunks.py create mode 100644 airbyte-integrations/connectors/source-linkedin-ads/unit_tests/output_slices.json create mode 100644 airbyte-integrations/connectors/source-linkedin-ads/unit_tests/responses/ad_member_country_analytics/response_1.json create mode 100644 airbyte-integrations/connectors/source-linkedin-ads/unit_tests/responses/ad_member_country_analytics/response_2.json create mode 100644 airbyte-integrations/connectors/source-linkedin-ads/unit_tests/responses/ad_member_country_analytics/response_3.json create mode 100644 airbyte-integrations/connectors/source-linkedin-ads/unit_tests/test_analytics_streams.py rename airbyte-integrations/connectors/source-linkedin-ads/unit_tests/{source_tests => }/test_source.py (100%) diff --git a/airbyte-integrations/connectors/source-linkedin-ads/metadata.yaml b/airbyte-integrations/connectors/source-linkedin-ads/metadata.yaml index 66936aa3b87d4..8954fcbe8dfca 100644 --- a/airbyte-integrations/connectors/source-linkedin-ads/metadata.yaml +++ b/airbyte-integrations/connectors/source-linkedin-ads/metadata.yaml @@ -7,11 +7,11 @@ data: - linkedin.com - api.linkedin.com connectorBuildOptions: - baseImage: docker.io/airbyte/python-connector-base:1.1.0@sha256:bd98f6505c6764b1b5f99d3aedc23dfc9e9af631a62533f60eb32b1d3dbab20c + baseImage: docker.io/airbyte/python-connector-base:1.2.0@sha256:c22a9d97464b69d6ef01898edf3f8612dc11614f05a84984451dde195f337db9 connectorSubtype: api connectorType: source definitionId: 137ece28-5434-455c-8f34-69dc3782f451 - dockerImageTag: 0.6.5 + dockerImageTag: 0.6.6 dockerRepository: airbyte/source-linkedin-ads documentationUrl: https://docs.airbyte.com/integrations/sources/linkedin-ads githubIssueLabel: source-linkedin-ads diff --git a/airbyte-integrations/connectors/source-linkedin-ads/setup.py b/airbyte-integrations/connectors/source-linkedin-ads/setup.py index 1c15f41abf5ff..2021f6cd6fcd8 100644 --- a/airbyte-integrations/connectors/source-linkedin-ads/setup.py +++ b/airbyte-integrations/connectors/source-linkedin-ads/setup.py @@ -5,9 +5,7 @@ from setuptools import find_packages, setup -MAIN_REQUIREMENTS = [ - "airbyte-cdk~=0.50", -] +MAIN_REQUIREMENTS = ["airbyte-cdk"] TEST_REQUIREMENTS = [ "pytest-mock~=3.6.1", diff --git a/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/analytics.py b/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/analytics.py deleted file mode 100644 index 6963ecf1ac9fa..0000000000000 --- a/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/analytics.py +++ /dev/null @@ -1,207 +0,0 @@ -# -# Copyright (c) 2023 Airbyte, Inc., all rights reserved. -# - - -from collections import defaultdict -from typing import Any, Iterable, List, Mapping - -import pendulum as pdm - -from .utils import get_parent_stream_values - -# LinkedIn has a max of 20 fields per request. We make chunks by size of 19 fields -# to have the `dateRange` be included as well. -FIELDS_CHUNK_SIZE = 19 -# Number of days ahead for date slices, from start date. -WINDOW_IN_DAYS = 30 -# List of Reporting Metrics fields available for fetch -ANALYTICS_FIELDS_V2: List = [ - "actionClicks", - "adUnitClicks", - "approximateUniqueImpressions", - "cardClicks", - "cardImpressions", - "clicks", - "commentLikes", - "comments", - "companyPageClicks", - "conversionValueInLocalCurrency", - "costInLocalCurrency", - "costInUsd", - "dateRange", - "documentCompletions", - "documentFirstQuartileCompletions", - "documentMidpointCompletions", - "documentThirdQuartileCompletions", - "downloadClicks", - "externalWebsiteConversions", - "externalWebsitePostClickConversions", - "externalWebsitePostViewConversions", - "follows", - "fullScreenPlays", - "impressions", - "jobApplications", - "jobApplyClicks", - "landingPageClicks", - "leadGenerationMailContactInfoShares", - "leadGenerationMailInterestedClicks", - "likes", - "oneClickLeadFormOpens", - "oneClickLeads", - "opens", - "otherEngagements", - "pivotValues", - "postClickJobApplications", - "postClickJobApplyClicks", - "postClickRegistrations", - "postViewJobApplications", - "postViewJobApplyClicks", - "postViewRegistrations", - "reactions", - "registrations", - "sends", - "shares", - "talentLeads", - "textUrlClicks", - "totalEngagements", - "validWorkEmailLeads", - "videoCompletions", - "videoFirstQuartileCompletions", - "videoMidpointCompletions", - "videoStarts", - "videoThirdQuartileCompletions", - "videoViews", - "viralCardClicks", - "viralCardImpressions", - "viralClicks", - "viralCommentLikes", - "viralComments", - "viralCompanyPageClicks", - "viralDocumentCompletions", - "viralDocumentFirstQuartileCompletions", - "viralDocumentMidpointCompletions", - "viralDocumentThirdQuartileCompletions", - "viralDownloadClicks", - "viralExternalWebsiteConversions", - "viralExternalWebsitePostClickConversions", - "viralExternalWebsitePostViewConversions", - "viralFollows", - "viralFullScreenPlays", - "viralImpressions", - "viralJobApplications", - "viralJobApplyClicks", - "viralLandingPageClicks", - "viralLikes", - "viralOneClickLeadFormOpens", - "viralOneClickLeads", - "viralOtherEngagements", - "viralPostClickJobApplications", - "viralPostClickJobApplyClicks", - "viralPostClickRegistrations", - "viralPostViewJobApplications", - "viralPostViewJobApplyClicks", - "viralPostViewRegistrations", - "viralReactions", - "viralRegistrations", - "viralShares", - "viralTotalEngagements", - "viralVideoCompletions", - "viralVideoFirstQuartileCompletions", - "viralVideoMidpointCompletions", - "viralVideoStarts", - "viralVideoThirdQuartileCompletions", - "viralVideoViews", -] -# Fields that are always present in fields_set chunks -BASE_ANALLYTICS_FIELDS = ["dateRange"] - - -def chunk_analytics_fields( - fields: List = ANALYTICS_FIELDS_V2, - base_fields: List = BASE_ANALLYTICS_FIELDS, - fields_chunk_size: int = FIELDS_CHUNK_SIZE, -) -> Iterable[List]: - """ - Chunks the list of available fields into the chunks of equal size. - """ - # Make chunks - chunks = list((fields[f : f + fields_chunk_size] for f in range(0, len(fields), fields_chunk_size))) - # Make sure base_fields are within the chunks - for chunk in chunks: - for field in base_fields: - if field not in chunk: - chunk.append(field) - yield from chunks - - -def make_date_slices(start_date: str, end_date: str = None, window_in_days: int = WINDOW_IN_DAYS) -> Iterable[List]: - """ - Produces date slices from start_date to end_date (if specified), - otherwise end_date will be present time. - """ - start = pdm.parse(start_date) - end = pdm.parse(end_date) if end_date else pdm.now() - date_slices = [] - while start < end: - slice_end_date = start.add(days=window_in_days) - date_slice = { - "start.day": start.day, - "start.month": start.month, - "start.year": start.year, - "end.day": slice_end_date.day, - "end.month": slice_end_date.month, - "end.year": slice_end_date.year, - } - date_slices.append({"dateRange": date_slice}) - start = slice_end_date - yield from date_slices - - -def make_analytics_slices( - record: Mapping[str, Any], key_value_map: Mapping[str, Any], start_date: str, end_date: str = None -) -> Iterable[Mapping[str, Any]]: - """ - We drive the ability to directly pass the prepared parameters inside the stream_slice. - The output of this method is ready slices for analytics streams: - """ - # define the base_slice - base_slice = get_parent_stream_values(record, key_value_map) - # add chunked fields, date_slices to the base_slice - analytics_slices = [] - for fields_set in chunk_analytics_fields(): - base_slice["fields"] = ",".join(map(str, fields_set)) - for date_slice in make_date_slices(start_date, end_date): - base_slice.update(**date_slice) - analytics_slices.append(base_slice.copy()) - yield from analytics_slices - - -def update_analytics_params(stream_slice: Mapping[str, Any]) -> Mapping[str, Any]: - """ - Produces the date range parameters from input stream_slice - """ - date_range = stream_slice["dateRange"] - return { - "dateRange": f"(start:(year:{date_range['start.year']},month:{date_range['start.month']},day:{date_range['start.day']})," - f"end:(year:{date_range['end.year']},month:{date_range['end.month']},day:{date_range['end.day']}))", - # Chunk of fields - "fields": stream_slice["fields"], - } - - -def merge_chunks(chunked_result: Iterable[Mapping[str, Any]], merge_by_key: str) -> Iterable[Mapping[str, Any]]: - """ - We need to merge the chunked API responses - into the single structure using any available unique field. - """ - # Merge the pieces together - merged = defaultdict(dict) - for chunk in chunked_result: - for item in chunk: - merged[item[merge_by_key]].update(item) - # Clean up the result by getting out the values of the merged keys - result = [] - for item in merged: - result.append(merged.get(item)) - yield from result diff --git a/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/analytics_streams.py b/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/analytics_streams.py new file mode 100644 index 0000000000000..f58da0e25c8b5 --- /dev/null +++ b/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/analytics_streams.py @@ -0,0 +1,373 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + +from abc import ABC, abstractmethod +from collections import defaultdict +from typing import Any, Iterable, List, Mapping, MutableMapping, Optional +from urllib.parse import urlencode + +import pendulum +import requests +from airbyte_cdk.sources.streams.core import package_name_from_class +from airbyte_cdk.sources.utils import casing +from airbyte_cdk.sources.utils.schema_helpers import ResourceSchemaLoader +from airbyte_protocol.models import SyncMode +from source_linkedin_ads.streams import Campaigns, Creatives, IncrementalLinkedinAdsStream + +from .utils import get_parent_stream_values, transform_data + +# Number of days ahead for date slices, from start date. +WINDOW_IN_DAYS = 30 +# List of Reporting Metrics fields available for fetch +ANALYTICS_FIELDS_V2: List = [ + "actionClicks", + "adUnitClicks", + "approximateUniqueImpressions", + "cardClicks", + "cardImpressions", + "clicks", + "commentLikes", + "comments", + "companyPageClicks", + "conversionValueInLocalCurrency", + "costInLocalCurrency", + "costInUsd", + "dateRange", + "documentCompletions", + "documentFirstQuartileCompletions", + "documentMidpointCompletions", + "documentThirdQuartileCompletions", + "downloadClicks", + "externalWebsiteConversions", + "externalWebsitePostClickConversions", + "externalWebsitePostViewConversions", + "follows", + "fullScreenPlays", + "impressions", + "jobApplications", + "jobApplyClicks", + "landingPageClicks", + "leadGenerationMailContactInfoShares", + "leadGenerationMailInterestedClicks", + "likes", + "oneClickLeadFormOpens", + "oneClickLeads", + "opens", + "otherEngagements", + "pivotValues", + "postClickJobApplications", + "postClickJobApplyClicks", + "postClickRegistrations", + "postViewJobApplications", + "postViewJobApplyClicks", + "postViewRegistrations", + "reactions", + "registrations", + "sends", + "shares", + "talentLeads", + "textUrlClicks", + "totalEngagements", + "validWorkEmailLeads", + "videoCompletions", + "videoFirstQuartileCompletions", + "videoMidpointCompletions", + "videoStarts", + "videoThirdQuartileCompletions", + "videoViews", + "viralCardClicks", + "viralCardImpressions", + "viralClicks", + "viralCommentLikes", + "viralComments", + "viralCompanyPageClicks", + "viralDocumentCompletions", + "viralDocumentFirstQuartileCompletions", + "viralDocumentMidpointCompletions", + "viralDocumentThirdQuartileCompletions", + "viralDownloadClicks", + "viralExternalWebsiteConversions", + "viralExternalWebsitePostClickConversions", + "viralExternalWebsitePostViewConversions", + "viralFollows", + "viralFullScreenPlays", + "viralImpressions", + "viralJobApplications", + "viralJobApplyClicks", + "viralLandingPageClicks", + "viralLikes", + "viralOneClickLeadFormOpens", + "viralOneClickLeads", + "viralOtherEngagements", + "viralPostClickJobApplications", + "viralPostClickJobApplyClicks", + "viralPostClickRegistrations", + "viralPostViewJobApplications", + "viralPostViewJobApplyClicks", + "viralPostViewRegistrations", + "viralReactions", + "viralRegistrations", + "viralShares", + "viralTotalEngagements", + "viralVideoCompletions", + "viralVideoFirstQuartileCompletions", + "viralVideoMidpointCompletions", + "viralVideoStarts", + "viralVideoThirdQuartileCompletions", + "viralVideoViews", +] + + +class LinkedInAdsAnalyticsStream(IncrementalLinkedinAdsStream, ABC): + """ + AdAnalytics Streams more info: + https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/ads-reporting?tabs=curl&view=li-lms-2023-05#analytics-finder + """ + + endpoint = "adAnalytics" + # For Analytics streams, the primary_key is the entity of the pivot [Campaign URN, Creative URN, etc.] + `end_date` + primary_key = ["pivotValue", "end_date"] + cursor_field = "end_date" + records_limit = 15000 + FIELDS_CHUNK_SIZE = 19 + + def get_json_schema(self) -> Mapping[str, Any]: + return ResourceSchemaLoader(package_name_from_class(self.__class__)).get_schema("ad_analytics") + + def __init__(self, name: str = None, pivot_by: str = None, time_granularity: str = None, **kwargs): + self.user_stream_name = name + if pivot_by: + self.pivot_by = pivot_by + if time_granularity: + self.time_granularity = time_granularity + super().__init__(**kwargs) + + @property + @abstractmethod + def search_param(self) -> str: + """ + :return: Search parameters for the request + """ + + @property + @abstractmethod + def search_param_value(self) -> str: + """ + :return: Name field to filter by + """ + + @property + @abstractmethod + def parent_values_map(self) -> Mapping[str, str]: + """ + :return: Mapping for parent child relation + """ + + @property + def name(self) -> str: + """We override the stream name to let the user change it via configuration.""" + name = self.user_stream_name or self.__class__.__name__ + return casing.camel_to_snake(name) + + @property + def base_analytics_params(self) -> MutableMapping[str, Any]: + """Define the base parameters for analytics streams""" + return {"q": "analytics", "pivot": f"(value:{self.pivot_by})", "timeGranularity": f"(value:{self.time_granularity})"} + + def request_headers( + self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None + ) -> Mapping[str, Any]: + headers = super().request_headers(stream_state, stream_slice, next_page_token) + return headers | {"X-Restli-Protocol-Version": "2.0.0"} + + def request_params( + self, + stream_state: Mapping[str, Any], + stream_slice: Mapping[str, Any] = None, + next_page_token: Mapping[str, Any] = None, + ) -> MutableMapping[str, Any]: + params = self.base_analytics_params + params.update(**self.update_analytics_params(stream_slice)) + params[self.search_param] = f"List(urn%3Ali%3A{self.search_param_value}%3A{self.get_primary_key_from_slice(stream_slice)})" + return urlencode(params, safe="():,%") + + @staticmethod + def update_analytics_params(stream_slice: Mapping[str, Any]) -> Mapping[str, Any]: + """ + Produces the date range parameters from input stream_slice + """ + date_range = stream_slice["dateRange"] + return { + "dateRange": f"(start:(year:{date_range['start.year']},month:{date_range['start.month']},day:{date_range['start.day']})," + f"end:(year:{date_range['end.year']},month:{date_range['end.month']},day:{date_range['end.day']}))", + # Chunk of fields + "fields": stream_slice["fields"], + } + + def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: + """ + Pagination is not supported + (See Restrictions: https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/ads-reporting?view=li-lms-2023-09&tabs=http#restrictions) + """ + parsed_response = response.json() + if len(parsed_response.get("elements")) < self.records_limit: + return None + raise Exception( + f"Limit {self.records_limit} elements exceeded. " + f"Try to request your data in more granular pieces. " + f"(For example switch `Time Granularity` from MONTHLY to DAILY)" + ) + + def get_primary_key_from_slice(self, stream_slice) -> str: + return stream_slice.get(self.primary_slice_key) + + def stream_slices( + self, *, sync_mode: SyncMode, cursor_field: Optional[List[str]] = None, stream_state: Optional[Mapping[str, Any]] = None + ) -> Iterable[List[Mapping[str, Any]]]: + """ + LinkedIn has a max of 20 fields per request. We make chunks by size of 19 fields to have the `dateRange` be included as well. + https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/ads-reporting?view=li-lms-2023-05&tabs=http#requesting-specific-metrics-in-the-analytics-finder + + :param sync_mode: + :param cursor_field: + :param stream_state: + :return: Iterable with List of stream slices within the same date range and chunked fields, example + [{'campaign_id': 123, 'fields': 'field_1,field_2,dateRange', 'dateRange': {'start.day': 1, 'start.month': 1, 'start.year': 2020, 'end.day': 30, 'end.month': 1, 'end.year': 2020}}, + {'campaign_id': 123, 'fields': 'field_2,field_3,dateRange', 'dateRange': {'start.day': 1, 'start.month': 1, 'start.year': 2020, 'end.day': 30, 'end.month': 1, 'end.year': 2020}}, + {'campaign_id': 123, 'fields': 'field_4,field_5,dateRange', 'dateRange': {'start.day': 1, 'start.month': 1, 'start.year': 2020, 'end.day': 30, 'end.month': 1, 'end.year': 2020}}] + + """ + parent_stream = self.parent_stream(config=self.config) + stream_state = stream_state or {self.cursor_field: self.config.get("start_date")} + for record in parent_stream.read_records(sync_mode=sync_mode): + base_slice = get_parent_stream_values(record, self.parent_values_map) + for date_slice in self.get_date_slices(stream_state.get(self.cursor_field), self.config.get("end_date")): + date_slice_with_fields: List = [] + for fields_set in self.chunk_analytics_fields(): + base_slice["fields"] = ",".join(fields_set) + date_slice_with_fields.append(base_slice | date_slice) + yield date_slice_with_fields + + @staticmethod + def get_date_slices(start_date: str, end_date: str = None, window_in_days: int = WINDOW_IN_DAYS) -> Iterable[Mapping[str, Any]]: + """ + Produces date slices from start_date to end_date (if specified), + otherwise end_date will be present time. + """ + start = pendulum.parse(start_date) + end = pendulum.parse(end_date) if end_date else pendulum.now() + date_slices = [] + while start < end: + slice_end_date = start.add(days=window_in_days) + date_slice = { + "start.day": start.day, + "start.month": start.month, + "start.year": start.year, + "end.day": slice_end_date.day, + "end.month": slice_end_date.month, + "end.year": slice_end_date.year, + } + date_slices.append({"dateRange": date_slice}) + start = slice_end_date + yield from date_slices + + @staticmethod + def chunk_analytics_fields( + fields: List = ANALYTICS_FIELDS_V2, + fields_chunk_size: int = FIELDS_CHUNK_SIZE, + ) -> Iterable[List]: + """ + Chunks the list of available fields into the chunks of equal size. + """ + # Make chunks + chunks = list((fields[f : f + fields_chunk_size] for f in range(0, len(fields), fields_chunk_size))) + # Make sure base_fields are within the chunks + for chunk in chunks: + if "dateRange" not in chunk: + chunk.append("dateRange") + yield from chunks + + def read_records( + self, stream_state: Mapping[str, Any] = None, stream_slice: Optional[Mapping[str, Any]] = None, **kwargs + ) -> Iterable[Mapping[str, Any]]: + merged_records = defaultdict(dict) + for field_slice in stream_slice: + for rec in super().read_records(stream_slice=field_slice, **kwargs): + merged_records[rec[self.cursor_field]].update(rec) + yield from merged_records.values() + + def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: + """ + We need to get out the nested complex data structures for further normalization, so the transform_data method is applied. + """ + for rec in transform_data(response.json().get("elements")): + yield rec | {"pivotValue": f"urn:li:{self.search_param_value}:{self.get_primary_key_from_slice(kwargs.get('stream_slice'))}"} + + +class AdCampaignAnalytics(LinkedInAdsAnalyticsStream): + """ + Campaign Analytics stream. + """ + + endpoint = "adAnalytics" + + parent_stream = Campaigns + parent_values_map = {"campaign_id": "id"} + search_param = "campaigns" + search_param_value = "sponsoredCampaign" + pivot_by = "CAMPAIGN" + time_granularity = "DAILY" + + +class AdCreativeAnalytics(LinkedInAdsAnalyticsStream): + """ + Creative Analytics stream. + """ + + parent_stream = Creatives + parent_values_map = {"creative_id": "id"} + search_param = "creatives" + search_param_value = "sponsoredCreative" + pivot_by = "CREATIVE" + time_granularity = "DAILY" + + def get_primary_key_from_slice(self, stream_slice) -> str: + creative_id = stream_slice.get(self.primary_slice_key).split(":")[-1] + return creative_id + + +class AdImpressionDeviceAnalytics(AdCampaignAnalytics): + pivot_by = "IMPRESSION_DEVICE_TYPE" + + +class AdMemberCompanySizeAnalytics(AdCampaignAnalytics): + pivot_by = "MEMBER_COMPANY_SIZE" + + +class AdMemberIndustryAnalytics(AdCampaignAnalytics): + pivot_by = "MEMBER_INDUSTRY" + + +class AdMemberSeniorityAnalytics(AdCampaignAnalytics): + pivot_by = "MEMBER_SENIORITY" + + +class AdMemberJobTitleAnalytics(AdCampaignAnalytics): + pivot_by = "MEMBER_JOB_TITLE" + + +class AdMemberJobFunctionAnalytics(AdCampaignAnalytics): + pivot_by = "MEMBER_JOB_FUNCTION" + + +class AdMemberCountryAnalytics(AdCampaignAnalytics): + pivot_by = "MEMBER_COUNTRY_V2" + + +class AdMemberRegionAnalytics(AdCampaignAnalytics): + pivot_by = "MEMBER_REGION_V2" + + +class AdMemberCompanyAnalytics(AdCampaignAnalytics): + pivot_by = "MEMBER_COMPANY" diff --git a/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/source.py b/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/source.py index 8a89a468eaabb..4716fe2450939 100644 --- a/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/source.py +++ b/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/source.py @@ -11,9 +11,7 @@ from airbyte_cdk.sources.streams.http.requests_native_auth import Oauth2Authenticator, TokenAuthenticator from airbyte_cdk.utils import AirbyteTracedException from airbyte_protocol.models import FailureType -from source_linkedin_ads.streams import ( - Accounts, - AccountUsers, +from source_linkedin_ads.analytics_streams import ( AdCampaignAnalytics, AdCreativeAnalytics, AdImpressionDeviceAnalytics, @@ -25,11 +23,8 @@ AdMemberJobTitleAnalytics, AdMemberRegionAnalytics, AdMemberSeniorityAnalytics, - CampaignGroups, - Campaigns, - Conversions, - Creatives, ) +from source_linkedin_ads.streams import Accounts, AccountUsers, CampaignGroups, Campaigns, Conversions, Creatives logger = logging.getLogger("airbyte") diff --git a/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/streams.py b/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/streams.py index ca13c1856b350..5151d52a961d4 100644 --- a/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/streams.py +++ b/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/streams.py @@ -10,13 +10,9 @@ import pendulum import requests -from airbyte_cdk.sources.streams.core import package_name_from_class from airbyte_cdk.sources.streams.http import HttpStream -from airbyte_cdk.sources.utils import casing -from airbyte_cdk.sources.utils.schema_helpers import ResourceSchemaLoader from airbyte_cdk.sources.utils.transform import TransformConfig, TypeTransformer -from .analytics import make_analytics_slices, merge_chunks, update_analytics_params from .utils import get_parent_stream_values, transform_data logger = logging.getLogger("airbyte") @@ -32,7 +28,6 @@ class LinkedinAdsStream(HttpStream, ABC): url_base = "https://api.linkedin.com/rest/" primary_key = "id" records_limit = 500 - endpoint = None transformer = TypeTransformer(TransformConfig.DefaultSchemaNormalization) def __init__(self, config: Dict): @@ -52,6 +47,11 @@ def accounts(self): """Property to return the list of the user Account Ids from input""" return ",".join(map(str, self.config.get("account_ids", []))) + @property + @abstractmethod + def endpoint(self) -> str: + """Endpoint associated with the current stream""" + def path( self, *, @@ -92,7 +92,7 @@ def request_params( def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: """ - We need to get out the nested complex data structures for further normalisation, so the transform_data method is applied. + We need to get out the nested complex data structures for further normalization, so the transform_data method is applied. """ for record in transform_data(response.json().get("elements")): yield self._date_time_to_rfc3339(record) @@ -126,6 +126,7 @@ class Accounts(LinkedinAdsStream): """ endpoint = "adAccounts" + use_cache = True def request_headers(self, stream_state: Mapping[str, Any], **kwargs) -> Mapping[str, Any]: """ @@ -169,12 +170,12 @@ def primary_slice_key(self) -> str: @property @abstractmethod - def parent_stream(self) -> object: - """Defines the parrent stream for slicing, the class object should be provided.""" + def parent_stream(self) -> LinkedinAdsStream: + """Defines the parent stream for slicing, the class object should be provided.""" @property def state_checkpoint_interval(self) -> Optional[int]: - """Define the checkpoint from the records output size.""" + """Define the checkpoint from the record output size.""" return 100 def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]) -> Mapping[str, Any]: @@ -182,11 +183,11 @@ def get_updated_state(self, current_stream_state: MutableMapping[str, Any], late return {self.cursor_field: max(latest_record.get(self.cursor_field), current_stream_state.get(self.cursor_field))} -class LinkedInAdsStreamSlicing(IncrementalLinkedinAdsStream): +class LinkedInAdsStreamSlicing(IncrementalLinkedinAdsStream, ABC): """ This class stands for provide stream slicing for other dependent streams. :: `parent_stream` - the reference to the parent stream class, - by default it's referenced to the Accounts stream class, as far as majority of streams are using it. + by default it's referenced to the Accounts stream class, as far as a majority of streams are using it. :: `parent_values_map` - key_value map for stream slices in a format: {: } :: `search_param` - the query param to pass with request_params """ @@ -315,7 +316,7 @@ class Creatives(LinkedInAdsStreamSlicing): endpoint = "creatives" parent_stream = Accounts cursor_field = "lastModifiedAt" - # standard records_limit=500 returns error 400: Request would return too many entities; https://github.com/airbytehq/oncall/issues/2159 + # standard records_limit=500 returns error 400: Request would return too many entities; https://github.com/airbytehq/oncall/issues/2159 records_limit = 100 def path( @@ -388,161 +389,3 @@ def get_updated_state(self, current_stream_state: MutableMapping[str, Any], late else current_stream_state ) return {self.cursor_field: max(latest_record.get(self.cursor_field), int(current_stream_state.get(self.cursor_field)))} - - -class LinkedInAdsAnalyticsStream(IncrementalLinkedinAdsStream, ABC): - """ - AdAnalytics Streams more info: - https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/ads-reporting?tabs=curl&view=li-lms-2023-05#analytics-finder - """ - - endpoint = "adAnalytics" - # For Analytics streams the primary_key is the entity of the pivot [Campaign URN, Creative URN, etc] + `end_date` - primary_key = ["pivotValue", "end_date"] - cursor_field = "end_date" - records_limit = 15000 - - def get_json_schema(self) -> Mapping[str, Any]: - return ResourceSchemaLoader(package_name_from_class(self.__class__)).get_schema("ad_analytics") - - def __init__(self, name: str = None, pivot_by: str = None, time_granularity: str = None, **kwargs): - self.user_stream_name = name - if pivot_by: - self.pivot_by = pivot_by - if time_granularity: - self.time_granularity = time_granularity - super().__init__(**kwargs) - - @property - def name(self) -> str: - """We override stream name to let the user change it via configuration.""" - name = self.user_stream_name or self.__class__.__name__ - return casing.camel_to_snake(name) - - @property - def base_analytics_params(self) -> MutableMapping[str, Any]: - """Define the base parameters for analytics streams""" - return {"q": "analytics", "pivot": f"(value:{self.pivot_by})", "timeGranularity": f"(value:{self.time_granularity})"} - - def request_headers( - self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None - ) -> Mapping[str, Any]: - headers = super().request_headers(stream_state, stream_slice, next_page_token) - return headers | {"X-Restli-Protocol-Version": "2.0.0"} - - def request_params( - self, - stream_state: Mapping[str, Any], - stream_slice: Mapping[str, Any] = None, - next_page_token: Mapping[str, Any] = None, - ) -> MutableMapping[str, Any]: - params = self.base_analytics_params - params.update(**update_analytics_params(stream_slice)) - params[self.search_param] = f"List(urn%3Ali%3A{self.search_param_value}%3A{self.get_primary_key_from_slice(stream_slice)})" - return urlencode(params, safe="():,%") - - def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - """ - Pagination is not supported - (See Restrictions: https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/ads-reporting?view=li-lms-2023-09&tabs=http#restrictions) - """ - parsed_response = response.json() - if len(parsed_response.get("elements")) < self.records_limit: - return None - raise Exception( - f"Limit {self.records_limit} elements exceeded. " - f"Try to request your data in more granular pieces. " - f"(For example switch `Time Granularity` from MONTHLY to DAILY)" - ) - - def get_primary_key_from_slice(self, stream_slice) -> str: - return stream_slice.get(self.primary_slice_key) - - def read_records( - self, stream_state: Mapping[str, Any] = None, stream_slice: Optional[Mapping[str, Any]] = None, **kwargs - ) -> Iterable[Mapping[str, Any]]: - stream_state = stream_state or {self.cursor_field: self.config.get("start_date")} - parent_stream = self.parent_stream(config=self.config) - for record in parent_stream.read_records(**kwargs): - result_chunks = [] - for analytics_slice in make_analytics_slices( - record, self.parent_values_map, stream_state.get(self.cursor_field), self.config.get("end_date") - ): - child_stream_slice = super().read_records(stream_slice=analytics_slice, **kwargs) - result_chunks.append(child_stream_slice) - yield from merge_chunks(result_chunks, self.cursor_field) - - def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - """ - We need to get out the nested complex data structures for further normalisation, so the transform_data method is applied. - """ - for rec in transform_data(response.json().get("elements")): - yield rec | {"pivotValue": f"urn:li:{self.search_param_value}:{self.get_primary_key_from_slice(kwargs.get('stream_slice'))}"} - - -class AdCampaignAnalytics(LinkedInAdsAnalyticsStream): - """ - Campaign Analytics stream. - """ - - endpoint = "adAnalytics" - - parent_stream = Campaigns - parent_values_map = {"campaign_id": "id"} - search_param = "campaigns" - search_param_value = "sponsoredCampaign" - pivot_by = "CAMPAIGN" - time_granularity = "DAILY" - - -class AdCreativeAnalytics(LinkedInAdsAnalyticsStream): - """ - Creative Analytics stream. - """ - - parent_stream = Creatives - parent_values_map = {"creative_id": "id"} - search_param = "creatives" - search_param_value = "sponsoredCreative" - pivot_by = "CREATIVE" - time_granularity = "DAILY" - - def get_primary_key_from_slice(self, stream_slice) -> str: - creative_id = stream_slice.get(self.primary_slice_key).split(":")[-1] - return creative_id - - -class AdImpressionDeviceAnalytics(AdCampaignAnalytics): - pivot_by = "IMPRESSION_DEVICE_TYPE" - - -class AdMemberCompanySizeAnalytics(AdCampaignAnalytics): - pivot_by = "MEMBER_COMPANY_SIZE" - - -class AdMemberIndustryAnalytics(AdCampaignAnalytics): - pivot_by = "MEMBER_INDUSTRY" - - -class AdMemberSeniorityAnalytics(AdCampaignAnalytics): - pivot_by = "MEMBER_SENIORITY" - - -class AdMemberJobTitleAnalytics(AdCampaignAnalytics): - pivot_by = "MEMBER_JOB_TITLE" - - -class AdMemberJobFunctionAnalytics(AdCampaignAnalytics): - pivot_by = "MEMBER_JOB_FUNCTION" - - -class AdMemberCountryAnalytics(AdCampaignAnalytics): - pivot_by = "MEMBER_COUNTRY_V2" - - -class AdMemberRegionAnalytics(AdCampaignAnalytics): - pivot_by = "MEMBER_REGION_V2" - - -class AdMemberCompanyAnalytics(AdCampaignAnalytics): - pivot_by = "MEMBER_COMPANY" diff --git a/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/utils.py b/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/utils.py index f000f88f343a4..9872ea0055b77 100644 --- a/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/utils.py +++ b/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/utils.py @@ -14,18 +14,11 @@ DESTINATION_RESERVED_KEYWORDS: list = ["pivot"] -def get_parent_stream_values(record: Dict, key_value_map: Dict) -> Dict: +def get_parent_stream_values(record: Mapping[str, Any], key_value_map: Mapping[str, str]) -> Mapping[str, Any]: """ - Outputs the Dict with key:value slices for the stream. - :: EXAMPLE: - Input: - records = [{dict}, {dict}, ...], - key_value_map = {: } - - Output: - { - : records..value, - } + :param record: Mapping[str, Any] + :param key_value_map: Mapping[str, str] {: } + :return: Mapping[str, str] { : records..value} """ result = {} for key in key_value_map: diff --git a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/__init__.py b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/samples/test_data_for_analytics.py b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/samples/test_data_for_analytics.py deleted file mode 100644 index 2b4f603356f41..0000000000000 --- a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/samples/test_data_for_analytics.py +++ /dev/null @@ -1,178 +0,0 @@ -# -# Copyright (c) 2023 Airbyte, Inc., all rights reserved. -# - -from typing import Dict, List - -""" -This is the example of input record for the test_make_analytics_slices. -""" -test_input_record: Dict = { - "id": 123, - "audienceExpansionEnabled": True, - "test": False, - "format": "STANDARD_UPDATE", - "servingStatuses": ["CAMPAIGN_GROUP_TOTAL_BUDGET_HOLD"], - "version": {"versionTag": "2"}, - "objectiveType": "TEST_TEST", - "associatedEntity": "urn:li:organization:456", - "offsitePreferences": { - "iabCategories": {"exclude": []}, - "publisherRestrictionFiles": {"exclude": []}, - }, - "campaignGroup": "urn:li:sponsoredCampaignGroup:1234567", - "account": "urn:li:sponsoredAccount:123456", - "status": "ACTIVE", - "created": "2021-08-06 06:03:52", - "lastModified": "2021-08-06 06:09:04", -} - -""" -This is the expected output from the `make_analytics_slices` method. -VALID PARAMETERS FOR THE OUTPUT ARE: -: TEST_KEY_VALUE_MAP = {"campaign_id": "id"} -: TEST_START_DATE = "2021-08-01" -: TEST_END_DATE = "2021-09-30" - -Change the input parameters inside of test_make_analytics_slices.py unit test. -Make sure for valid KEY_VALUE_MAP references inside of the `test_input_record` -""" -test_output_slices: List = [ - { - "camp_id": 123, - "fields": "actionClicks,adUnitClicks,approximateUniqueImpressions,cardClicks,cardImpressions,clicks,commentLikes,comments,companyPageClicks,conversionValueInLocalCurrency,costInLocalCurrency,costInUsd,dateRange,documentCompletions,documentFirstQuartileCompletions,documentMidpointCompletions,documentThirdQuartileCompletions,downloadClicks,externalWebsiteConversions", - "dateRange": { - "start.day": 1, - "start.month": 8, - "start.year": 2021, - "end.day": 31, - "end.month": 8, - "end.year": 2021, - }, - }, - { - "camp_id": 123, - "fields": "actionClicks,adUnitClicks,approximateUniqueImpressions,cardClicks,cardImpressions,clicks,commentLikes,comments,companyPageClicks,conversionValueInLocalCurrency,costInLocalCurrency,costInUsd,dateRange,documentCompletions,documentFirstQuartileCompletions,documentMidpointCompletions,documentThirdQuartileCompletions,downloadClicks,externalWebsiteConversions", - "dateRange": {"start.day": 31, "start.month": 8, "start.year": 2021, "end.day": 30, "end.month": 9, "end.year": 2021}, - }, - { - "camp_id": 123, - "fields": "externalWebsitePostClickConversions,externalWebsitePostViewConversions,follows,fullScreenPlays,impressions,jobApplications,jobApplyClicks,landingPageClicks,leadGenerationMailContactInfoShares,leadGenerationMailInterestedClicks,likes,oneClickLeadFormOpens,oneClickLeads,opens,otherEngagements,pivotValues,postClickJobApplications,postClickJobApplyClicks,postClickRegistrations,dateRange", - "dateRange": {"start.day": 1, "start.month": 8, "start.year": 2021, "end.day": 31, "end.month": 8, "end.year": 2021}, - }, - { - "camp_id": 123, - "fields": "externalWebsitePostClickConversions,externalWebsitePostViewConversions,follows,fullScreenPlays,impressions,jobApplications,jobApplyClicks,landingPageClicks,leadGenerationMailContactInfoShares,leadGenerationMailInterestedClicks,likes,oneClickLeadFormOpens,oneClickLeads,opens,otherEngagements,pivotValues,postClickJobApplications,postClickJobApplyClicks,postClickRegistrations,dateRange", - "dateRange": {"start.day": 31, "start.month": 8, "start.year": 2021, "end.day": 30, "end.month": 9, "end.year": 2021}, - }, - { - "camp_id": 123, - "fields": "postViewJobApplications,postViewJobApplyClicks,postViewRegistrations,reactions,registrations,sends,shares,talentLeads,textUrlClicks,totalEngagements,validWorkEmailLeads,videoCompletions,videoFirstQuartileCompletions,videoMidpointCompletions,videoStarts,videoThirdQuartileCompletions,videoViews,viralCardClicks,viralCardImpressions,dateRange", - "dateRange": {"start.day": 1, "start.month": 8, "start.year": 2021, "end.day": 31, "end.month": 8, "end.year": 2021}, - }, - { - "camp_id": 123, - "fields": "postViewJobApplications,postViewJobApplyClicks,postViewRegistrations,reactions,registrations,sends,shares,talentLeads,textUrlClicks,totalEngagements,validWorkEmailLeads,videoCompletions,videoFirstQuartileCompletions,videoMidpointCompletions,videoStarts,videoThirdQuartileCompletions,videoViews,viralCardClicks,viralCardImpressions,dateRange", - "dateRange": {"start.day": 31, "start.month": 8, "start.year": 2021, "end.day": 30, "end.month": 9, "end.year": 2021}, - }, - { - "camp_id": 123, - "fields": "viralClicks,viralCommentLikes,viralComments,viralCompanyPageClicks,viralDocumentCompletions,viralDocumentFirstQuartileCompletions,viralDocumentMidpointCompletions,viralDocumentThirdQuartileCompletions,viralDownloadClicks,viralExternalWebsiteConversions,viralExternalWebsitePostClickConversions,viralExternalWebsitePostViewConversions,viralFollows,viralFullScreenPlays,viralImpressions,viralJobApplications,viralJobApplyClicks,viralLandingPageClicks,viralLikes,dateRange", - "dateRange": {"start.day": 1, "start.month": 8, "start.year": 2021, "end.day": 31, "end.month": 8, "end.year": 2021}, - }, - { - "camp_id": 123, - "fields": "viralClicks,viralCommentLikes,viralComments,viralCompanyPageClicks,viralDocumentCompletions,viralDocumentFirstQuartileCompletions,viralDocumentMidpointCompletions,viralDocumentThirdQuartileCompletions,viralDownloadClicks,viralExternalWebsiteConversions,viralExternalWebsitePostClickConversions,viralExternalWebsitePostViewConversions,viralFollows,viralFullScreenPlays,viralImpressions,viralJobApplications,viralJobApplyClicks,viralLandingPageClicks,viralLikes,dateRange", - "dateRange": {"start.day": 31, "start.month": 8, "start.year": 2021, "end.day": 30, "end.month": 9, "end.year": 2021}, - }, - { - "camp_id": 123, - "fields": "viralOneClickLeadFormOpens,viralOneClickLeads,viralOtherEngagements,viralPostClickJobApplications,viralPostClickJobApplyClicks,viralPostClickRegistrations,viralPostViewJobApplications,viralPostViewJobApplyClicks,viralPostViewRegistrations,viralReactions,viralRegistrations,viralShares,viralTotalEngagements,viralVideoCompletions,viralVideoFirstQuartileCompletions,viralVideoMidpointCompletions,viralVideoStarts,viralVideoThirdQuartileCompletions,viralVideoViews,dateRange", - "dateRange": {"start.day": 1, "start.month": 8, "start.year": 2021, "end.day": 31, "end.month": 8, "end.year": 2021}, - }, - { - "camp_id": 123, - "fields": "viralOneClickLeadFormOpens,viralOneClickLeads,viralOtherEngagements,viralPostClickJobApplications,viralPostClickJobApplyClicks,viralPostClickRegistrations,viralPostViewJobApplications,viralPostViewJobApplyClicks,viralPostViewRegistrations,viralReactions,viralRegistrations,viralShares,viralTotalEngagements,viralVideoCompletions,viralVideoFirstQuartileCompletions,viralVideoMidpointCompletions,viralVideoStarts,viralVideoThirdQuartileCompletions,viralVideoViews,dateRange", - "dateRange": {"start.day": 31, "start.month": 8, "start.year": 2021, "end.day": 30, "end.month": 9, "end.year": 2021}, - }, -] - -""" This is the example of the input chunks for the `test_merge_chunks` """ -test_input_result_record_chunks = [ - [ - { - "field_1": "test1", - "start_date": "2021-08-06", - "end_date": "2021-08-06", - }, - { - "field_1": "test2", - "start_date": "2021-08-07", - "end_date": "2021-08-07", - }, - { - "field_1": "test3", - "start_date": "2021-08-08", - "end_date": "2021-08-08", - }, - ], - [ - { - "field_2": "test1", - "start_date": "2021-08-06", - "end_date": "2021-08-06", - }, - { - "field_2": "test2", - "start_date": "2021-08-07", - "end_date": "2021-08-07", - }, - { - "field_2": "test3", - "start_date": "2021-08-08", - "end_date": "2021-08-08", - }, - ], - [ - { - "field_3": "test1", - "start_date": "2021-08-06", - "end_date": "2021-08-06", - }, - { - "field_3": "test2", - "start_date": "2021-08-07", - "end_date": "2021-08-07", - }, - { - "field_3": "test3", - "start_date": "2021-08-08", - "end_date": "2021-08-08", - }, - ], -] - -""" This is the expected test ouptput from the `merge_chunks` method from analytics module """ -test_output_merged_chunks = [ - { - "field_1": "test1", - "start_date": "2021-08-06", - "end_date": "2021-08-06", - "field_2": "test1", - "field_3": "test1", - }, - { - "field_1": "test2", - "start_date": "2021-08-07", - "end_date": "2021-08-07", - "field_2": "test2", - "field_3": "test2", - }, - { - "field_1": "test3", - "start_date": "2021-08-08", - "end_date": "2021-08-08", - "field_2": "test3", - "field_3": "test3", - }, -] diff --git a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_chunk_analytics_fields.py b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_chunk_analytics_fields.py deleted file mode 100644 index c360c249159f0..0000000000000 --- a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_chunk_analytics_fields.py +++ /dev/null @@ -1,39 +0,0 @@ -# -# Copyright (c) 2023 Airbyte, Inc., all rights reserved. -# - -from source_linkedin_ads.analytics import chunk_analytics_fields - -# Test chunk size for each field set -TEST_FIELDS_CHUNK_SIZE = 3 -# Test fields assuming they are really available for the fetch -TEST_ANALYTICS_FIELDS = [ - "field_1", - "base_field_1", - "field_2", - "base_field_2", - "field_3", - "field_4", - "field_5", - "field_6", - "field_7", - "field_8", -] -# Fields that are always present in fields_set chunks -TEST_BASE_ANALLYTICS_FIELDS = ["base_field_1", "base_field_2"] - - -def test_chunk_analytics_fields(): - """ - We expect to truncate the fields list into the chunks of equal size, - with TEST_BASE_ANALLYTICS_FIELDS presence in each chunk, - order is not matter. - """ - expected_output = [ - ["field_1", "base_field_1", "field_2", "base_field_2"], - ["base_field_2", "field_3", "field_4", "base_field_1"], - ["field_5", "field_6", "field_7", "base_field_1", "base_field_2"], - ["field_8", "base_field_1", "base_field_2"], - ] - - assert list(chunk_analytics_fields(TEST_ANALYTICS_FIELDS, TEST_BASE_ANALLYTICS_FIELDS, TEST_FIELDS_CHUNK_SIZE)) == expected_output diff --git a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_make_analytics_slices.py b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_make_analytics_slices.py deleted file mode 100644 index f2579852d87cf..0000000000000 --- a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_make_analytics_slices.py +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright (c) 2023 Airbyte, Inc., all rights reserved. -# - -from samples.test_data_for_analytics import test_input_record, test_output_slices -from source_linkedin_ads.analytics import make_analytics_slices - -# Test input arguments for the `make_analytics_slices` -TEST_KEY_VALUE_MAP = {"camp_id": "id"} -TEST_START_DATE = "2021-08-01" -TEST_END_DATE = "2021-09-30" - -# This is the mock of the request_params -TEST_REQUEST_PRAMS = {} - - -def test_make_analytics_slices(): - assert list(make_analytics_slices(test_input_record, TEST_KEY_VALUE_MAP, TEST_START_DATE, TEST_END_DATE)) == test_output_slices diff --git a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_make_date_slices.py b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_make_date_slices.py deleted file mode 100644 index ec5e5c9d5d853..0000000000000 --- a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_make_date_slices.py +++ /dev/null @@ -1,24 +0,0 @@ -# -# Copyright (c) 2023 Airbyte, Inc., all rights reserved. -# - -from source_linkedin_ads.analytics import make_date_slices - -TEST_START_DATE = "2021-08-01" -TEST_END_DATE = "2021-10-01" - - -def test_make_date_slices(): - """ - : By default we use the `WINDOW_SIZE = 30`, as it set in the analytics module - : This value could be changed by setting the corresponding argument in the method. - : The `end_date` is not specified by default, but for this test it was specified to have the test static. - """ - - expected_output = [ - {"dateRange": {"start.day": 1, "start.month": 8, "start.year": 2021, "end.day": 31, "end.month": 8, "end.year": 2021}}, - {"dateRange": {"start.day": 31, "start.month": 8, "start.year": 2021, "end.day": 30, "end.month": 9, "end.year": 2021}}, - {"dateRange": {"start.day": 30, "start.month": 9, "start.year": 2021, "end.day": 30, "end.month": 10, "end.year": 2021}}, - ] - - assert list(make_date_slices(TEST_START_DATE, TEST_END_DATE)) == expected_output diff --git a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_merge_chunks.py b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_merge_chunks.py deleted file mode 100644 index 65036b99d06a5..0000000000000 --- a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/analytics_tests/test_merge_chunks.py +++ /dev/null @@ -1,13 +0,0 @@ -# -# Copyright (c) 2023 Airbyte, Inc., all rights reserved. -# - -from samples.test_data_for_analytics import test_input_result_record_chunks, test_output_merged_chunks -from source_linkedin_ads.analytics import merge_chunks - -TEST_MERGE_BY_KEY = "end_date" - - -def test_merge_chunks(): - """`merge_chunks` is the generator object, to get the output the list() function is applied""" - assert list(merge_chunks(test_input_result_record_chunks, TEST_MERGE_BY_KEY)) == test_output_merged_chunks diff --git a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/output_slices.json b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/output_slices.json new file mode 100644 index 0000000000000..edab652341716 --- /dev/null +++ b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/output_slices.json @@ -0,0 +1,126 @@ +[ + [ + { + "campaign_id": 123, + "dateRange": { + "end.day": 31, + "end.month": 1, + "end.year": 2021, + "start.day": 1, + "start.month": 1, + "start.year": 2021 + }, + "fields": "actionClicks,adUnitClicks,approximateUniqueImpressions,cardClicks,cardImpressions,clicks,commentLikes,comments,companyPageClicks,conversionValueInLocalCurrency,costInLocalCurrency,costInUsd,dateRange,documentCompletions,documentFirstQuartileCompletions,documentMidpointCompletions,documentThirdQuartileCompletions,downloadClicks,externalWebsiteConversions" + }, + { + "campaign_id": 123, + "dateRange": { + "end.day": 31, + "end.month": 1, + "end.year": 2021, + "start.day": 1, + "start.month": 1, + "start.year": 2021 + }, + "fields": "externalWebsitePostClickConversions,externalWebsitePostViewConversions,follows,fullScreenPlays,impressions,jobApplications,jobApplyClicks,landingPageClicks,leadGenerationMailContactInfoShares,leadGenerationMailInterestedClicks,likes,oneClickLeadFormOpens,oneClickLeads,opens,otherEngagements,pivotValues,postClickJobApplications,postClickJobApplyClicks,postClickRegistrations,dateRange" + }, + { + "campaign_id": 123, + "dateRange": { + "end.day": 31, + "end.month": 1, + "end.year": 2021, + "start.day": 1, + "start.month": 1, + "start.year": 2021 + }, + "fields": "postViewJobApplications,postViewJobApplyClicks,postViewRegistrations,reactions,registrations,sends,shares,talentLeads,textUrlClicks,totalEngagements,validWorkEmailLeads,videoCompletions,videoFirstQuartileCompletions,videoMidpointCompletions,videoStarts,videoThirdQuartileCompletions,videoViews,viralCardClicks,viralCardImpressions,dateRange" + }, + { + "campaign_id": 123, + "dateRange": { + "end.day": 31, + "end.month": 1, + "end.year": 2021, + "start.day": 1, + "start.month": 1, + "start.year": 2021 + }, + "fields": "viralClicks,viralCommentLikes,viralComments,viralCompanyPageClicks,viralDocumentCompletions,viralDocumentFirstQuartileCompletions,viralDocumentMidpointCompletions,viralDocumentThirdQuartileCompletions,viralDownloadClicks,viralExternalWebsiteConversions,viralExternalWebsitePostClickConversions,viralExternalWebsitePostViewConversions,viralFollows,viralFullScreenPlays,viralImpressions,viralJobApplications,viralJobApplyClicks,viralLandingPageClicks,viralLikes,dateRange" + }, + { + "campaign_id": 123, + "dateRange": { + "end.day": 31, + "end.month": 1, + "end.year": 2021, + "start.day": 1, + "start.month": 1, + "start.year": 2021 + }, + "fields": "viralOneClickLeadFormOpens,viralOneClickLeads,viralOtherEngagements,viralPostClickJobApplications,viralPostClickJobApplyClicks,viralPostClickRegistrations,viralPostViewJobApplications,viralPostViewJobApplyClicks,viralPostViewRegistrations,viralReactions,viralRegistrations,viralShares,viralTotalEngagements,viralVideoCompletions,viralVideoFirstQuartileCompletions,viralVideoMidpointCompletions,viralVideoStarts,viralVideoThirdQuartileCompletions,viralVideoViews,dateRange" + } + ], + [ + { + "campaign_id": 123, + "dateRange": { + "end.day": 2, + "end.month": 3, + "end.year": 2021, + "start.day": 31, + "start.month": 1, + "start.year": 2021 + }, + "fields": "actionClicks,adUnitClicks,approximateUniqueImpressions,cardClicks,cardImpressions,clicks,commentLikes,comments,companyPageClicks,conversionValueInLocalCurrency,costInLocalCurrency,costInUsd,dateRange,documentCompletions,documentFirstQuartileCompletions,documentMidpointCompletions,documentThirdQuartileCompletions,downloadClicks,externalWebsiteConversions" + }, + { + "campaign_id": 123, + "dateRange": { + "end.day": 2, + "end.month": 3, + "end.year": 2021, + "start.day": 31, + "start.month": 1, + "start.year": 2021 + }, + "fields": "externalWebsitePostClickConversions,externalWebsitePostViewConversions,follows,fullScreenPlays,impressions,jobApplications,jobApplyClicks,landingPageClicks,leadGenerationMailContactInfoShares,leadGenerationMailInterestedClicks,likes,oneClickLeadFormOpens,oneClickLeads,opens,otherEngagements,pivotValues,postClickJobApplications,postClickJobApplyClicks,postClickRegistrations,dateRange" + }, + { + "campaign_id": 123, + "dateRange": { + "end.day": 2, + "end.month": 3, + "end.year": 2021, + "start.day": 31, + "start.month": 1, + "start.year": 2021 + }, + "fields": "postViewJobApplications,postViewJobApplyClicks,postViewRegistrations,reactions,registrations,sends,shares,talentLeads,textUrlClicks,totalEngagements,validWorkEmailLeads,videoCompletions,videoFirstQuartileCompletions,videoMidpointCompletions,videoStarts,videoThirdQuartileCompletions,videoViews,viralCardClicks,viralCardImpressions,dateRange" + }, + { + "campaign_id": 123, + "dateRange": { + "end.day": 2, + "end.month": 3, + "end.year": 2021, + "start.day": 31, + "start.month": 1, + "start.year": 2021 + }, + "fields": "viralClicks,viralCommentLikes,viralComments,viralCompanyPageClicks,viralDocumentCompletions,viralDocumentFirstQuartileCompletions,viralDocumentMidpointCompletions,viralDocumentThirdQuartileCompletions,viralDownloadClicks,viralExternalWebsiteConversions,viralExternalWebsitePostClickConversions,viralExternalWebsitePostViewConversions,viralFollows,viralFullScreenPlays,viralImpressions,viralJobApplications,viralJobApplyClicks,viralLandingPageClicks,viralLikes,dateRange" + }, + { + "campaign_id": 123, + "dateRange": { + "end.day": 2, + "end.month": 3, + "end.year": 2021, + "start.day": 31, + "start.month": 1, + "start.year": 2021 + }, + "fields": "viralOneClickLeadFormOpens,viralOneClickLeads,viralOtherEngagements,viralPostClickJobApplications,viralPostClickJobApplyClicks,viralPostClickRegistrations,viralPostViewJobApplications,viralPostViewJobApplyClicks,viralPostViewRegistrations,viralReactions,viralRegistrations,viralShares,viralTotalEngagements,viralVideoCompletions,viralVideoFirstQuartileCompletions,viralVideoMidpointCompletions,viralVideoStarts,viralVideoThirdQuartileCompletions,viralVideoViews,dateRange" + } + ] +] diff --git a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/responses/ad_member_country_analytics/response_1.json b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/responses/ad_member_country_analytics/response_1.json new file mode 100644 index 0000000000000..a68474329ce06 --- /dev/null +++ b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/responses/ad_member_country_analytics/response_1.json @@ -0,0 +1,71 @@ +{ + "paging": { + "start": 0, + "count": 10, + "links": [] + }, + "elements": [ + { + "documentFirstQuartileCompletions": 0, + "actionClicks": 0, + "comments": 0, + "costInUsd": "-2E-18", + "dateRange": { + "start": { + "month": 1, + "day": 2, + "year": 2023 + }, + "end": { + "month": 1, + "day": 2, + "year": 2023 + } + }, + "commentLikes": 0, + "adUnitClicks": 0, + "companyPageClicks": 0, + "costInLocalCurrency": "-2E-18", + "documentThirdQuartileCompletions": 0, + "externalWebsiteConversions": 0, + "cardImpressions": 0, + "documentCompletions": 0, + "clicks": 0, + "cardClicks": 0, + "approximateUniqueImpressions": 0, + "documentMidpointCompletions": 0, + "downloadClicks": 0 + }, + { + "documentFirstQuartileCompletions": 0, + "actionClicks": 0, + "comments": 0, + "costInUsd": "100", + "dateRange": { + "start": { + "month": 1, + "day": 2, + "year": 2023 + }, + "end": { + "month": 1, + "day": 2, + "year": 2023 + } + }, + "commentLikes": 0, + "adUnitClicks": 0, + "companyPageClicks": 0, + "costInLocalCurrency": "100", + "documentThirdQuartileCompletions": 0, + "externalWebsiteConversions": 0, + "cardImpressions": 0, + "documentCompletions": 0, + "clicks": 106, + "cardClicks": 0, + "approximateUniqueImpressions": 17392, + "documentMidpointCompletions": 0, + "downloadClicks": 0 + } + ] +} diff --git a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/responses/ad_member_country_analytics/response_2.json b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/responses/ad_member_country_analytics/response_2.json new file mode 100644 index 0000000000000..ac6433682403b --- /dev/null +++ b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/responses/ad_member_country_analytics/response_2.json @@ -0,0 +1,61 @@ +{ + "paging": { + "start": 0, + "count": 10, + "links": [] + }, + "elements": [ + { + "oneClickLeads": 0, + "dateRange": { + "start": { + "month": 1, + "day": 2, + "year": 2021 + }, + "end": { + "month": 1, + "day": 2, + "year": 2023 + } + }, + "landingPageClicks": 0, + "fullScreenPlays": 0, + "oneClickLeadFormOpens": 0, + "follows": 0, + "impressions": 1, + "otherEngagements": 0, + "leadGenerationMailContactInfoShares": 0, + "opens": 0, + "leadGenerationMailInterestedClicks": 0, + "pivotValues": ["urn:li:sponsoredCreative:1"], + "likes": 0 + }, + { + "oneClickLeads": 0, + "dateRange": { + "start": { + "month": 1, + "day": 1, + "year": 2021 + }, + "end": { + "month": 1, + "day": 1, + "year": 2021 + } + }, + "landingPageClicks": 106, + "fullScreenPlays": 0, + "oneClickLeadFormOpens": 0, + "follows": 0, + "impressions": 19464, + "otherEngagements": 0, + "leadGenerationMailContactInfoShares": 0, + "opens": 0, + "leadGenerationMailInterestedClicks": 0, + "pivotValues": ["urn:li:sponsoredCreative:1"], + "likes": 0 + } + ] +} diff --git a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/responses/ad_member_country_analytics/response_3.json b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/responses/ad_member_country_analytics/response_3.json new file mode 100644 index 0000000000000..5e128840ccb48 --- /dev/null +++ b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/responses/ad_member_country_analytics/response_3.json @@ -0,0 +1,37 @@ +{ + "paging": { + "start": 0, + "count": 10, + "links": [] + }, + "elements": [ + { + "videoCompletions": 0, + "dateRange": { + "start": { + "month": 1, + "day": 2, + "year": 2023 + }, + "end": { + "month": 1, + "day": 2, + "year": 2023 + } + }, + "viralCardImpressions": 0, + "videoFirstQuartileCompletions": 0, + "textUrlClicks": 0, + "videoStarts": 0, + "sends": 0, + "shares": 0, + "videoMidpointCompletions": 0, + "validWorkEmailLeads": 0, + "viralCardClicks": 0, + "videoThirdQuartileCompletions": 0, + "totalEngagements": 105, + "reactions": 0, + "videoViews": 0 + } + ] +} diff --git a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/test_analytics_streams.py b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/test_analytics_streams.py new file mode 100644 index 0000000000000..3936c7fad7e6d --- /dev/null +++ b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/test_analytics_streams.py @@ -0,0 +1,109 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# +import json +import os +from typing import Any, Mapping + +from airbyte_cdk.sources.streams.http.requests_native_auth import TokenAuthenticator +from source_linkedin_ads.analytics_streams import AdMemberCountryAnalytics, LinkedInAdsAnalyticsStream + +# Test input arguments for the `make_analytics_slices` +TEST_KEY_VALUE_MAP = {"camp_id": "id"} +TEST_START_DATE = "2021-08-01" +TEST_END_DATE = "2021-09-30" + +# This is the mock of the request_params +TEST_REQUEST_PRAMS = {} + + +TEST_CONFIG: dict = { + "start_date": "2021-01-01", + "end_date": "2021-02-01", + "account_ids": [1, 2], + "credentials": { + "auth_method": "access_token", + "access_token": "access_token", + "authenticator": TokenAuthenticator(token="123"), + }, +} + +# Test chunk size for each field set +TEST_FIELDS_CHUNK_SIZE = 3 +# Test fields assuming they are really available for the fetch +TEST_ANALYTICS_FIELDS = [ + "field_1", + "base_field_1", + "field_2", + "base_field_2", + "field_3", + "field_4", + "field_5", + "field_6", + "field_7", + "field_8", +] + + +# HELPERS +def load_json_file(file_name: str) -> Mapping[str, Any]: + with open(f"{os.path.dirname(__file__)}/{file_name}", "r") as data: + return json.load(data) + + +def test_analytics_stream_slices(requests_mock): + requests_mock.get("https://api.linkedin.com/rest/adAccounts", json={"elements": [{"id": 1}]}) + requests_mock.get("https://api.linkedin.com/rest/adAccounts/1/adCampaigns", json={"elements": [{"id": 123}]}) + assert list( + AdMemberCountryAnalytics(config=TEST_CONFIG).stream_slices( + sync_mode=None, + ) + ) == load_json_file("output_slices.json") + + +def test_read_records(requests_mock): + requests_mock.get( + "https://api.linkedin.com/rest/adAnalytics", + [ + {"json": load_json_file("responses/ad_member_country_analytics/response_1.json")}, + {"json": load_json_file("responses/ad_member_country_analytics/response_2.json")}, + {"json": load_json_file("responses/ad_member_country_analytics/response_3.json")}, + ], + ) + stream_slice = load_json_file("output_slices.json")[0] + records = list(AdMemberCountryAnalytics(config=TEST_CONFIG).read_records(stream_slice=stream_slice, sync_mode=None)) + assert len(records) == 2 + + +def test_chunk_analytics_fields(): + """ + We expect to truncate the field list into the chunks of equal size, + with "dateRange" field presented in each chunk. + """ + expected_output = [ + ["field_1", "base_field_1", "field_2", "dateRange"], + ["base_field_2", "field_3", "field_4", "dateRange"], + ["field_5", "field_6", "field_7", "dateRange"], + ["field_8", "dateRange"], + ] + + assert list(LinkedInAdsAnalyticsStream.chunk_analytics_fields(TEST_ANALYTICS_FIELDS, TEST_FIELDS_CHUNK_SIZE)) == expected_output + + +def test_get_date_slices(): + """ + By default, we use the `WINDOW_SIZE = 30`, as it set in the analytics module + This value could be changed by setting the corresponding argument in the method. + The `end_date` is not specified by default, but for this test it was specified to have the test static. + """ + + test_start_date = "2021-08-01" + test_end_date = "2021-10-01" + + expected_output = [ + {"dateRange": {"start.day": 1, "start.month": 8, "start.year": 2021, "end.day": 31, "end.month": 8, "end.year": 2021}}, + {"dateRange": {"start.day": 31, "start.month": 8, "start.year": 2021, "end.day": 30, "end.month": 9, "end.year": 2021}}, + {"dateRange": {"start.day": 30, "start.month": 9, "start.year": 2021, "end.day": 30, "end.month": 10, "end.year": 2021}}, + ] + + assert list(LinkedInAdsAnalyticsStream.get_date_slices(test_start_date, test_end_date)) == expected_output diff --git a/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/source_tests/test_source.py b/airbyte-integrations/connectors/source-linkedin-ads/unit_tests/test_source.py similarity index 100% rename from airbyte-integrations/connectors/source-linkedin-ads/unit_tests/source_tests/test_source.py rename to airbyte-integrations/connectors/source-linkedin-ads/unit_tests/test_source.py diff --git a/docs/integrations/sources/linkedin-ads.md b/docs/integrations/sources/linkedin-ads.md index d27c0914b4fea..d82b35738668f 100644 --- a/docs/integrations/sources/linkedin-ads.md +++ b/docs/integrations/sources/linkedin-ads.md @@ -171,6 +171,7 @@ After 5 unsuccessful attempts - the connector will stop the sync operation. In s | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------| +| 0.6.6 | 2024-01-15 | [34222](https://github.com/airbytehq/airbyte/pull/34222) | Use stream slices for Analytics streams | | 0.6.5 | 2023-12-15 | [33530](https://github.com/airbytehq/airbyte/pull/33530) | Fix typo in `Pivot Category` list | | 0.6.4 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | | 0.6.3 | 2023-10-13 | [31396](https://github.com/airbytehq/airbyte/pull/31396) | Fix pagination for reporting | From c4048990926dc3922e7ad686c04fad1de9c5c0df Mon Sep 17 00:00:00 2001 From: Artem Inzhyyants <36314070+artem1205@users.noreply.github.com> Date: Mon, 15 Jan 2024 11:37:41 +0100 Subject: [PATCH 088/574] =?UTF-8?q?=E2=9C=A8=20Source=20google=20analytics?= =?UTF-8?q?=20Data=20API:=20=20add=20a=20report=20option=20`keepEmptyRows`?= =?UTF-8?q?=20(#34176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../source-google-analytics-data-api/metadata.yaml | 2 +- .../source_google_analytics_data_api/source.py | 1 + .../source_google_analytics_data_api/spec.json | 7 +++++++ .../unit_tests/conftest.py | 1 + .../unit_tests/test_streams.py | 1 + docs/integrations/sources/google-analytics-data-api.md | 8 +++++--- 6 files changed, 16 insertions(+), 4 deletions(-) diff --git a/airbyte-integrations/connectors/source-google-analytics-data-api/metadata.yaml b/airbyte-integrations/connectors/source-google-analytics-data-api/metadata.yaml index ed216435519ea..7570e71c94f9a 100644 --- a/airbyte-integrations/connectors/source-google-analytics-data-api/metadata.yaml +++ b/airbyte-integrations/connectors/source-google-analytics-data-api/metadata.yaml @@ -12,7 +12,7 @@ data: connectorSubtype: api connectorType: source definitionId: 3cc2eafd-84aa-4dca-93af-322d9dfeec1a - dockerImageTag: 2.1.1 + dockerImageTag: 2.2.0 dockerRepository: airbyte/source-google-analytics-data-api documentationUrl: https://docs.airbyte.com/integrations/sources/google-analytics-data-api githubIssueLabel: source-google-analytics-data-api diff --git a/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/source.py b/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/source.py index 149bc5e7a786c..2418186d271b9 100644 --- a/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/source.py +++ b/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/source.py @@ -292,6 +292,7 @@ def request_body_json( "returnPropertyQuota": True, "offset": str(0), "limit": str(self.page_size), + "keepEmptyRows": self.config.get("keep_empty_rows", False), } dimension_filter = self.config.get("dimensionFilter") diff --git a/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/spec.json b/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/spec.json index 8efc2e7b13a5c..c487c6ff3572d 100644 --- a/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/spec.json +++ b/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/spec.json @@ -2235,6 +2235,13 @@ "maximum": 364, "default": 1, "order": 5 + }, + "keep_empty_rows": { + "type": "boolean", + "title": "Keep Empty Rows", + "description": "If false, each row with all metrics equal to 0 will not be returned. If true, these rows will be returned if they are not separately removed by a filter. More information is available in the documentation.", + "default": false, + "order": 6 } } }, diff --git a/airbyte-integrations/connectors/source-google-analytics-data-api/unit_tests/conftest.py b/airbyte-integrations/connectors/source-google-analytics-data-api/unit_tests/conftest.py index 6abb31990bd2d..fcd8e1b879be3 100644 --- a/airbyte-integrations/connectors/source-google-analytics-data-api/unit_tests/conftest.py +++ b/airbyte-integrations/connectors/source-google-analytics-data-api/unit_tests/conftest.py @@ -49,6 +49,7 @@ def config(one_year_ago): "screenPageViewsPerSession", "bounceRate", ], + "keep_empty_rows": True, "custom_reports": json.dumps( [ { diff --git a/airbyte-integrations/connectors/source-google-analytics-data-api/unit_tests/test_streams.py b/airbyte-integrations/connectors/source-google-analytics-data-api/unit_tests/test_streams.py index 393492730b29c..94597c7dc1840 100644 --- a/airbyte-integrations/connectors/source-google-analytics-data-api/unit_tests/test_streams.py +++ b/airbyte-integrations/connectors/source-google-analytics-data-api/unit_tests/test_streams.py @@ -80,6 +80,7 @@ def test_request_body_json(patch_base_class): {"name": "operatingSystem"}, {"name": "browser"}, ], + "keepEmptyRows": True, "dateRanges": [request_body_params["stream_slice"]], "returnPropertyQuota": True, "offset": str(0), diff --git a/docs/integrations/sources/google-analytics-data-api.md b/docs/integrations/sources/google-analytics-data-api.md index 6923ec0801aa9..5cac246e905a0 100644 --- a/docs/integrations/sources/google-analytics-data-api.md +++ b/docs/integrations/sources/google-analytics-data-api.md @@ -91,8 +91,9 @@ If the start date is not provided, the default value will be used, which is two Many analyses and data investigations may require 24-48 hours to process information from your website or app. To ensure the accuracy of the data, we subtract two days from the starting date. For more details, please refer to [Google's documentation](https://support.google.com/analytics/answer/9333790?hl=en). ::: -7. (Optional) In the **Custom Reports** field, you may optionally describe any custom reports you want to sync from Google Analytics. See the [Custom Reports](#custom-reports) section below for more information on formulating these reports. -8. (Optional) In the **Data Request Interval (Days)** field, you can specify the interval in days (ranging from 1 to 364) used when requesting data from the Google Analytics API. The bigger this value is, the faster the sync will be, but the more likely that sampling will be applied to your data, potentially causing inaccuracies in the returned results. We recommend setting this to 1 unless you have a hard requirement to make the sync faster at the expense of accuracy. This field does not apply to custom Cohort reports. See the [Data Sampling](#data-sampling-and-data-request-intervals) section below for more context on this field. +7. (Optional) Toggle the switch **Keep Empty Rows** if you want each row with all metrics equal to 0 to be returned. +8. (Optional) In the **Custom Reports** field, you may optionally describe any custom reports you want to sync from Google Analytics. See the [Custom Reports](#custom-reports) section below for more information on formulating these reports. +9. (Optional) In the **Data Request Interval (Days)** field, you can specify the interval in days (ranging from 1 to 364) used when requesting data from the Google Analytics API. The bigger this value is, the faster the sync will be, but the more likely that sampling will be applied to your data, potentially causing inaccuracies in the returned results. We recommend setting this to 1 unless you have a hard requirement to make the sync faster at the expense of accuracy. This field does not apply to custom Cohort reports. See the [Data Sampling](#data-sampling-and-data-request-intervals) section below for more context on this field. :::caution @@ -263,7 +264,8 @@ The Google Analytics connector is subject to Google Analytics Data API quotas. P | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:--------------------------------------------------------------------------------| -| 2.1.1 | 2024-01-08 | [1234](https://github.com/airbytehq/airbyte/pull/1234) | prepare for airbyte-lib | +| 2.2.0 | 2024-01-10 | [34176](https://github.com/airbytehq/airbyte/pull/34176) | Add a report option keepEmptyRows | +| 2.1.1 | 2024-01-08 | [34018](https://github.com/airbytehq/airbyte/pull/34018) | prepare for airbyte-lib | | 2.1.0 | 2023-12-28 | [33802](https://github.com/airbytehq/airbyte/pull/33802) | Add `CohortSpec` to custom report in specification | | 2.0.3 | 2023-11-03 | [32149](https://github.com/airbytehq/airbyte/pull/32149) | Fixed bug with missing `metadata` when the credentials are not valid | | 2.0.2 | 2023-11-02 | [32094](https://github.com/airbytehq/airbyte/pull/32094) | Added handling for `JSONDecodeError` while checking for `api qouta` limits | From 8d27b6be6800a0a9e7948301d87d02ad39c8680c Mon Sep 17 00:00:00 2001 From: Augustin Date: Mon, 15 Jan 2024 11:55:03 +0100 Subject: [PATCH 089/574] airbyte-ci: connector test steps can take extra parameters from CLI (#34050) --- airbyte-ci/connectors/pipelines/README.md | 14 ++++ .../airbyte_ci/connectors/context.py | 2 +- .../airbyte_ci/connectors/test/commands.py | 20 ++++-- .../airbyte_ci/connectors/test/pipeline.py | 19 ++++-- .../connectors/test/steps/common.py | 22 +++++-- .../connectors/test/steps/java_connectors.py | 14 ++-- .../test/steps/python_connectors.py | 39 +++++------ .../pipelines/airbyte_ci/metadata/pipeline.py | 2 +- .../pipelines/airbyte_ci/steps/gradle.py | 19 ++++-- .../pipelines/helpers/execution/__init__.py | 0 .../helpers/execution/argument_parsing.py | 66 +++++++++++++++++++ .../helpers/{ => execution}/run_steps.py | 9 ++- .../models/contexts/pipeline_context.py | 2 +- .../pipelines/pipelines/models/steps.py | 43 +++++++++++- .../connectors/pipelines/pyproject.toml | 2 +- .../connectors/pipelines/tests/test_gradle.py | 19 ++++++ .../test_helpers/test_execution/__init__.py | 0 .../test_execution/test_argument_parsing.py | 36 ++++++++++ .../{ => test_execution}/test_run_steps.py | 15 ++++- .../pipelines/tests/test_tests/test_common.py | 6 ++ .../test_tests/test_python_connectors.py | 8 +++ 21 files changed, 305 insertions(+), 52 deletions(-) create mode 100644 airbyte-ci/connectors/pipelines/pipelines/helpers/execution/__init__.py create mode 100644 airbyte-ci/connectors/pipelines/pipelines/helpers/execution/argument_parsing.py rename airbyte-ci/connectors/pipelines/pipelines/helpers/{ => execution}/run_steps.py (95%) create mode 100644 airbyte-ci/connectors/pipelines/tests/test_helpers/test_execution/__init__.py create mode 100644 airbyte-ci/connectors/pipelines/tests/test_helpers/test_execution/test_argument_parsing.py rename airbyte-ci/connectors/pipelines/tests/test_helpers/{ => test_execution}/test_run_steps.py (95%) diff --git a/airbyte-ci/connectors/pipelines/README.md b/airbyte-ci/connectors/pipelines/README.md index 4f26d9ad07fe9..552e103f2bdb7 100644 --- a/airbyte-ci/connectors/pipelines/README.md +++ b/airbyte-ci/connectors/pipelines/README.md @@ -264,11 +264,24 @@ flowchart TD | `--fail-fast` | False | False | Abort after any tests fail, rather than continuing to run additional tests. Use this setting to confirm a known bug is fixed (or not), or when you only require a pass/fail result. | | `--code-tests-only` | True | False | Skip any tests not directly related to code updates. For instance, metadata checks, version bump checks, changelog verification, etc. Use this setting to help focus on code quality during development. | | `--concurrent-cat` | False | False | Make CAT tests run concurrently using pytest-xdist. Be careful about source or destination API rate limits. | +| `--.=` | True | | You can pass extra parameters for specific test steps. More details in the extra parameters section below | Note: - The above options are implemented for Java connectors but may not be available for Python connectors. If an option is not supported, the pipeline will not fail but instead the 'default' behavior will be executed. +#### Extra parameters +You can pass extra parameters to the following steps: +* `unit` +* `integration` +* `acceptance` + +This allows you to override the default parameters of these steps. +For example, you can only run the `test_read` test of the acceptance test suite with: +`airbyte-ci connectors --name=source-pokeapi test --acceptance.-k=test_read` +Here the `-k` parameter is passed to the pytest command running acceptance tests. +Please keep in mind that the extra parameters are not validated by the CLI: if you pass an invalid parameter, you'll face a late failure during the pipeline execution. + ### `connectors build` command Run a build pipeline for one or multiple connectors and export the built docker image to the local docker host. @@ -521,6 +534,7 @@ E.G.: running `pytest` on a specific test folder: | Version | PR | Description | | ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| 3.2.0 | [#34050](https://github.com/airbytehq/airbyte/pull/34050) | Connector test steps can take extra parameters | | 3.1.3 | [#34136](https://github.com/airbytehq/airbyte/pull/34136) | Fix issue where dagger excludes were not being properly applied | | 3.1.2 | [#33972](https://github.com/airbytehq/airbyte/pull/33972) | Remove secrets scrubbing hack for --is-local and other small tweaks. | | 3.1.1 | [#33979](https://github.com/airbytehq/airbyte/pull/33979) | Fix AssertionError on report existence again | diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/context.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/context.py index 9120fc07e0cf4..dff4f9b2a7360 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/context.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/context.py @@ -19,8 +19,8 @@ from pipelines.consts import BUILD_PLATFORMS from pipelines.dagger.actions import secrets from pipelines.helpers.connectors.modifed import ConnectorWithModifiedFiles +from pipelines.helpers.execution.run_steps import RunStepOptions from pipelines.helpers.github import update_commit_status_check -from pipelines.helpers.run_steps import RunStepOptions from pipelines.helpers.slack import send_message_to_webhook from pipelines.helpers.utils import METADATA_FILE_NAME from pipelines.models.contexts.pipeline_context import PipelineContext diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/commands.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/commands.py index 00bdffeabccf2..07f48bc5ca751 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/commands.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/commands.py @@ -3,7 +3,7 @@ # import sys -from typing import List +from typing import Dict, List import asyncclick as click from pipelines import main_logger @@ -13,12 +13,20 @@ from pipelines.airbyte_ci.connectors.test.pipeline import run_connector_test_pipeline from pipelines.cli.dagger_pipeline_command import DaggerPipelineCommand from pipelines.consts import LOCAL_BUILD_PLATFORM, ContextState +from pipelines.helpers.execution import argument_parsing +from pipelines.helpers.execution.run_steps import RunStepOptions from pipelines.helpers.github import update_global_commit_status_check_for_tests -from pipelines.helpers.run_steps import RunStepOptions from pipelines.helpers.utils import fail_if_missing_docker_hub_creds +from pipelines.models.steps import STEP_PARAMS -@click.command(cls=DaggerPipelineCommand, help="Test all the selected connectors.") +@click.command( + cls=DaggerPipelineCommand, + help="Test all the selected connectors.", + context_settings=dict( + ignore_unknown_options=True, + ), +) @click.option( "--code-tests-only", is_flag=True, @@ -47,6 +55,9 @@ type=click.Choice([step_id.value for step_id in CONNECTOR_TEST_STEP_ID]), help="Skip a step by name. Can be used multiple times to skip multiple steps.", ) +@click.argument( + "extra_params", nargs=-1, type=click.UNPROCESSED, callback=argument_parsing.build_extra_params_mapping(CONNECTOR_TEST_STEP_ID) +) @click.pass_context async def test( ctx: click.Context, @@ -54,6 +65,7 @@ async def test( fail_fast: bool, concurrent_cat: bool, skip_step: List[str], + extra_params: Dict[CONNECTOR_TEST_STEP_ID, STEP_PARAMS], ) -> bool: """Runs a test pipeline for the selected connectors. @@ -76,8 +88,8 @@ async def test( run_step_options = RunStepOptions( fail_fast=fail_fast, skip_steps=[CONNECTOR_TEST_STEP_ID(step_id) for step_id in skip_step], + step_params=extra_params, ) - connectors_tests_contexts = [ ConnectorContext( pipeline_name=f"Testing connector {connector.technical_name}", diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/pipeline.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/pipeline.py index 82b9b0099efc4..d1b875a1c1800 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/pipeline.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/pipeline.py @@ -3,6 +3,9 @@ # """This module groups factory like functions to dispatch tests steps according to the connector under test language.""" +from __future__ import annotations + +from typing import TYPE_CHECKING import anyio from connector_ops.utils import ConnectorLanguage # type: ignore @@ -12,7 +15,11 @@ from pipelines.airbyte_ci.connectors.test.steps import java_connectors, python_connectors from pipelines.airbyte_ci.connectors.test.steps.common import QaChecks, VersionFollowsSemverCheck, VersionIncrementCheck from pipelines.airbyte_ci.metadata.pipeline import MetadataValidation -from pipelines.helpers.run_steps import STEP_TREE, StepToRun, run_steps +from pipelines.helpers.execution.run_steps import StepToRun, run_steps + +if TYPE_CHECKING: + + from pipelines.helpers.execution.run_steps import STEP_TREE LANGUAGE_MAPPING = { "get_test_steps": { @@ -30,7 +37,7 @@ def get_test_steps(context: ConnectorContext) -> STEP_TREE: context (ConnectorContext): The current connector context. Returns: - List[StepResult]: The list of tests steps. + STEP_TREE: The list of tests steps. """ if _get_test_steps := LANGUAGE_MAPPING["get_test_steps"].get(context.connector.language): return _get_test_steps(context) @@ -43,11 +50,12 @@ async def run_connector_test_pipeline(context: ConnectorContext, semaphore: anyi """ Compute the steps to run for a connector test pipeline. """ + all_steps_to_run: STEP_TREE = [] - steps_to_run = get_test_steps(context) + all_steps_to_run += get_test_steps(context) if not context.code_tests_only: - steps_to_run += [ + static_analysis_steps_to_run = [ [ StepToRun(id=CONNECTOR_TEST_STEP_ID.METADATA_VALIDATION, step=MetadataValidation(context)), StepToRun(id=CONNECTOR_TEST_STEP_ID.VERSION_FOLLOW_CHECK, step=VersionFollowsSemverCheck(context)), @@ -55,11 +63,12 @@ async def run_connector_test_pipeline(context: ConnectorContext, semaphore: anyi StepToRun(id=CONNECTOR_TEST_STEP_ID.QA_CHECKS, step=QaChecks(context)), ] ] + all_steps_to_run += static_analysis_steps_to_run async with semaphore: async with context: result_dict = await run_steps( - runnables=steps_to_run, + runnables=all_steps_to_run, options=context.run_step_options, ) diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/steps/common.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/steps/common.py index f7139fe885bd9..dc780ac50f1ae 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/steps/common.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/steps/common.py @@ -21,7 +21,7 @@ from pipelines.dagger.actions import secrets from pipelines.dagger.containers import internal_tools from pipelines.helpers.utils import METADATA_FILE_NAME -from pipelines.models.steps import Step, StepResult, StepStatus +from pipelines.models.steps import STEP_PARAMS, Step, StepResult, StepStatus class VersionCheck(Step, ABC): @@ -193,6 +193,20 @@ class AcceptanceTests(Step): CONTAINER_TEST_INPUT_DIRECTORY = "/test_input" CONTAINER_SECRETS_DIRECTORY = "/test_input/secrets" skipped_exit_code = 5 + accept_extra_params = True + + @property + def default_params(self) -> STEP_PARAMS: + """Default pytest options. + + Returns: + dict: The default pytest options. + """ + return super().default_params | { + "-ra": [], # Show extra test summary info in the report for all but the passed tests + "--disable-warnings": [], # Disable warnings in the pytest report + "--durations": ["3"], # Show the 3 slowest tests in the report + } @property def base_cat_command(self) -> List[str]: @@ -200,14 +214,12 @@ def base_cat_command(self) -> List[str]: "python", "-m", "pytest", - "--disable-warnings", - "--durations=3", # Show the 3 slowest tests in the report - "-ra", # Show extra test summary info in the report for all but the passed tests "-p", # Load the connector_acceptance_test plugin "connector_acceptance_test.plugin", "--acceptance-test-config", self.CONTAINER_TEST_INPUT_DIRECTORY, ] + if self.concurrent_test_run: command += ["--numprocesses=auto"] # Using pytest-xdist to run tests in parallel, auto means using all available cores return command @@ -232,7 +244,7 @@ async def get_cat_command(self, connector_dir: Directory) -> List[str]: if "integration_tests" in await connector_dir.entries(): if "acceptance.py" in await connector_dir.directory("integration_tests").entries(): cat_command += ["-p", "integration_tests.acceptance"] - return cat_command + return cat_command + self.params_as_cli_options async def _run(self, connector_under_test_container: Container) -> StepResult: """Run the acceptance test suite on a connector dev image. Build the connector acceptance test image if the tag is :dev. diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/steps/java_connectors.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/steps/java_connectors.py index 5ca660a1112d4..06b0aaea43141 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/steps/java_connectors.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/steps/java_connectors.py @@ -21,24 +21,30 @@ from pipelines.airbyte_ci.steps.gradle import GradleTask from pipelines.consts import LOCAL_BUILD_PLATFORM from pipelines.dagger.actions.system import docker -from pipelines.helpers.run_steps import StepToRun +from pipelines.helpers.execution.run_steps import StepToRun from pipelines.helpers.utils import export_container_to_tarball -from pipelines.models.steps import StepResult, StepStatus +from pipelines.models.steps import STEP_PARAMS, StepResult, StepStatus if TYPE_CHECKING: from typing import Callable, Dict, List, Optional - from pipelines.helpers.run_steps import RESULTS_DICT, STEP_TREE + from pipelines.helpers.execution.run_steps import RESULTS_DICT, STEP_TREE class IntegrationTests(GradleTask): """A step to run integrations tests for Java connectors using the integrationTestJava Gradle task.""" title = "Java Connector Integration Tests" - gradle_task_name = "integrationTestJava -x buildConnectorImage -x assemble" + gradle_task_name = "integrationTestJava" mount_connector_secrets = True bind_to_docker_host = True + @property + def default_params(self) -> STEP_PARAMS: + return super().default_params | { + "-x": ["buildConnectorImage", "assemble"], # Exclude the buildConnectorImage and assemble tasks + } + async def _load_normalization_image(self, normalization_tar_file: File) -> None: normalization_image_tag = f"{self.context.connector.normalization_repository}:dev" self.context.logger.info("Load the normalization image to the docker host.") diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/steps/python_connectors.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/steps/python_connectors.py index 65117b6d804b9..5b2d71c465c6d 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/steps/python_connectors.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/steps/python_connectors.py @@ -16,8 +16,8 @@ from pipelines.airbyte_ci.connectors.test.steps.common import AcceptanceTests, CheckBaseImageIsUsed from pipelines.consts import LOCAL_BUILD_PLATFORM from pipelines.dagger.actions import secrets -from pipelines.helpers.run_steps import STEP_TREE, StepToRun -from pipelines.models.steps import Step, StepResult +from pipelines.helpers.execution.run_steps import STEP_TREE, StepToRun +from pipelines.models.steps import STEP_PARAMS, Step, StepResult class PytestStep(Step, ABC): @@ -31,6 +31,18 @@ class PytestStep(Step, ABC): skipped_exit_code = 5 bind_to_docker_host = False + accept_extra_params = True + + @property + def default_params(self) -> STEP_PARAMS: + """Default pytest options. + + Returns: + dict: The default pytest options. + """ + return super().default_params | { + "-s": [], # Disable capturing stdout/stderr in pytest + } @property @abstractmethod @@ -43,15 +55,6 @@ def extra_dependencies_names(self) -> Sequence[str]: return ("dev",) return ("dev", "tests") - @property - def additional_pytest_options(self) -> List[str]: - """Theses options are added to the pytest command. - - Returns: - List[str]: The additional pytest options. - """ - return [] - async def _run(self, connector_under_test: Container) -> StepResult: """Run all pytest tests declared in the test directory of the connector code. @@ -83,7 +86,7 @@ def get_pytest_command(self, test_config_file_name: str) -> List[str]: Returns: List[str]: The pytest command to run. """ - cmd = ["pytest", "-s", self.test_directory_name, "-c", test_config_file_name] + self.additional_pytest_options + cmd = ["pytest", self.test_directory_name, "-c", test_config_file_name] + self.params_as_cli_options if self.context.connector.is_using_poetry: return ["poetry", "run"] + cmd return cmd @@ -174,18 +177,16 @@ class UnitTests(PytestStep): MINIMUM_COVERAGE_FOR_CERTIFIED_CONNECTORS = 90 @property - def additional_pytest_options(self) -> List[str]: + def default_params(self) -> STEP_PARAMS: """Make sure the coverage computation is run for the unit tests. - Fail if the coverage is under 90% for certified connectors. Returns: - List[str]: The additional pytest options to run coverage reports. + dict: The default pytest options. """ - coverage_options = ["--cov", self.context.connector.technical_name.replace("-", "_")] + coverage_options = {"--cov": [self.context.connector.technical_name.replace("-", "_")]} if self.context.connector.support_level == "certified": - coverage_options += ["--cov-fail-under", str(self.MINIMUM_COVERAGE_FOR_CERTIFIED_CONNECTORS)] - - return super().additional_pytest_options + coverage_options + coverage_options["--cov-fail-under"] = [str(self.MINIMUM_COVERAGE_FOR_CERTIFIED_CONNECTORS)] + return super().default_params | coverage_options class IntegrationTests(PytestStep): diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/pipeline.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/pipeline.py index eae516a8db79f..4860decdaf739 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/pipeline.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/pipeline.py @@ -13,7 +13,7 @@ from pipelines.consts import DOCS_DIRECTORY_ROOT_PATH, INTERNAL_TOOL_PATHS from pipelines.dagger.actions.python.common import with_pip_packages from pipelines.dagger.containers.python import with_python_base -from pipelines.helpers.run_steps import STEP_TREE, StepToRun, run_steps +from pipelines.helpers.execution.run_steps import STEP_TREE, StepToRun, run_steps from pipelines.helpers.utils import DAGGER_CONFIG, get_secret_host_variable from pipelines.models.reports import Report from pipelines.models.steps import MountPath, Step, StepResult diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/steps/gradle.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/steps/gradle.py index 94e0cb8ff7690..ae44de953449c 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/steps/gradle.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/steps/gradle.py @@ -11,7 +11,7 @@ from pipelines.consts import AMAZONCORRETTO_IMAGE from pipelines.dagger.actions import secrets from pipelines.helpers.utils import sh_dash_c -from pipelines.models.steps import Step, StepResult +from pipelines.models.steps import STEP_PARAMS, Step, StepResult class GradleTask(Step, ABC): @@ -27,14 +27,23 @@ class GradleTask(Step, ABC): context: ConnectorContext - DEFAULT_GRADLE_TASK_OPTIONS = ("--no-daemon", "--no-watch-fs", "--scan", "--build-cache", "--console=plain") LOCAL_MAVEN_REPOSITORY_PATH = "/root/.m2" GRADLE_DEP_CACHE_PATH = "/root/gradle-cache" GRADLE_HOME_PATH = "/root/.gradle" - + STATIC_GRADLE_TASK_OPTIONS = ("--no-daemon", "--no-watch-fs") gradle_task_name: ClassVar[str] bind_to_docker_host: ClassVar[bool] = False mount_connector_secrets: ClassVar[bool] = False + accept_extra_params = True + + @property + def default_params(self) -> STEP_PARAMS: + return super().default_params | { + "-Ds3BuildCachePrefix": [self.context.connector.technical_name], # Set the S3 build cache prefix. + "--build-cache": [], # Enable the gradle build cache. + "--scan": [], # Enable the gradle build scan. + "--console": ["plain"], # Disable the gradle rich console. + } @property def dependency_cache_volume(self) -> CacheVolume: @@ -56,7 +65,7 @@ def build_include(self) -> List[str]: ] def _get_gradle_command(self, task: str, *args: Any) -> str: - return f"./gradlew {' '.join(self.DEFAULT_GRADLE_TASK_OPTIONS + args)} {task}" + return f"./gradlew {' '.join(self.STATIC_GRADLE_TASK_OPTIONS + args)} {task}" async def _run(self, *args: Any, **kwargs: Any) -> StepResult: include = [ @@ -191,7 +200,7 @@ async def _run(self, *args: Any, **kwargs: Any) -> StepResult: # Warm the gradle cache. f"(rsync -a --stats --mkpath {self.GRADLE_DEP_CACHE_PATH}/ {self.GRADLE_HOME_PATH} || true)", # Run the gradle task. - self._get_gradle_command(connector_task, f"-Ds3BuildCachePrefix={self.context.connector.technical_name}"), + self._get_gradle_command(connector_task, *self.params_as_cli_options), ] ) ) diff --git a/airbyte-ci/connectors/pipelines/pipelines/helpers/execution/__init__.py b/airbyte-ci/connectors/pipelines/pipelines/helpers/execution/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/airbyte-ci/connectors/pipelines/pipelines/helpers/execution/argument_parsing.py b/airbyte-ci/connectors/pipelines/pipelines/helpers/execution/argument_parsing.py new file mode 100644 index 0000000000000..af32aa52b2130 --- /dev/null +++ b/airbyte-ci/connectors/pipelines/pipelines/helpers/execution/argument_parsing.py @@ -0,0 +1,66 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +import asyncclick as click + +if TYPE_CHECKING: + from enum import Enum + from typing import Callable, Dict, Tuple, Type + + from pipelines.models.steps import STEP_PARAMS + +# Pattern for extra param options: --.= +EXTRA_PARAM_PATTERN_FOR_OPTION = re.compile(r"^--([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_-][a-zA-Z0-9_-]*)=([^=]+)$") +# Pattern for extra param flag: --. +EXTRA_PARAM_PATTERN_FOR_FLAG = re.compile(r"^--([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_-][a-zA-Z0-9_-]*)$") +EXTRA_PARAM_PATTERN_ERROR_MESSAGE = "The extra flags must be structured as --. for flags or --.= for options. You can use - or -- for option/flag names." + + +def build_extra_params_mapping(SupportedStepIds: Type[Enum]) -> Callable: + def callback(ctx: click.Context, argument: click.core.Argument, raw_extra_params: Tuple[str]) -> Dict[str, STEP_PARAMS]: + """Build a mapping of step id to extra params. + Validate the extra params and raise a ValueError if they are invalid. + Validation rules: + - The extra params must be structured as --.= for options or --. for flags. + - The step id must be one of the existing step ids. + + + Args: + ctx (click.Context): The click context. + argument (click.core.Argument): The click argument. + raw_extra_params (Tuple[str]): The extra params provided by the user. + Raises: + ValueError: Raised if the extra params format is invalid. + ValueError: Raised if the step id in the extra params is not one of the unique steps to run. + + Returns: + Dict[Literal, STEP_PARAMS]: The mapping of step id to extra params. + """ + extra_params_mapping: Dict[str, STEP_PARAMS] = {} + for param in raw_extra_params: + is_flag = "=" not in param + pattern = EXTRA_PARAM_PATTERN_FOR_FLAG if is_flag else EXTRA_PARAM_PATTERN_FOR_OPTION + matches = pattern.match(param) + if not matches: + raise ValueError(f"Invalid parameter {param}. {EXTRA_PARAM_PATTERN_ERROR_MESSAGE}") + if is_flag: + step_name, param_name = matches.groups() + param_value = None + else: + step_name, param_name, param_value = matches.groups() + try: + step_id = SupportedStepIds(step_name).value + except ValueError: + raise ValueError(f"Invalid step name {step_name}, it must be one of {[step_id.value for step_id in SupportedStepIds]}") + + extra_params_mapping.setdefault(step_id, {}).setdefault(param_name, []) + # param_value is None if the param is a flag + if param_value is not None: + extra_params_mapping[step_id][param_name].append(param_value) + return extra_params_mapping + + return callback diff --git a/airbyte-ci/connectors/pipelines/pipelines/helpers/run_steps.py b/airbyte-ci/connectors/pipelines/pipelines/helpers/execution/run_steps.py similarity index 95% rename from airbyte-ci/connectors/pipelines/pipelines/helpers/run_steps.py rename to airbyte-ci/connectors/pipelines/pipelines/helpers/execution/run_steps.py index 7a66acad61d7c..8fb320d9fd5e7 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/helpers/run_steps.py +++ b/airbyte-ci/connectors/pipelines/pipelines/helpers/execution/run_steps.py @@ -16,7 +16,8 @@ from pipelines.models.steps import StepStatus if TYPE_CHECKING: - from pipelines.models.steps import Step, StepResult + from pipelines.airbyte_ci.connectors.consts import CONNECTOR_TEST_STEP_ID + from pipelines.models.steps import STEP_PARAMS, Step, StepResult RESULTS_DICT = Dict[str, StepResult] ARGS_TYPE = Union[Dict, Callable[[RESULTS_DICT], Dict], Awaitable[Dict]] @@ -34,6 +35,7 @@ class RunStepOptions: skip_steps: List[str] = field(default_factory=list) log_step_tree: bool = True concurrency: int = 10 + step_params: Dict[CONNECTOR_TEST_STEP_ID, STEP_PARAMS] = field(default_factory=dict) @dataclass(frozen=True) @@ -44,7 +46,7 @@ class StepToRun: Used to coordinate the execution of multiple steps inside a pipeline. """ - id: str + id: CONNECTOR_TEST_STEP_ID step: Step args: ARGS_TYPE = field(default_factory=dict) depends_on: List[str] = field(default_factory=list) @@ -71,7 +73,7 @@ def _skip_remaining_steps(remaining_steps: STEP_TREE) -> RESULTS_DICT: """ Skip all remaining steps. """ - skipped_results = {} + skipped_results: Dict[str, StepResult] = {} for runnable_step in remaining_steps: if isinstance(runnable_step, StepToRun): skipped_results[runnable_step.id] = runnable_step.step.skip() @@ -243,6 +245,7 @@ async def run_steps( tasks.append(task_group.soonify(run_steps)(list(step_to_run), results, options)) else: step_args = await evaluate_run_args(step_to_run.args, results) + step_to_run.step.extra_params = options.step_params.get(step_to_run.id, {}) main_logger.info(f"QUEUING STEP {step_to_run.id}") tasks.append(task_group.soonify(step_to_run.step.run)(**step_args)) diff --git a/airbyte-ci/connectors/pipelines/pipelines/models/contexts/pipeline_context.py b/airbyte-ci/connectors/pipelines/pipelines/models/contexts/pipeline_context.py index 83b1cb8e4b6d7..0d2431560145f 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/models/contexts/pipeline_context.py +++ b/airbyte-ci/connectors/pipelines/pipelines/models/contexts/pipeline_context.py @@ -18,9 +18,9 @@ from github import PullRequest from pipelines.airbyte_ci.connectors.reports import ConnectorReport from pipelines.consts import CIContext, ContextState +from pipelines.helpers.execution.run_steps import RunStepOptions from pipelines.helpers.gcs import sanitize_gcs_credentials from pipelines.helpers.github import update_commit_status_check -from pipelines.helpers.run_steps import RunStepOptions from pipelines.helpers.slack import send_message_to_webhook from pipelines.helpers.utils import AIRBYTE_REPO_URL from pipelines.models.reports import Report diff --git a/airbyte-ci/connectors/pipelines/pipelines/models/steps.py b/airbyte-ci/connectors/pipelines/pipelines/models/steps.py index 7a285c8ec4a8e..bc3acafebc060 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/models/steps.py +++ b/airbyte-ci/connectors/pipelines/pipelines/models/steps.py @@ -10,7 +10,7 @@ from datetime import datetime, timedelta from enum import Enum from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, List import anyio import asyncer @@ -25,10 +25,13 @@ from pipelines.airbyte_ci.format.format_command import FormatCommand from pipelines.models.contexts.pipeline_context import PipelineContext + from abc import ABC from rich.style import Style +STEP_PARAMS = Dict[str, List[str]] + @dataclass class MountPath: @@ -155,14 +158,50 @@ class Step(ABC): # The max duration of a step run. If the step run for more than this duration it will be considered as timed out. # The default of 5 hours is arbitrary and can be changed if needed. max_duration: ClassVar[timedelta] = timedelta(hours=5) - retry_delay = timedelta(seconds=10) + accept_extra_params: bool = False def __init__(self, context: PipelineContext) -> None: # noqa D107 self.context = context self.retry_count = 0 self.started_at: Optional[datetime] = None self.stopped_at: Optional[datetime] = None + self._extra_params: STEP_PARAMS = {} + + @property + def extra_params(self) -> STEP_PARAMS: + return self._extra_params + + @extra_params.setter + def extra_params(self, value: STEP_PARAMS) -> None: + if value and not self.accept_extra_params: + raise ValueError(f"{self.__class__.__name__} does not accept extra params.") + self._extra_params = value + self.logger.info(f"Will run with the following parameters: {self.params}") + + @property + def default_params(self) -> STEP_PARAMS: + return {} + + @property + def params(self) -> STEP_PARAMS: + return self.default_params | self.extra_params + + @property + def params_as_cli_options(self) -> List[str]: + """Return the step params as a list of CLI options. + + Returns: + List[str]: The step params as a list of CLI options. + """ + cli_options: List[str] = [] + for name, values in self.params.items(): + if not values: + # If no values are available, we assume it is a flag + cli_options.append(name) + else: + cli_options.extend(f"{name}={value}" for value in values) + return cli_options @property def title(self) -> str: diff --git a/airbyte-ci/connectors/pipelines/pyproject.toml b/airbyte-ci/connectors/pipelines/pyproject.toml index 256d1075bad88..ef811f62aaca1 100644 --- a/airbyte-ci/connectors/pipelines/pyproject.toml +++ b/airbyte-ci/connectors/pipelines/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pipelines" -version = "3.1.3" +version = "3.2.0" description = "Packaged maintained by the connector operations team to perform CI for connectors' pipelines" authors = ["Airbyte "] diff --git a/airbyte-ci/connectors/pipelines/tests/test_gradle.py b/airbyte-ci/connectors/pipelines/tests/test_gradle.py index 1435d0d5ffcf4..5e867c3582ba7 100644 --- a/airbyte-ci/connectors/pipelines/tests/test_gradle.py +++ b/airbyte-ci/connectors/pipelines/tests/test_gradle.py @@ -18,6 +18,7 @@ class TestGradleTask: class DummyStep(gradle.GradleTask): gradle_task_name = "dummyTask" + title = "Dummy Step" async def _run(self) -> steps.StepResult: return steps.StepResult(self, steps.StepStatus.SUCCESS) @@ -35,3 +36,21 @@ def test_context(self, mocker, dagger_client): async def test_build_include(self, test_context): step = self.DummyStep(test_context) assert step.build_include + + def test_params(self, test_context): + step = self.DummyStep(test_context) + assert set(step.params_as_cli_options) == { + f"-Ds3BuildCachePrefix={test_context.connector.technical_name}", + "--build-cache", + "--scan", + "--console=plain", + } + step.extra_params = {"-x": ["dummyTask", "dummyTask2"], "--console": ["rich"]} + assert set(step.params_as_cli_options) == { + f"-Ds3BuildCachePrefix={test_context.connector.technical_name}", + "--build-cache", + "--scan", + "--console=rich", + "-x=dummyTask", + "-x=dummyTask2", + } diff --git a/airbyte-ci/connectors/pipelines/tests/test_helpers/test_execution/__init__.py b/airbyte-ci/connectors/pipelines/tests/test_helpers/test_execution/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/airbyte-ci/connectors/pipelines/tests/test_helpers/test_execution/test_argument_parsing.py b/airbyte-ci/connectors/pipelines/tests/test_helpers/test_execution/test_argument_parsing.py new file mode 100644 index 0000000000000..7201a2b83059c --- /dev/null +++ b/airbyte-ci/connectors/pipelines/tests/test_helpers/test_execution/test_argument_parsing.py @@ -0,0 +1,36 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import enum +import time + +import anyio +import pytest +from pipelines.helpers.execution import argument_parsing + + +class SupportedStepIds(enum.Enum): + STEP1 = "step1" + STEP2 = "step2" + STEP3 = "step3" + + +def test_build_extra_params_mapping(mocker): + ctx = mocker.Mock() + argument = mocker.Mock() + + raw_extra_params = ( + "--step1.param1=value1", + "--step2.param2=value2", + "--step3.param3=value3", + "--step1.param4", + ) + + result = argument_parsing.build_extra_params_mapping(SupportedStepIds)(ctx, argument, raw_extra_params) + + expected_result = { + SupportedStepIds.STEP1.value: {"param1": ["value1"], "param4": []}, + SupportedStepIds.STEP2.value: {"param2": ["value2"]}, + SupportedStepIds.STEP3.value: {"param3": ["value3"]}, + } + + assert result == expected_result diff --git a/airbyte-ci/connectors/pipelines/tests/test_helpers/test_run_steps.py b/airbyte-ci/connectors/pipelines/tests/test_helpers/test_execution/test_run_steps.py similarity index 95% rename from airbyte-ci/connectors/pipelines/tests/test_helpers/test_run_steps.py rename to airbyte-ci/connectors/pipelines/tests/test_helpers/test_execution/test_run_steps.py index e0ac8d3af4b16..cc2f2e4cb7c68 100644 --- a/airbyte-ci/connectors/pipelines/tests/test_helpers/test_run_steps.py +++ b/airbyte-ci/connectors/pipelines/tests/test_helpers/test_execution/test_run_steps.py @@ -4,7 +4,7 @@ import anyio import pytest -from pipelines.helpers.run_steps import InvalidStepConfiguration, RunStepOptions, StepToRun, run_steps +from pipelines.helpers.execution.run_steps import InvalidStepConfiguration, RunStepOptions, StepToRun, run_steps from pipelines.models.contexts.pipeline_context import PipelineContext from pipelines.models.steps import Step, StepResult, StepStatus @@ -346,3 +346,16 @@ async def test_run_steps_throws_on_invalid_args(invalid_args): with pytest.raises(TypeError): await run_steps(steps) + + +@pytest.mark.anyio +async def test_run_steps_with_params(): + steps = [StepToRun(id="step1", step=TestStep(test_context))] + options = RunStepOptions(fail_fast=True, step_params={"step1": {"--param1": ["value1"]}}) + TestStep.accept_extra_params = False + with pytest.raises(ValueError): + await run_steps(steps, options=options) + assert steps[0].step.params_as_cli_options == [] + TestStep.accept_extra_params = True + await run_steps(steps, options=options) + assert steps[0].step.params_as_cli_options == ["--param1=value1"] diff --git a/airbyte-ci/connectors/pipelines/tests/test_tests/test_common.py b/airbyte-ci/connectors/pipelines/tests/test_tests/test_common.py index 567136eeb2646..a9c1470b6dd71 100644 --- a/airbyte-ci/connectors/pipelines/tests/test_tests/test_common.py +++ b/airbyte-ci/connectors/pipelines/tests/test_tests/test_common.py @@ -241,6 +241,12 @@ async def test_cat_container_caching( fourth_date_result = await cat_container.stdout() assert fourth_date_result != third_date_result + async def test_params(self, dagger_client, mocker, test_context_ci, test_input_dir): + acceptance_test_step = self.get_patched_acceptance_test_step(dagger_client, mocker, test_context_ci, test_input_dir) + assert set(acceptance_test_step.params_as_cli_options) == {"-ra", "--disable-warnings", "--durations=3"} + acceptance_test_step.extra_params = {"--durations": ["5"], "--collect-only": []} + assert set(acceptance_test_step.params_as_cli_options) == {"-ra", "--disable-warnings", "--durations=5", "--collect-only"} + class TestCheckBaseImageIsUsed: @pytest.fixture diff --git a/airbyte-ci/connectors/pipelines/tests/test_tests/test_python_connectors.py b/airbyte-ci/connectors/pipelines/tests/test_tests/test_python_connectors.py index 4063c782c05fd..2d89af9ec94d3 100644 --- a/airbyte-ci/connectors/pipelines/tests/test_tests/test_python_connectors.py +++ b/airbyte-ci/connectors/pipelines/tests/test_tests/test_python_connectors.py @@ -97,3 +97,11 @@ async def test__run_for_poetry(self, context_for_connector_with_poetry, containe context_for_connector_with_poetry.connector.technical_name in pip_freeze_output ), "The connector should be installed in the test environment." assert "pytest" in pip_freeze_output, "The pytest package should be installed in the test environment." + + def test_params(self, context_for_certified_connector_with_setup): + step = UnitTests(context_for_certified_connector_with_setup) + assert step.params_as_cli_options == [ + "-s", + f"--cov={context_for_certified_connector_with_setup.connector.technical_name.replace('-', '_')}", + f"--cov-fail-under={step.MINIMUM_COVERAGE_FOR_CERTIFIED_CONNECTORS}", + ] From 919f94bc502ac86bcffc8a3fb9c74433a171fde1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 15 Jan 2024 12:15:49 +0100 Subject: [PATCH 090/574] Source Shopify: Convert to airbyte-lib (#33935) --- .../connectors/source-shopify/main.py | 8 ++------ .../connectors/source-shopify/metadata.yaml | 2 +- .../connectors/source-shopify/setup.py | 5 +++++ .../source-shopify/source_shopify/run.py | 15 +++++++++++++++ docs/integrations/sources/shopify.md | 1 + 5 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 airbyte-integrations/connectors/source-shopify/source_shopify/run.py diff --git a/airbyte-integrations/connectors/source-shopify/main.py b/airbyte-integrations/connectors/source-shopify/main.py index a45dc5aaf6118..aca13eebbb253 100644 --- a/airbyte-integrations/connectors/source-shopify/main.py +++ b/airbyte-integrations/connectors/source-shopify/main.py @@ -3,11 +3,7 @@ # -import sys - -from airbyte_cdk.entrypoint import launch -from source_shopify import SourceShopify +from source_shopify.run import run if __name__ == "__main__": - source = SourceShopify() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-shopify/metadata.yaml b/airbyte-integrations/connectors/source-shopify/metadata.yaml index 544755b10fe30..859bd0c29ba48 100644 --- a/airbyte-integrations/connectors/source-shopify/metadata.yaml +++ b/airbyte-integrations/connectors/source-shopify/metadata.yaml @@ -11,7 +11,7 @@ data: connectorSubtype: api connectorType: source definitionId: 9da77001-af33-4bcd-be46-6252bf9342b9 - dockerImageTag: 1.1.5 + dockerImageTag: 1.1.6 dockerRepository: airbyte/source-shopify documentationUrl: https://docs.airbyte.com/integrations/sources/shopify githubIssueLabel: source-shopify diff --git a/airbyte-integrations/connectors/source-shopify/setup.py b/airbyte-integrations/connectors/source-shopify/setup.py index d1ec88ea5e7bb..7c87ac4cf564b 100644 --- a/airbyte-integrations/connectors/source-shopify/setup.py +++ b/airbyte-integrations/connectors/source-shopify/setup.py @@ -24,4 +24,9 @@ extras_require={ "tests": TEST_REQUIREMENTS, }, + entry_points={ + "console_scripts": [ + "source-shopify=source_shopify.run:run", + ], + }, ) diff --git a/airbyte-integrations/connectors/source-shopify/source_shopify/run.py b/airbyte-integrations/connectors/source-shopify/source_shopify/run.py new file mode 100644 index 0000000000000..9c13e936ca719 --- /dev/null +++ b/airbyte-integrations/connectors/source-shopify/source_shopify/run.py @@ -0,0 +1,15 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch + +from .source import SourceShopify + + +def run(): + source = SourceShopify() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/shopify.md b/docs/integrations/sources/shopify.md index a135de8dd621f..94172db09e12b 100644 --- a/docs/integrations/sources/shopify.md +++ b/docs/integrations/sources/shopify.md @@ -210,6 +210,7 @@ If a child stream is synced independently of its parent stream, a full sync will | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------| +| 1.1.6 | 2024-01-04 | [33414](https://github.com/airbytehq/airbyte/pull/33414) | Prepare for airbyte-lib | | 1.1.5 | 2023-12-28 | [33827](https://github.com/airbytehq/airbyte/pull/33827) | Fix GraphQL query | | 1.1.4 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | | 1.1.3 | 2023-10-17 | [31500](https://github.com/airbytehq/airbyte/pull/31500) | Fixed the issue caused by the `missing access token` while setup the new source and not yet authenticated | From e2476f2ab3d9538963d0ec0a48e0f1d9fe7c896b Mon Sep 17 00:00:00 2001 From: midavadim Date: Mon, 15 Jan 2024 14:31:11 +0200 Subject: [PATCH 091/574] :bug: CDK HttpRequester should support str http_method which comes from manifest.yaml file (#34011) --- .../sources/declarative/requesters/http_requester.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/airbyte-cdk/python/airbyte_cdk/sources/declarative/requesters/http_requester.py b/airbyte-cdk/python/airbyte_cdk/sources/declarative/requesters/http_requester.py index ce15c127686a3..20c18ec9ba6c8 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/declarative/requesters/http_requester.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/declarative/requesters/http_requester.py @@ -59,7 +59,7 @@ class HttpRequester(Requester): config: Config parameters: InitVar[Mapping[str, Any]] authenticator: Optional[DeclarativeAuthenticator] = None - http_method: HttpMethod = HttpMethod.GET + http_method: Union[str, HttpMethod] = HttpMethod.GET request_options_provider: Optional[InterpolatedRequestOptionsProvider] = None error_handler: Optional[ErrorHandler] = None disable_retries: bool = False @@ -80,6 +80,7 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None: else: self._request_options_provider = self.request_options_provider self._authenticator = self.authenticator or NoAuth(parameters=parameters) + self._http_method = HttpMethod[self.http_method] if isinstance(self.http_method, str) else self.http_method self.error_handler = self.error_handler self._parameters = parameters self.decoder = JsonDecoder(parameters={}) @@ -138,7 +139,7 @@ def get_path( return path.lstrip("/") def get_method(self) -> HttpMethod: - return self.http_method + return self._http_method def interpret_response_status(self, response: requests.Response) -> ResponseStatus: if self.error_handler is None: @@ -419,7 +420,7 @@ def _create_prepared_request( data: Any = None, ) -> requests.PreparedRequest: url = urljoin(self.get_url_base(), path) - http_method = str(self.http_method.value) + http_method = str(self._http_method.value) query_params = self.deduplicate_query_params(url, params) args = {"method": http_method, "url": url, "headers": headers, "params": query_params} if http_method.upper() in BODY_REQUEST_METHODS: From 3f4074e3d3f051995ef26faa8c6ef84bfe678510 Mon Sep 17 00:00:00 2001 From: midavadim Date: Mon, 15 Jan 2024 12:50:23 +0000 Subject: [PATCH 092/574] =?UTF-8?q?=F0=9F=A4=96=20Bump=20patch=20version?= =?UTF-8?q?=20of=20Python=20CDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airbyte-cdk/python/.bumpversion.cfg | 2 +- airbyte-cdk/python/CHANGELOG.md | 3 +++ airbyte-cdk/python/Dockerfile | 2 +- airbyte-cdk/python/setup.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/airbyte-cdk/python/.bumpversion.cfg b/airbyte-cdk/python/.bumpversion.cfg index a43a0aa292bf0..df8a9afee1000 100644 --- a/airbyte-cdk/python/.bumpversion.cfg +++ b/airbyte-cdk/python/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.58.7 +current_version = 0.58.8 commit = False [bumpversion:file:setup.py] diff --git a/airbyte-cdk/python/CHANGELOG.md b/airbyte-cdk/python/CHANGELOG.md index 3baf17d7480b3..223e8b50e29a3 100644 --- a/airbyte-cdk/python/CHANGELOG.md +++ b/airbyte-cdk/python/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 0.58.8 +CDK: HttpRequester can accept http_method in str format, which is required by custom low code components + ## 0.58.7 diff --git a/airbyte-cdk/python/Dockerfile b/airbyte-cdk/python/Dockerfile index 14c33a7d72a01..7494661031ee0 100644 --- a/airbyte-cdk/python/Dockerfile +++ b/airbyte-cdk/python/Dockerfile @@ -32,5 +32,5 @@ ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] # needs to be the same as CDK -LABEL io.airbyte.version=0.58.7 +LABEL io.airbyte.version=0.58.8 LABEL io.airbyte.name=airbyte/source-declarative-manifest diff --git a/airbyte-cdk/python/setup.py b/airbyte-cdk/python/setup.py index 129f675e6fc0f..a5b870e690510 100644 --- a/airbyte-cdk/python/setup.py +++ b/airbyte-cdk/python/setup.py @@ -36,7 +36,7 @@ name="airbyte-cdk", # The version of the airbyte-cdk package is used at runtime to validate manifests. That validation must be # updated if our semver format changes such as using release candidate versions. - version="0.58.7", + version="0.58.8", description="A framework for writing Airbyte Connectors.", long_description=README, long_description_content_type="text/markdown", From 5380d234c50fec72a7c2d0319127aa5c746f4ed6 Mon Sep 17 00:00:00 2001 From: Daryna Ishchenko <80129833+darynaishchenko@users.noreply.github.com> Date: Mon, 15 Jan 2024 15:08:11 +0200 Subject: [PATCH 093/574] :bug: Source Google Ads: Remove metrics from ad group for manager account (#34212) --- .../acceptance-test-config.yml | 1 + .../expected_records_click.jsonl | 8 +- .../source-google-ads/metadata.yaml | 2 +- .../source_google_ads/streams.py | 15 ++ .../source-google-ads/unit_tests/conftest.py | 5 + .../unit_tests/test_streams.py | 13 +- docs/integrations/sources/google-ads.md | 199 +++++++++--------- 7 files changed, 138 insertions(+), 105 deletions(-) diff --git a/airbyte-integrations/connectors/source-google-ads/acceptance-test-config.yml b/airbyte-integrations/connectors/source-google-ads/acceptance-test-config.yml index 10b39b2b95465..8c7f037815c4d 100644 --- a/airbyte-integrations/connectors/source-google-ads/acceptance-test-config.yml +++ b/airbyte-integrations/connectors/source-google-ads/acceptance-test-config.yml @@ -123,6 +123,7 @@ acceptance_tests: tests: - config_path: "secrets/config.json" configured_catalog_path: "integration_tests/configured_catalog.json" + - config_path: "secrets/config_manager_account.json" incremental: tests: - config_path: "secrets/incremental_config.json" diff --git a/airbyte-integrations/connectors/source-google-ads/integration_tests/expected_records_click.jsonl b/airbyte-integrations/connectors/source-google-ads/integration_tests/expected_records_click.jsonl index 07fd5caa07a11..46f5b1b428ad9 100644 --- a/airbyte-integrations/connectors/source-google-ads/integration_tests/expected_records_click.jsonl +++ b/airbyte-integrations/connectors/source-google-ads/integration_tests/expected_records_click.jsonl @@ -1,7 +1,7 @@ -{"stream": "ad_group_ad_legacy", "data": {"ad_group_ad.ad.legacy_responsive_display_ad.accent_color": "", "ad_group.id": 155311392438, "customer.currency_code": "USD", "customer.descriptive_name": "Airbyte", "customer.time_zone": "America/Los_Angeles", "metrics.active_view_cpm": 0.0, "metrics.active_view_ctr": 0.0, "metrics.active_view_impressions": 0, "metrics.active_view_measurability": 0.0, "metrics.active_view_measurable_cost_micros": 0, "metrics.active_view_measurable_impressions": 0, "metrics.active_view_viewability": 0.0, "ad_group_ad.ad_group": "customers/4651612872/adGroups/155311392438", "ad_group.name": "Airbyte", "ad_group.status": "ENABLED", "segments.ad_network_type": "SEARCH", "ad_group_ad.ad_strength": "EXCELLENT", "ad_group_ad.ad.type": "RESPONSIVE_SEARCH_AD", "metrics.all_conversions_from_interactions_rate": 4.833920699999999, "metrics.all_conversions_value": 800.783622, "metrics.all_conversions": 145.017621, "ad_group_ad.ad.legacy_responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.added_by_google_ads": false, "metrics.average_cost": 5602666.666666667, "metrics.average_cpc": 5602666.666666667, "metrics.average_cpe": 0.0, "metrics.average_cpm": 1031165644.1717792, "metrics.average_cpv": 0.0, "metrics.average_page_views": 0.0, "metrics.average_time_on_site": 0.0, "ad_group.base_ad_group": "customers/4651612872/adGroups/155311392438", "campaign.base_campaign": "customers/4651612872/campaigns/20643300404", "metrics.bounce_rate": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.business_name": "", "ad_group_ad.ad.legacy_responsive_display_ad.call_to_action_text": "", "campaign.id": 20643300404, "campaign.name": "mm_search_brand", "campaign.status": "PAUSED", "metrics.clicks": 30, "ad_group_ad.policy_summary.approval_status": "APPROVED", "metrics.conversions_from_interactions_rate": 0.22079663333333333, "metrics.conversions_value": 662.3899, "metrics.conversions": 6.623899, "metrics.cost_micros": 168080000, "metrics.cost_per_all_conversions": 1159031.5634815167, "metrics.cost_per_conversion": 25374783.039415307, "metrics.cost_per_current_model_attributed_conversion": 25374783.039415307, "ad_group_ad.ad.final_mobile_urls": [], "ad_group_ad.ad.final_urls": ["https://airbyte.com/"], "ad_group_ad.ad.tracking_url_template": "", "ad_group_ad.ad.url_custom_parameters": [], "metrics.cross_device_conversions": 0.0, "metrics.ctr": 0.18404907975460122, "metrics.current_model_attributed_conversions_value": 662.3899, "metrics.current_model_attributed_conversions": 6.623899, "segments.date": "2023-12-31", "segments.day_of_week": "SUNDAY", "ad_group_ad.ad.expanded_text_ad.description": "", "ad_group_ad.ad.text_ad.description1": "", "ad_group_ad.ad.text_ad.description2": "", "ad_group_ad.ad.device_preference": "UNSPECIFIED", "ad_group_ad.ad.display_url": "", "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "ad_group_ad.ad.legacy_responsive_display_ad.logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.marketing_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_marketing_image": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description": "", "ad_group_ad.ad.expanded_text_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.headline_part3": "", "customer.id": 4651612872, "ad_group_ad.ad.legacy_responsive_display_ad.format_setting": "UNSPECIFIED", "metrics.gmail_forwards": 0, "metrics.gmail_saves": 0, "metrics.gmail_secondary_clicks": 0, "ad_group_ad.ad.text_ad.headline": "", "ad_group_ad.ad.expanded_text_ad.headline_part1": "", "ad_group_ad.ad.expanded_text_ad.headline_part2": "", "ad_group_ad.ad.id": 676665180945, "ad_group_ad.ad.image_ad.image_url": "", "ad_group_ad.ad.image_ad.pixel_height": 0, "ad_group_ad.ad.image_ad.pixel_width": 0, "ad_group_ad.ad.image_ad.mime_type": "UNSPECIFIED", "ad_group_ad.ad.image_ad.name": "", "metrics.impressions": 163, "metrics.interaction_rate": 0.18404907975460122, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interactions": 30, "ad_group_ad.ad.legacy_responsive_display_ad.long_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.main_color": "", "segments.month": "2023-12-01", "ad_group_ad.ad.responsive_display_ad.accent_color": "", "ad_group_ad.ad.responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.responsive_display_ad.business_name": "", "ad_group_ad.ad.responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.responsive_display_ad.descriptions": [], "ad_group_ad.ad.responsive_display_ad.price_prefix": "", "ad_group_ad.ad.responsive_display_ad.promo_text": "", "ad_group_ad.ad.responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.responsive_display_ad.headlines": [], "ad_group_ad.ad.responsive_display_ad.logo_images": [], "ad_group_ad.ad.responsive_display_ad.square_logo_images": [], "ad_group_ad.ad.responsive_display_ad.long_headline": "", "ad_group_ad.ad.responsive_display_ad.main_color": "", "ad_group_ad.ad.responsive_display_ad.marketing_images": [], "ad_group_ad.ad.responsive_display_ad.square_marketing_images": [], "ad_group_ad.ad.responsive_display_ad.youtube_videos": [], "ad_group_ad.ad.expanded_text_ad.path1": "", "ad_group_ad.ad.expanded_text_ad.path2": "", "metrics.percent_new_visitors": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.price_prefix": "", "ad_group_ad.ad.legacy_responsive_display_ad.promo_text": "", "segments.quarter": "2023-10-01", "ad_group_ad.ad.responsive_search_ad.descriptions": ["text: \"The most comprehensive catalog of connectors, trusted by 40,000K engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"A high-performing and scalable data integration platform with advanced features.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte for free! Connect Any Data, Any User, & Any Application Effortlessly.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build custom connectors in 10 min with our no-code connector builder.\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.headlines": ["text: \"Airbyte\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"The only ETL tool you need\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build ELT Pipelines In Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No code, ELT Tool\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Replicate Data in Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open Source Integration\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte Cloud Free\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Browse Our Catalog\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Trusted by over 40K Engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"14 Day Free Trial\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"300+ off-the-shelf connectors\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No-Code Connector Builder\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Get started in minutes\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Extract, Load & Transform\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Streamlined Data Pipeline\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.path1": "", "ad_group_ad.ad.responsive_search_ad.path2": "", "ad_group_ad.ad.legacy_responsive_display_ad.short_headline": "", "ad_group_ad.status": "ENABLED", "ad_group_ad.ad.system_managed_resource_source": "UNSPECIFIED", "metrics.top_impression_percentage": 0.8834355828220859, "ad_group_ad.ad.app_ad.descriptions": [], "ad_group_ad.ad.app_ad.headlines": [], "ad_group_ad.ad.app_ad.html5_media_bundles": [], "ad_group_ad.ad.app_ad.images": [], "ad_group_ad.ad.app_ad.mandatory_ad_text": "", "ad_group_ad.ad.app_ad.youtube_videos": [], "metrics.value_per_all_conversions": 5.52197461576066, "metrics.value_per_conversion": 100.0, "metrics.value_per_current_model_attributed_conversion": 100.0, "metrics.video_quartile_p100_rate": 0.0, "metrics.video_quartile_p25_rate": 0.0, "metrics.video_quartile_p50_rate": 0.0, "metrics.video_quartile_p75_rate": 0.0, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0, "segments.week": "2023-12-25", "segments.year": 2023}, "emitted_at": 1704716796168} -{"stream": "ad_group_ad_legacy", "data": {"ad_group_ad.ad.legacy_responsive_display_ad.accent_color": "", "ad_group.id": 155311392438, "customer.currency_code": "USD", "customer.descriptive_name": "Airbyte", "customer.time_zone": "America/Los_Angeles", "metrics.active_view_cpm": 0.0, "metrics.active_view_ctr": 0.0, "metrics.active_view_impressions": 0, "metrics.active_view_measurability": 0.0, "metrics.active_view_measurable_cost_micros": 0, "metrics.active_view_measurable_impressions": 0, "metrics.active_view_viewability": 0.0, "ad_group_ad.ad_group": "customers/4651612872/adGroups/155311392438", "ad_group.name": "Airbyte", "ad_group.status": "ENABLED", "segments.ad_network_type": "SEARCH", "ad_group_ad.ad_strength": "EXCELLENT", "ad_group_ad.ad.type": "RESPONSIVE_SEARCH_AD", "metrics.all_conversions_from_interactions_rate": 4.832213216216216, "metrics.all_conversions_value": 1254.549854, "metrics.all_conversions": 178.791889, "ad_group_ad.ad.legacy_responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.added_by_google_ads": false, "metrics.average_cost": 8890000.0, "metrics.average_cpc": 8890000.0, "metrics.average_cpe": 0.0, "metrics.average_cpm": 1279883268.4824903, "metrics.average_cpv": 0.0, "metrics.average_page_views": 0.0, "metrics.average_time_on_site": 0.0, "ad_group.base_ad_group": "customers/4651612872/adGroups/155311392438", "campaign.base_campaign": "customers/4651612872/campaigns/20643300404", "metrics.bounce_rate": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.business_name": "", "ad_group_ad.ad.legacy_responsive_display_ad.call_to_action_text": "", "campaign.id": 20643300404, "campaign.name": "mm_search_brand", "campaign.status": "PAUSED", "metrics.clicks": 37, "ad_group_ad.policy_summary.approval_status": "APPROVED", "metrics.conversions_from_interactions_rate": 0.34582527027027027, "metrics.conversions_value": 1079.5535, "metrics.conversions": 12.795535, "metrics.cost_micros": 328930000, "metrics.cost_per_all_conversions": 1839736.7008074957, "metrics.cost_per_conversion": 25706623.443255793, "metrics.cost_per_current_model_attributed_conversion": 25706623.443255793, "ad_group_ad.ad.final_mobile_urls": [], "ad_group_ad.ad.final_urls": ["https://airbyte.com/"], "ad_group_ad.ad.tracking_url_template": "", "ad_group_ad.ad.url_custom_parameters": [], "metrics.cross_device_conversions": 2.0, "metrics.ctr": 0.14396887159533073, "metrics.current_model_attributed_conversions_value": 1079.5535, "metrics.current_model_attributed_conversions": 12.795535, "segments.date": "2024-01-01", "segments.day_of_week": "MONDAY", "ad_group_ad.ad.expanded_text_ad.description": "", "ad_group_ad.ad.text_ad.description1": "", "ad_group_ad.ad.text_ad.description2": "", "ad_group_ad.ad.device_preference": "UNSPECIFIED", "ad_group_ad.ad.display_url": "", "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "ad_group_ad.ad.legacy_responsive_display_ad.logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.marketing_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_marketing_image": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description": "", "ad_group_ad.ad.expanded_text_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.headline_part3": "", "customer.id": 4651612872, "ad_group_ad.ad.legacy_responsive_display_ad.format_setting": "UNSPECIFIED", "metrics.gmail_forwards": 0, "metrics.gmail_saves": 0, "metrics.gmail_secondary_clicks": 0, "ad_group_ad.ad.text_ad.headline": "", "ad_group_ad.ad.expanded_text_ad.headline_part1": "", "ad_group_ad.ad.expanded_text_ad.headline_part2": "", "ad_group_ad.ad.id": 676665180945, "ad_group_ad.ad.image_ad.image_url": "", "ad_group_ad.ad.image_ad.pixel_height": 0, "ad_group_ad.ad.image_ad.pixel_width": 0, "ad_group_ad.ad.image_ad.mime_type": "UNSPECIFIED", "ad_group_ad.ad.image_ad.name": "", "metrics.impressions": 257, "metrics.interaction_rate": 0.14396887159533073, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interactions": 37, "ad_group_ad.ad.legacy_responsive_display_ad.long_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.main_color": "", "segments.month": "2024-01-01", "ad_group_ad.ad.responsive_display_ad.accent_color": "", "ad_group_ad.ad.responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.responsive_display_ad.business_name": "", "ad_group_ad.ad.responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.responsive_display_ad.descriptions": [], "ad_group_ad.ad.responsive_display_ad.price_prefix": "", "ad_group_ad.ad.responsive_display_ad.promo_text": "", "ad_group_ad.ad.responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.responsive_display_ad.headlines": [], "ad_group_ad.ad.responsive_display_ad.logo_images": [], "ad_group_ad.ad.responsive_display_ad.square_logo_images": [], "ad_group_ad.ad.responsive_display_ad.long_headline": "", "ad_group_ad.ad.responsive_display_ad.main_color": "", "ad_group_ad.ad.responsive_display_ad.marketing_images": [], "ad_group_ad.ad.responsive_display_ad.square_marketing_images": [], "ad_group_ad.ad.responsive_display_ad.youtube_videos": [], "ad_group_ad.ad.expanded_text_ad.path1": "", "ad_group_ad.ad.expanded_text_ad.path2": "", "metrics.percent_new_visitors": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.price_prefix": "", "ad_group_ad.ad.legacy_responsive_display_ad.promo_text": "", "segments.quarter": "2024-01-01", "ad_group_ad.ad.responsive_search_ad.descriptions": ["text: \"The most comprehensive catalog of connectors, trusted by 40,000K engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"A high-performing and scalable data integration platform with advanced features.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte for free! Connect Any Data, Any User, & Any Application Effortlessly.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build custom connectors in 10 min with our no-code connector builder.\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.headlines": ["text: \"Airbyte\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"The only ETL tool you need\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build ELT Pipelines In Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No code, ELT Tool\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Replicate Data in Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open Source Integration\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte Cloud Free\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Browse Our Catalog\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Trusted by over 40K Engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"14 Day Free Trial\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"300+ off-the-shelf connectors\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No-Code Connector Builder\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Get started in minutes\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Extract, Load & Transform\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Streamlined Data Pipeline\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.path1": "", "ad_group_ad.ad.responsive_search_ad.path2": "", "ad_group_ad.ad.legacy_responsive_display_ad.short_headline": "", "ad_group_ad.status": "ENABLED", "ad_group_ad.ad.system_managed_resource_source": "UNSPECIFIED", "metrics.top_impression_percentage": 0.9688715953307393, "ad_group_ad.ad.app_ad.descriptions": [], "ad_group_ad.ad.app_ad.headlines": [], "ad_group_ad.ad.app_ad.html5_media_bundles": [], "ad_group_ad.ad.app_ad.images": [], "ad_group_ad.ad.app_ad.mandatory_ad_text": "", "ad_group_ad.ad.app_ad.youtube_videos": [], "metrics.value_per_all_conversions": 7.016816372469783, "metrics.value_per_conversion": 84.36954765861685, "metrics.value_per_current_model_attributed_conversion": 84.36954765861685, "metrics.video_quartile_p100_rate": 0.0, "metrics.video_quartile_p25_rate": 0.0, "metrics.video_quartile_p50_rate": 0.0, "metrics.video_quartile_p75_rate": 0.0, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0, "segments.week": "2024-01-01", "segments.year": 2024}, "emitted_at": 1704716796174} -{"stream": "campaign_budget", "data": {"customer.id": 4651612872, "campaign.id": 20643300404, "campaign_budget.aligned_bidding_strategy_id": 0, "campaign_budget.amount_micros": 330000000, "campaign_budget.delivery_method": "STANDARD", "campaign_budget.explicitly_shared": false, "campaign_budget.has_recommended_budget": false, "campaign_budget.id": 13022493317, "campaign_budget.name": "mm_search_brand", "campaign_budget.period": "DAILY", "campaign_budget.recommended_budget_amount_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_clicks": 0, "campaign_budget.recommended_budget_estimated_change_weekly_cost_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_interactions": 0, "campaign_budget.recommended_budget_estimated_change_weekly_views": 0, "campaign_budget.reference_count": 1, "campaign_budget.resource_name": "customers/4651612872/campaignBudgets/13022493317", "campaign_budget.status": "ENABLED", "campaign_budget.total_amount_micros": 0, "campaign_budget.type": "STANDARD", "segments.date": "2023-12-31", "segments.budget_campaign_association_status.campaign": "customers/4651612872/campaigns/20643300404", "segments.budget_campaign_association_status.status": "ENABLED", "metrics.all_conversions": 145.017621, "metrics.all_conversions_from_interactions_rate": 4.833920699999999, "metrics.all_conversions_value": 800.783622, "metrics.average_cost": 5602666.666666667, "metrics.average_cpc": 5602666.666666667, "metrics.average_cpe": 0.0, "metrics.average_cpm": 1031165644.1717792, "metrics.average_cpv": 0.0, "metrics.clicks": 30, "metrics.conversions": 6.623899, "metrics.conversions_from_interactions_rate": 0.22079663333333333, "metrics.conversions_value": 662.3899, "metrics.cost_micros": 168080000, "metrics.cost_per_all_conversions": 1159031.5634815167, "metrics.cost_per_conversion": 25374783.039415307, "metrics.cross_device_conversions": 0.0, "metrics.ctr": 0.18404907975460122, "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "metrics.impressions": 163, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interaction_rate": 0.18404907975460122, "metrics.interactions": 30, "metrics.value_per_all_conversions": 5.52197461576066, "metrics.value_per_conversion": 100.0, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0}, "emitted_at": 1704717423823} -{"stream": "campaign_budget", "data": {"customer.id": 4651612872, "campaign.id": 20643300404, "campaign_budget.aligned_bidding_strategy_id": 0, "campaign_budget.amount_micros": 330000000, "campaign_budget.delivery_method": "STANDARD", "campaign_budget.explicitly_shared": false, "campaign_budget.has_recommended_budget": false, "campaign_budget.id": 13022493317, "campaign_budget.name": "mm_search_brand", "campaign_budget.period": "DAILY", "campaign_budget.recommended_budget_amount_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_clicks": 0, "campaign_budget.recommended_budget_estimated_change_weekly_cost_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_interactions": 0, "campaign_budget.recommended_budget_estimated_change_weekly_views": 0, "campaign_budget.reference_count": 1, "campaign_budget.resource_name": "customers/4651612872/campaignBudgets/13022493317", "campaign_budget.status": "ENABLED", "campaign_budget.total_amount_micros": 0, "campaign_budget.type": "STANDARD", "segments.date": "2024-01-01", "segments.budget_campaign_association_status.campaign": "customers/4651612872/campaigns/20643300404", "segments.budget_campaign_association_status.status": "ENABLED", "metrics.all_conversions": 178.791889, "metrics.all_conversions_from_interactions_rate": 4.832213216216216, "metrics.all_conversions_value": 1254.549854, "metrics.average_cost": 8890000.0, "metrics.average_cpc": 8890000.0, "metrics.average_cpe": 0.0, "metrics.average_cpm": 1279883268.4824903, "metrics.average_cpv": 0.0, "metrics.clicks": 37, "metrics.conversions": 12.795535, "metrics.conversions_from_interactions_rate": 0.34582527027027027, "metrics.conversions_value": 1079.5535, "metrics.cost_micros": 328930000, "metrics.cost_per_all_conversions": 1839736.7008074957, "metrics.cost_per_conversion": 25706623.443255793, "metrics.cross_device_conversions": 2.0, "metrics.ctr": 0.14396887159533073, "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "metrics.impressions": 257, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interaction_rate": 0.14396887159533073, "metrics.interactions": 37, "metrics.value_per_all_conversions": 7.016816372469783, "metrics.value_per_conversion": 84.36954765861685, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0}, "emitted_at": 1704717423824} +{"stream": "ad_group_ad_legacy", "data": {"ad_group_ad.ad.legacy_responsive_display_ad.accent_color": "", "ad_group.id": 155311392438, "customer.currency_code": "USD", "customer.descriptive_name": "Airbyte", "customer.time_zone": "America/Los_Angeles", "metrics.active_view_cpm": 0.0, "metrics.active_view_ctr": 0.0, "metrics.active_view_impressions": 0, "metrics.active_view_measurability": 0.0, "metrics.active_view_measurable_cost_micros": 0, "metrics.active_view_measurable_impressions": 0, "metrics.active_view_viewability": 0.0, "ad_group_ad.ad_group": "customers/4651612872/adGroups/155311392438", "ad_group.name": "Airbyte", "ad_group.status": "ENABLED", "segments.ad_network_type": "SEARCH", "ad_group_ad.ad_strength": "EXCELLENT", "ad_group_ad.ad.type": "RESPONSIVE_SEARCH_AD", "metrics.all_conversions_from_interactions_rate": 4.9339207, "metrics.all_conversions_value": 803.783622, "metrics.all_conversions": 148.017621, "ad_group_ad.ad.legacy_responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.added_by_google_ads": false, "metrics.average_cost": 5602666.666666667, "metrics.average_cpc": 5602666.666666667, "metrics.average_cpe": 0.0, "metrics.average_cpm": 1031165644.1717792, "metrics.average_cpv": 0.0, "metrics.average_page_views": 0.0, "metrics.average_time_on_site": 0.0, "ad_group.base_ad_group": "customers/4651612872/adGroups/155311392438", "campaign.base_campaign": "customers/4651612872/campaigns/20643300404", "metrics.bounce_rate": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.business_name": "", "ad_group_ad.ad.legacy_responsive_display_ad.call_to_action_text": "", "campaign.id": 20643300404, "campaign.name": "mm_search_brand", "campaign.status": "PAUSED", "metrics.clicks": 30, "ad_group_ad.policy_summary.approval_status": "APPROVED", "metrics.conversions_from_interactions_rate": 0.22079663333333333, "metrics.conversions_value": 662.3899, "metrics.conversions": 6.623899, "metrics.cost_micros": 168080000, "metrics.cost_per_all_conversions": 1135540.4773057392, "metrics.cost_per_conversion": 25374783.039415307, "metrics.cost_per_current_model_attributed_conversion": 25374783.039415307, "ad_group_ad.ad.final_mobile_urls": [], "ad_group_ad.ad.final_urls": ["https://airbyte.com/"], "ad_group_ad.ad.tracking_url_template": "", "ad_group_ad.ad.url_custom_parameters": [], "metrics.cross_device_conversions": 0.0, "metrics.ctr": 0.18404907975460122, "metrics.current_model_attributed_conversions_value": 662.3899, "metrics.current_model_attributed_conversions": 6.623899, "segments.date": "2023-12-31", "segments.day_of_week": "SUNDAY", "ad_group_ad.ad.expanded_text_ad.description": "", "ad_group_ad.ad.text_ad.description1": "", "ad_group_ad.ad.text_ad.description2": "", "ad_group_ad.ad.device_preference": "UNSPECIFIED", "ad_group_ad.ad.display_url": "", "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "ad_group_ad.ad.legacy_responsive_display_ad.logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.marketing_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_marketing_image": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description": "", "ad_group_ad.ad.expanded_text_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.headline_part3": "", "customer.id": 4651612872, "ad_group_ad.ad.legacy_responsive_display_ad.format_setting": "UNSPECIFIED", "metrics.gmail_forwards": 0, "metrics.gmail_saves": 0, "metrics.gmail_secondary_clicks": 0, "ad_group_ad.ad.text_ad.headline": "", "ad_group_ad.ad.expanded_text_ad.headline_part1": "", "ad_group_ad.ad.expanded_text_ad.headline_part2": "", "ad_group_ad.ad.id": 676665180945, "ad_group_ad.ad.image_ad.image_url": "", "ad_group_ad.ad.image_ad.pixel_height": 0, "ad_group_ad.ad.image_ad.pixel_width": 0, "ad_group_ad.ad.image_ad.mime_type": "UNSPECIFIED", "ad_group_ad.ad.image_ad.name": "", "metrics.impressions": 163, "metrics.interaction_rate": 0.18404907975460122, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interactions": 30, "ad_group_ad.ad.legacy_responsive_display_ad.long_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.main_color": "", "segments.month": "2023-12-01", "ad_group_ad.ad.responsive_display_ad.accent_color": "", "ad_group_ad.ad.responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.responsive_display_ad.business_name": "", "ad_group_ad.ad.responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.responsive_display_ad.descriptions": [], "ad_group_ad.ad.responsive_display_ad.price_prefix": "", "ad_group_ad.ad.responsive_display_ad.promo_text": "", "ad_group_ad.ad.responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.responsive_display_ad.headlines": [], "ad_group_ad.ad.responsive_display_ad.logo_images": [], "ad_group_ad.ad.responsive_display_ad.square_logo_images": [], "ad_group_ad.ad.responsive_display_ad.long_headline": "", "ad_group_ad.ad.responsive_display_ad.main_color": "", "ad_group_ad.ad.responsive_display_ad.marketing_images": [], "ad_group_ad.ad.responsive_display_ad.square_marketing_images": [], "ad_group_ad.ad.responsive_display_ad.youtube_videos": [], "ad_group_ad.ad.expanded_text_ad.path1": "", "ad_group_ad.ad.expanded_text_ad.path2": "", "metrics.percent_new_visitors": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.price_prefix": "", "ad_group_ad.ad.legacy_responsive_display_ad.promo_text": "", "segments.quarter": "2023-10-01", "ad_group_ad.ad.responsive_search_ad.descriptions": ["text: \"The most comprehensive catalog of connectors, trusted by 40,000K engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"A high-performing and scalable data integration platform with advanced features.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte for free! Connect Any Data, Any User, & Any Application Effortlessly.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build custom connectors in 10 min with our no-code connector builder.\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.headlines": ["text: \"Airbyte\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"The only ETL tool you need\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build ELT Pipelines In Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No code, ELT Tool\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Replicate Data in Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open Source Integration\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte Cloud Free\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Browse Our Catalog\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Trusted by over 40K Engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"14 Day Free Trial\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"300+ off-the-shelf connectors\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No-Code Connector Builder\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Get started in minutes\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Extract, Load & Transform\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Streamlined Data Pipeline\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.path1": "", "ad_group_ad.ad.responsive_search_ad.path2": "", "ad_group_ad.ad.legacy_responsive_display_ad.short_headline": "", "ad_group_ad.status": "ENABLED", "ad_group_ad.ad.system_managed_resource_source": "UNSPECIFIED", "metrics.top_impression_percentage": 0.8834355828220859, "ad_group_ad.ad.app_ad.descriptions": [], "ad_group_ad.ad.app_ad.headlines": [], "ad_group_ad.ad.app_ad.html5_media_bundles": [], "ad_group_ad.ad.app_ad.images": [], "ad_group_ad.ad.app_ad.mandatory_ad_text": "", "ad_group_ad.ad.app_ad.youtube_videos": [], "metrics.value_per_all_conversions": 5.430323880154783, "metrics.value_per_conversion": 100.0, "metrics.value_per_current_model_attributed_conversion": 100.0, "metrics.video_quartile_p100_rate": 0.0, "metrics.video_quartile_p25_rate": 0.0, "metrics.video_quartile_p50_rate": 0.0, "metrics.video_quartile_p75_rate": 0.0, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0, "segments.week": "2023-12-25", "segments.year": 2023}, "emitted_at": 1705321896341} +{"stream": "ad_group_ad_legacy", "data": {"ad_group_ad.ad.legacy_responsive_display_ad.accent_color": "", "ad_group.id": 155311392438, "customer.currency_code": "USD", "customer.descriptive_name": "Airbyte", "customer.time_zone": "America/Los_Angeles", "metrics.active_view_cpm": 0.0, "metrics.active_view_ctr": 0.0, "metrics.active_view_impressions": 0, "metrics.active_view_measurability": 0.0, "metrics.active_view_measurable_cost_micros": 0, "metrics.active_view_measurable_impressions": 0, "metrics.active_view_viewability": 0.0, "ad_group_ad.ad_group": "customers/4651612872/adGroups/155311392438", "ad_group.name": "Airbyte", "ad_group.status": "ENABLED", "segments.ad_network_type": "SEARCH", "ad_group_ad.ad_strength": "EXCELLENT", "ad_group_ad.ad.type": "RESPONSIVE_SEARCH_AD", "metrics.all_conversions_from_interactions_rate": 5.129510513513513, "metrics.all_conversions_value": 1315.049854, "metrics.all_conversions": 189.791889, "ad_group_ad.ad.legacy_responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.added_by_google_ads": false, "metrics.average_cost": 8890000.0, "metrics.average_cpc": 8890000.0, "metrics.average_cpe": 0.0, "metrics.average_cpm": 1279883268.4824903, "metrics.average_cpv": 0.0, "metrics.average_page_views": 0.0, "metrics.average_time_on_site": 0.0, "ad_group.base_ad_group": "customers/4651612872/adGroups/155311392438", "campaign.base_campaign": "customers/4651612872/campaigns/20643300404", "metrics.bounce_rate": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.business_name": "", "ad_group_ad.ad.legacy_responsive_display_ad.call_to_action_text": "", "campaign.id": 20643300404, "campaign.name": "mm_search_brand", "campaign.status": "PAUSED", "metrics.clicks": 37, "ad_group_ad.policy_summary.approval_status": "APPROVED", "metrics.conversions_from_interactions_rate": 0.35933878378378376, "metrics.conversions_value": 1129.5535, "metrics.conversions": 13.295535, "metrics.cost_micros": 328930000, "metrics.cost_per_all_conversions": 1733108.8369113603, "metrics.cost_per_conversion": 24739884.480015285, "metrics.cost_per_current_model_attributed_conversion": 24739884.480015285, "ad_group_ad.ad.final_mobile_urls": [], "ad_group_ad.ad.final_urls": ["https://airbyte.com/"], "ad_group_ad.ad.tracking_url_template": "", "ad_group_ad.ad.url_custom_parameters": [], "metrics.cross_device_conversions": 3.0, "metrics.ctr": 0.14396887159533073, "metrics.current_model_attributed_conversions_value": 1129.5535, "metrics.current_model_attributed_conversions": 13.295535, "segments.date": "2024-01-01", "segments.day_of_week": "MONDAY", "ad_group_ad.ad.expanded_text_ad.description": "", "ad_group_ad.ad.text_ad.description1": "", "ad_group_ad.ad.text_ad.description2": "", "ad_group_ad.ad.device_preference": "UNSPECIFIED", "ad_group_ad.ad.display_url": "", "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "ad_group_ad.ad.legacy_responsive_display_ad.logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_logo_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.marketing_image": "", "ad_group_ad.ad.legacy_responsive_display_ad.square_marketing_image": "", "ad_group_ad.ad.expanded_dynamic_search_ad.description": "", "ad_group_ad.ad.expanded_text_ad.description2": "", "ad_group_ad.ad.expanded_text_ad.headline_part3": "", "customer.id": 4651612872, "ad_group_ad.ad.legacy_responsive_display_ad.format_setting": "UNSPECIFIED", "metrics.gmail_forwards": 0, "metrics.gmail_saves": 0, "metrics.gmail_secondary_clicks": 0, "ad_group_ad.ad.text_ad.headline": "", "ad_group_ad.ad.expanded_text_ad.headline_part1": "", "ad_group_ad.ad.expanded_text_ad.headline_part2": "", "ad_group_ad.ad.id": 676665180945, "ad_group_ad.ad.image_ad.image_url": "", "ad_group_ad.ad.image_ad.pixel_height": 0, "ad_group_ad.ad.image_ad.pixel_width": 0, "ad_group_ad.ad.image_ad.mime_type": "UNSPECIFIED", "ad_group_ad.ad.image_ad.name": "", "metrics.impressions": 257, "metrics.interaction_rate": 0.14396887159533073, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interactions": 37, "ad_group_ad.ad.legacy_responsive_display_ad.long_headline": "", "ad_group_ad.ad.legacy_responsive_display_ad.main_color": "", "segments.month": "2024-01-01", "ad_group_ad.ad.responsive_display_ad.accent_color": "", "ad_group_ad.ad.responsive_display_ad.allow_flexible_color": false, "ad_group_ad.ad.responsive_display_ad.business_name": "", "ad_group_ad.ad.responsive_display_ad.call_to_action_text": "", "ad_group_ad.ad.responsive_display_ad.descriptions": [], "ad_group_ad.ad.responsive_display_ad.price_prefix": "", "ad_group_ad.ad.responsive_display_ad.promo_text": "", "ad_group_ad.ad.responsive_display_ad.format_setting": "UNSPECIFIED", "ad_group_ad.ad.responsive_display_ad.headlines": [], "ad_group_ad.ad.responsive_display_ad.logo_images": [], "ad_group_ad.ad.responsive_display_ad.square_logo_images": [], "ad_group_ad.ad.responsive_display_ad.long_headline": "", "ad_group_ad.ad.responsive_display_ad.main_color": "", "ad_group_ad.ad.responsive_display_ad.marketing_images": [], "ad_group_ad.ad.responsive_display_ad.square_marketing_images": [], "ad_group_ad.ad.responsive_display_ad.youtube_videos": [], "ad_group_ad.ad.expanded_text_ad.path1": "", "ad_group_ad.ad.expanded_text_ad.path2": "", "metrics.percent_new_visitors": 0.0, "ad_group_ad.ad.legacy_responsive_display_ad.price_prefix": "", "ad_group_ad.ad.legacy_responsive_display_ad.promo_text": "", "segments.quarter": "2024-01-01", "ad_group_ad.ad.responsive_search_ad.descriptions": ["text: \"The most comprehensive catalog of connectors, trusted by 40,000K engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"A high-performing and scalable data integration platform with advanced features.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte for free! Connect Any Data, Any User, & Any Application Effortlessly.\"\nasset_performance_label: BEST\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build custom connectors in 10 min with our no-code connector builder.\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.headlines": ["text: \"Airbyte\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"The only ETL tool you need\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Build ELT Pipelines In Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No code, ELT Tool\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Replicate Data in Minutes\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Open Source Integration\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Try Airbyte Cloud Free\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Browse Our Catalog\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Trusted by over 40K Engineers\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"14 Day Free Trial\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"300+ off-the-shelf connectors\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"No-Code Connector Builder\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Get started in minutes\"\nasset_performance_label: GOOD\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Extract, Load & Transform\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n", "text: \"Streamlined Data Pipeline\"\nasset_performance_label: LEARNING\npolicy_summary_info {\n review_status: REVIEWED\n approval_status: APPROVED\n}\n"], "ad_group_ad.ad.responsive_search_ad.path1": "", "ad_group_ad.ad.responsive_search_ad.path2": "", "ad_group_ad.ad.legacy_responsive_display_ad.short_headline": "", "ad_group_ad.status": "ENABLED", "ad_group_ad.ad.system_managed_resource_source": "UNSPECIFIED", "metrics.top_impression_percentage": 0.9688715953307393, "ad_group_ad.ad.app_ad.descriptions": [], "ad_group_ad.ad.app_ad.headlines": [], "ad_group_ad.ad.app_ad.html5_media_bundles": [], "ad_group_ad.ad.app_ad.images": [], "ad_group_ad.ad.app_ad.mandatory_ad_text": "", "ad_group_ad.ad.app_ad.youtube_videos": [], "metrics.value_per_all_conversions": 6.928904395909143, "metrics.value_per_conversion": 84.9573559845467, "metrics.value_per_current_model_attributed_conversion": 84.9573559845467, "metrics.video_quartile_p100_rate": 0.0, "metrics.video_quartile_p25_rate": 0.0, "metrics.video_quartile_p50_rate": 0.0, "metrics.video_quartile_p75_rate": 0.0, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0, "segments.week": "2024-01-01", "segments.year": 2024}, "emitted_at": 1705321896352} +{"stream": "campaign_budget", "data": {"customer.id": 4651612872, "campaign.id": 20643300404, "campaign_budget.aligned_bidding_strategy_id": 0, "campaign_budget.amount_micros": 330000000, "campaign_budget.delivery_method": "STANDARD", "campaign_budget.explicitly_shared": false, "campaign_budget.has_recommended_budget": false, "campaign_budget.id": 13022493317, "campaign_budget.name": "mm_search_brand", "campaign_budget.period": "DAILY", "campaign_budget.recommended_budget_amount_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_clicks": 0, "campaign_budget.recommended_budget_estimated_change_weekly_cost_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_interactions": 0, "campaign_budget.recommended_budget_estimated_change_weekly_views": 0, "campaign_budget.reference_count": 1, "campaign_budget.resource_name": "customers/4651612872/campaignBudgets/13022493317", "campaign_budget.status": "ENABLED", "campaign_budget.total_amount_micros": 0, "campaign_budget.type": "STANDARD", "segments.date": "2023-12-31", "segments.budget_campaign_association_status.campaign": "customers/4651612872/campaigns/20643300404", "segments.budget_campaign_association_status.status": "ENABLED", "metrics.all_conversions": 148.017621, "metrics.all_conversions_from_interactions_rate": 4.9339207, "metrics.all_conversions_value": 803.783622, "metrics.average_cost": 5602666.666666667, "metrics.average_cpc": 5602666.666666667, "metrics.average_cpe": 0.0, "metrics.average_cpm": 1031165644.1717792, "metrics.average_cpv": 0.0, "metrics.clicks": 30, "metrics.conversions": 6.623899, "metrics.conversions_from_interactions_rate": 0.22079663333333333, "metrics.conversions_value": 662.3899, "metrics.cost_micros": 168080000, "metrics.cost_per_all_conversions": 1135540.4773057392, "metrics.cost_per_conversion": 25374783.039415307, "metrics.cross_device_conversions": 0.0, "metrics.ctr": 0.18404907975460122, "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "metrics.impressions": 163, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interaction_rate": 0.18404907975460122, "metrics.interactions": 30, "metrics.value_per_all_conversions": 5.430323880154783, "metrics.value_per_conversion": 100.0, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0}, "emitted_at": 1705322166925} +{"stream": "campaign_budget", "data": {"customer.id": 4651612872, "campaign.id": 20643300404, "campaign_budget.aligned_bidding_strategy_id": 0, "campaign_budget.amount_micros": 330000000, "campaign_budget.delivery_method": "STANDARD", "campaign_budget.explicitly_shared": false, "campaign_budget.has_recommended_budget": false, "campaign_budget.id": 13022493317, "campaign_budget.name": "mm_search_brand", "campaign_budget.period": "DAILY", "campaign_budget.recommended_budget_amount_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_clicks": 0, "campaign_budget.recommended_budget_estimated_change_weekly_cost_micros": 0, "campaign_budget.recommended_budget_estimated_change_weekly_interactions": 0, "campaign_budget.recommended_budget_estimated_change_weekly_views": 0, "campaign_budget.reference_count": 1, "campaign_budget.resource_name": "customers/4651612872/campaignBudgets/13022493317", "campaign_budget.status": "ENABLED", "campaign_budget.total_amount_micros": 0, "campaign_budget.type": "STANDARD", "segments.date": "2024-01-01", "segments.budget_campaign_association_status.campaign": "customers/4651612872/campaigns/20643300404", "segments.budget_campaign_association_status.status": "ENABLED", "metrics.all_conversions": 189.791889, "metrics.all_conversions_from_interactions_rate": 5.129510513513513, "metrics.all_conversions_value": 1315.049854, "metrics.average_cost": 8890000.0, "metrics.average_cpc": 8890000.0, "metrics.average_cpe": 0.0, "metrics.average_cpm": 1279883268.4824903, "metrics.average_cpv": 0.0, "metrics.clicks": 37, "metrics.conversions": 13.295535, "metrics.conversions_from_interactions_rate": 0.35933878378378376, "metrics.conversions_value": 1129.5535, "metrics.cost_micros": 328930000, "metrics.cost_per_all_conversions": 1733108.8369113603, "metrics.cost_per_conversion": 24739884.480015285, "metrics.cross_device_conversions": 3.0, "metrics.ctr": 0.14396887159533073, "metrics.engagement_rate": 0.0, "metrics.engagements": 0, "metrics.impressions": 257, "metrics.interaction_event_types": ["InteractionEventType.CLICK"], "metrics.interaction_rate": 0.14396887159533073, "metrics.interactions": 37, "metrics.value_per_all_conversions": 6.928904395909143, "metrics.value_per_conversion": 84.9573559845467, "metrics.video_view_rate": 0.0, "metrics.video_views": 0, "metrics.view_through_conversions": 0}, "emitted_at": 1705322166935} {"stream": "ad_group_custom", "data": {"ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "segments.date": "2024-01-03"}, "emitted_at": 1704408105935} {"stream": "ad_group_custom", "data": {"ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n", "targeting_dimension: TOPIC\nbid_only: true\n"], "segments.date": "2024-01-03"}, "emitted_at": 1704408105942} {"stream": "ad_group_custom", "data": {"ad_group.targeting_setting.target_restrictions": ["targeting_dimension: AUDIENCE\nbid_only: true\n", "targeting_dimension: AGE_RANGE\nbid_only: true\n", "targeting_dimension: GENDER\nbid_only: true\n", "targeting_dimension: PARENTAL_STATUS\nbid_only: true\n", "targeting_dimension: INCOME_RANGE\nbid_only: true\n"], "segments.date": "2024-01-02"}, "emitted_at": 1704408105943} diff --git a/airbyte-integrations/connectors/source-google-ads/metadata.yaml b/airbyte-integrations/connectors/source-google-ads/metadata.yaml index 9c19525503148..82d21ed64f367 100644 --- a/airbyte-integrations/connectors/source-google-ads/metadata.yaml +++ b/airbyte-integrations/connectors/source-google-ads/metadata.yaml @@ -11,7 +11,7 @@ data: connectorSubtype: api connectorType: source definitionId: 253487c0-2246-43ba-a21f-5116b20a2c50 - dockerImageTag: 3.2.1 + dockerImageTag: 3.3.0 dockerRepository: airbyte/source-google-ads documentationUrl: https://docs.airbyte.com/integrations/sources/google-ads githubIssueLabel: source-google-ads diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py b/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py index 284728394c9f5..499bceca367e8 100644 --- a/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/streams.py @@ -353,6 +353,21 @@ class AdGroup(IncrementalGoogleAdsStream): primary_key = ["ad_group.id", "segments.date"] + def get_query(self, stream_slice: Mapping[str, Any] = None) -> str: + fields = GoogleAds.get_fields_from_schema(self.get_json_schema()) + # validation that the customer is not a manager + # due to unsupported metrics.cost_micros field and removing it in case custom is a manager + if [customer for customer in self.customers if customer.id == stream_slice["customer_id"]][0].is_manager_account: + fields = [field for field in fields if field != "metrics.cost_micros"] + table_name = get_resource_name(self.name) + start_date, end_date = stream_slice.get("start_date"), stream_slice.get("end_date") + cursor_condition = [f"{self.cursor_field} >= '{start_date}' AND {self.cursor_field} <= '{end_date}'"] + + query = GoogleAds.convert_schema_into_query( + fields=fields, table_name=table_name, conditions=cursor_condition, order_field=self.cursor_field + ) + return query + class AdGroupLabel(GoogleAdsStream): """ diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/conftest.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/conftest.py index 859c7b31d81ff..0ed82b8024ec3 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/conftest.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/conftest.py @@ -56,3 +56,8 @@ def mock_oauth_call(requests_mock): @pytest.fixture def customers(config): return [CustomerModel(id=_id, time_zone="local", is_manager_account=False) for _id in config["customer_id"].split(",")] + + +@pytest.fixture +def customers_manager(config): + return [CustomerModel(id=_id, time_zone="local", is_manager_account=True) for _id in config["customer_id"].split(",")] diff --git a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_streams.py b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_streams.py index 3323a6811a232..a171b869d3d22 100644 --- a/airbyte-integrations/connectors/source-google-ads/unit_tests/test_streams.py +++ b/airbyte-integrations/connectors/source-google-ads/unit_tests/test_streams.py @@ -14,7 +14,7 @@ from google.api_core.exceptions import DataLoss, InternalServerError, ResourceExhausted, TooManyRequests, Unauthenticated from grpc import RpcError from source_google_ads.google_ads import GoogleAds -from source_google_ads.streams import ClickView, Customer, CustomerLabel +from source_google_ads.streams import AdGroup, ClickView, Customer, CustomerLabel # EXPIRED_PAGE_TOKEN exception will be raised when page token has expired. exception = GoogleAdsException( @@ -287,3 +287,14 @@ def test_read_records_unauthenticated(mocker, customers, config): assert exc_info.value.message == ( "Authentication failed for the customer 'customer_id'. " "Please try to Re-authenticate your credentials on set up Google Ads page." ) + + +def test_ad_group_stream_query_removes_metrics_field_for_manager(customers_manager, customers, config): + credentials = config["credentials"] + api = GoogleAds(credentials=credentials) + stream_config = dict(api=api, customers=customers_manager, start_date="2020-01-01", conversion_window_days=10) + stream = AdGroup(**stream_config) + assert "metrics" not in stream.get_query(stream_slice={"customer_id": "123"}) + stream_config = dict(api=api, customers=customers, start_date="2020-01-01", conversion_window_days=10) + stream = AdGroup(**stream_config) + assert "metrics" in stream.get_query(stream_slice={"customer_id": "123"}) diff --git a/docs/integrations/sources/google-ads.md b/docs/integrations/sources/google-ads.md index 1aafb621bf8b7..c509c5dd54dcc 100644 --- a/docs/integrations/sources/google-ads.md +++ b/docs/integrations/sources/google-ads.md @@ -278,102 +278,103 @@ Due to a limitation in the Google Ads API which does not allow getting performan ## Changelog -| Version | Date | Pull Request | Subject | -|:---------|:-----------|:---------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------| -| `3.2.1` | 2024-01-12 | [34200](https://github.com/airbytehq/airbyte/pull/34200) | Disable raising error for not enabled accounts | -| `3.2.0` | 2024-01-09 | [33707](https://github.com/airbytehq/airbyte/pull/33707) | Add possibility to sync all connected accounts | -| `3.1.0` | 2024-01-09 | [33603](https://github.com/airbytehq/airbyte/pull/33603) | Fix two issues in the custom queries: automatic addition of `segments.date` in the query; incorrect field type for `DATE` fields. | -| `3.0.2` | 2024-01-08 | [33494](https://github.com/airbytehq/airbyte/pull/33494) | Add handling for 401 error while parsing response. Add `metrics.cost_micros` field to Ad Group stream. | -| `3.0.1` | 2023-12-26 | [33769](https://github.com/airbytehq/airbyte/pull/33769) | Run a read function in a separate thread to enforce a time limit for its execution | -| `3.0.0` | 2023-12-07 | [33120](https://github.com/airbytehq/airbyte/pull/33120) | Upgrade API version to v15 | -| `2.0.4` | 2023-11-10 | [32414](https://github.com/airbytehq/airbyte/pull/32414) | Add backoff strategy for read_records method | -| `2.0.3` | 2023-11-02 | [32102](https://github.com/airbytehq/airbyte/pull/32102) | Fix incremental events streams | -| `2.0.2` | 2023-10-31 | [32001](https://github.com/airbytehq/airbyte/pull/32001) | Added handling (retry) for `InternalServerError` while reading the streams | -| `2.0.1` | 2023-10-27 | [31908](https://github.com/airbytehq/airbyte/pull/31908) | Base image migration: remove Dockerfile and use the python-connector-base image | -| `2.0.0` | 2023-10-04 | [31048](https://github.com/airbytehq/airbyte/pull/31048) | Fix schem default streams, change names of streams. | -| `1.0.0` | 2023-09-28 | [30705](https://github.com/airbytehq/airbyte/pull/30705) | Fix schemas for custom queries | -| `0.11.1` | 2023-09-26 | [30758](https://github.com/airbytehq/airbyte/pull/30758) | Exception should not be raises if a stream is not found | -| `0.11.0` | 2023-09-23 | [30704](https://github.com/airbytehq/airbyte/pull/30704) | Update error handling | -| `0.10.0` | 2023-09-19 | [30091](https://github.com/airbytehq/airbyte/pull/30091) | Fix schemas for correct primary and foreign keys | -| `0.9.0` | 2023-09-14 | [28970](https://github.com/airbytehq/airbyte/pull/28970) | Add incremental deletes for Campaign and Ad Group Criterion streams | -| `0.8.1` | 2023-09-13 | [30376](https://github.com/airbytehq/airbyte/pull/30376) | Revert pagination changes from 0.8.0 | -| `0.8.0` | 2023-09-01 | [30071](https://github.com/airbytehq/airbyte/pull/30071) | Delete start_date from required parameters and fix pagination | -| `0.7.4` | 2023-07-28 | [28832](https://github.com/airbytehq/airbyte/pull/28832) | Update field descriptions | -| `0.7.3` | 2023-07-24 | [28510](https://github.com/airbytehq/airbyte/pull/28510) | Set dates with client's timezone | -| `0.7.2` | 2023-07-20 | [28535](https://github.com/airbytehq/airbyte/pull/28535) | UI improvement: Make the query field in custom reports a multi-line string field | -| `0.7.1` | 2023-07-17 | [28365](https://github.com/airbytehq/airbyte/pull/28365) | 0.3.1 and 0.3.2 follow up: make today the end date, not yesterday | -| `0.7.0` | 2023-07-12 | [28246](https://github.com/airbytehq/airbyte/pull/28246) | Add new streams: labels, criterions, biddig strategies | -| `0.6.1` | 2023-07-12 | [28230](https://github.com/airbytehq/airbyte/pull/28230) | Reduce amount of logs produced by the connector while working with big amount of data | -| `0.6.0` | 2023-07-10 | [28078](https://github.com/airbytehq/airbyte/pull/28078) | Add new stream `Campaign Budget` | -| `0.5.0` | 2023-07-07 | [28042](https://github.com/airbytehq/airbyte/pull/28042) | Add metrics & segment to `Campaigns` stream | -| `0.4.3` | 2023-07-05 | [27959](https://github.com/airbytehq/airbyte/pull/27959) | Add `audience` and `user_interest` streams | -| `0.3.3` | 2023-07-03 | [27913](https://github.com/airbytehq/airbyte/pull/27913) | Improve Google Ads exception handling (wrong customer ID) | -| `0.3.2` | 2023-06-29 | [27835](https://github.com/airbytehq/airbyte/pull/27835) | Fix bug introduced in 0.3.1: update query template | -| `0.3.1` | 2023-06-26 | [27711](https://github.com/airbytehq/airbyte/pull/27711) | Refactor date slicing; make start date inclusive | -| `0.3.0` | 2023-06-26 | [27738](https://github.com/airbytehq/airbyte/pull/27738) | License Update: Elv2 | -| `0.2.24` | 2023-06-06 | [27608](https://github.com/airbytehq/airbyte/pull/27608) | Improve Google Ads exception handling | -| `0.2.23` | 2023-06-06 | [26905](https://github.com/airbytehq/airbyte/pull/26905) | Replace deprecated `authSpecification` in the connector specification with `advancedAuth` | -| `0.2.22` | 2023-06-02 | [26948](https://github.com/airbytehq/airbyte/pull/26948) | Refactor error messages; add `pattern_descriptor` for fields in spec | -| `0.2.21` | 2023-05-30 | [25314](https://github.com/airbytehq/airbyte/pull/25314) | Add full refresh custom table `asset_group_listing_group_filter` | -| `0.2.20` | 2023-05-30 | [25624](https://github.com/airbytehq/airbyte/pull/25624) | Add `asset` Resource to full refresh custom tables (GAQL Queries) | -| `0.2.19` | 2023-05-15 | [26209](https://github.com/airbytehq/airbyte/pull/26209) | Handle Token Refresh errors as `config_error` | -| `0.2.18` | 2023-05-15 | [25947](https://github.com/airbytehq/airbyte/pull/25947) | Improve GAQL parser error message if multiple resources provided | -| `0.2.17` | 2023-05-11 | [25987](https://github.com/airbytehq/airbyte/pull/25987) | Categorized Config Errors Accurately | -| `0.2.16` | 2023-05-10 | [25965](https://github.com/airbytehq/airbyte/pull/25965) | Fix Airbyte date-time data-types | -| `0.2.14` | 2023-03-21 | [24945](https://github.com/airbytehq/airbyte/pull/24945) | For custom google query fixed schema type for "data_type: ENUM" and "is_repeated: true" to array of strings | -| `0.2.13` | 2023-03-21 | [24338](https://github.com/airbytehq/airbyte/pull/24338) | Migrate to v13 | -| `0.2.12` | 2023-03-17 | [22985](https://github.com/airbytehq/airbyte/pull/22985) | Specified date formatting in specification | -| `0.2.11` | 2023-03-13 | [23999](https://github.com/airbytehq/airbyte/pull/23999) | Fix incremental sync for Campaigns stream | -| `0.2.10` | 2023-02-11 | [22703](https://github.com/airbytehq/airbyte/pull/22703) | Add support for custom full_refresh streams | -| `0.2.9` | 2023-01-23 | [21705](https://github.com/airbytehq/airbyte/pull/21705) | Fix multibyte issue; Bump google-ads package to 19.0.0 | -| `0.2.8` | 2023-01-18 | [21517](https://github.com/airbytehq/airbyte/pull/21517) | Write fewer logs | -| `0.2.7` | 2023-01-10 | [20755](https://github.com/airbytehq/airbyte/pull/20755) | Add more logs to debug stuck syncs | -| `0.2.6` | 2022-12-22 | [20855](https://github.com/airbytehq/airbyte/pull/20855) | Retry 429 and 5xx errors | -| `0.2.5` | 2022-11-22 | [19700](https://github.com/airbytehq/airbyte/pull/19700) | Fix schema for `campaigns` stream | -| `0.2.4` | 2022-11-09 | [19208](https://github.com/airbytehq/airbyte/pull/19208) | Add TypeTransofrmer to Campaings stream to force proper type casting | -| `0.2.3` | 2022-10-17 | [18069](https://github.com/airbytehq/airbyte/pull/18069) | Add `segments.hour`, `metrics.ctr`, `metrics.conversions` and `metrics.conversions_values` fields to `campaigns` report stream | -| `0.2.2` | 2022-10-21 | [17412](https://github.com/airbytehq/airbyte/pull/17412) | Release with CDK >= 0.2.2 | -| `0.2.1` | 2022-09-29 | [17412](https://github.com/airbytehq/airbyte/pull/17412) | Always use latest CDK version | -| `0.2.0` | 2022-08-23 | [15858](https://github.com/airbytehq/airbyte/pull/15858) | Mark the `query` and `table_name` fields in `custom_queries` as required | -| `0.1.44` | 2022-07-27 | [15084](https://github.com/airbytehq/airbyte/pull/15084) | Fix data type `ad_group_criterion.topic.path` in `display_topics_performance_report` and shifted `campaigns` to non-managers streams | -| `0.1.43` | 2022-07-12 | [14614](https://github.com/airbytehq/airbyte/pull/14614) | Update API version to `v11`, update `google-ads` to 17.0.0 | -| `0.1.42` | 2022-06-08 | [13624](https://github.com/airbytehq/airbyte/pull/13624) | Update `google-ads` to 15.1.1, pin `protobuf==3.20.0` to work on MacOS M1 machines (AMD) | -| `0.1.41` | 2022-06-08 | [13618](https://github.com/airbytehq/airbyte/pull/13618) | Add missing dependency | -| `0.1.40` | 2022-06-02 | [13423](https://github.com/airbytehq/airbyte/pull/13423) | Fix the missing data [issue](https://github.com/airbytehq/airbyte/issues/12999) | -| `0.1.39` | 2022-05-18 | [12914](https://github.com/airbytehq/airbyte/pull/12914) | Fix GAQL query validation and log auth errors instead of failing the sync | -| `0.1.38` | 2022-05-12 | [12807](https://github.com/airbytehq/airbyte/pull/12807) | Documentation updates | -| `0.1.37` | 2022-05-06 | [12651](https://github.com/airbytehq/airbyte/pull/12651) | Improve integration and unit tests | -| `0.1.36` | 2022-04-19 | [12158](https://github.com/airbytehq/airbyte/pull/12158) | Fix `*_labels` streams data type | -| `0.1.35` | 2022-04-18 | [9310](https://github.com/airbytehq/airbyte/pull/9310) | Add new fields to reports | -| `0.1.34` | 2022-03-29 | [11602](https://github.com/airbytehq/airbyte/pull/11602) | Add budget amount to campaigns stream. | -| `0.1.33` | 2022-03-29 | [11513](https://github.com/airbytehq/airbyte/pull/11513) | When `end_date` is configured in the future, use today's date instead. | -| `0.1.32` | 2022-03-24 | [11371](https://github.com/airbytehq/airbyte/pull/11371) | Improve how connection check returns error messages | -| `0.1.31` | 2022-03-23 | [11301](https://github.com/airbytehq/airbyte/pull/11301) | Update docs and spec to clarify usage | -| `0.1.30` | 2022-03-23 | [11221](https://github.com/airbytehq/airbyte/pull/11221) | Add `*_labels` streams to fetch the label text rather than their IDs | -| `0.1.29` | 2022-03-22 | [10919](https://github.com/airbytehq/airbyte/pull/10919) | Fix user location report schema and add to acceptance tests | -| `0.1.28` | 2022-02-25 | [10372](https://github.com/airbytehq/airbyte/pull/10372) | Add network fields to click view stream | -| `0.1.27` | 2022-02-16 | [10315](https://github.com/airbytehq/airbyte/pull/10315) | Make `ad_group_ads` and other streams support incremental sync. | -| `0.1.26` | 2022-02-11 | [10150](https://github.com/airbytehq/airbyte/pull/10150) | Add support for multiple customer IDs. | -| `0.1.25` | 2022-02-04 | [9812](https://github.com/airbytehq/airbyte/pull/9812) | Handle `EXPIRED_PAGE_TOKEN` exception and retry with updated state. | -| `0.1.24` | 2022-02-04 | [9996](https://github.com/airbytehq/airbyte/pull/9996) | Use Google Ads API version V9. | -| `0.1.23` | 2022-01-25 | [8669](https://github.com/airbytehq/airbyte/pull/8669) | Add end date parameter in spec. | -| `0.1.22` | 2022-01-24 | [9608](https://github.com/airbytehq/airbyte/pull/9608) | Reduce stream slice date range. | -| `0.1.21` | 2021-12-28 | [9149](https://github.com/airbytehq/airbyte/pull/9149) | Update title and description | -| `0.1.20` | 2021-12-22 | [9071](https://github.com/airbytehq/airbyte/pull/9071) | Fix: Keyword schema enum | -| `0.1.19` | 2021-12-14 | [8431](https://github.com/airbytehq/airbyte/pull/8431) | Add new streams: Geographic and Keyword | -| `0.1.18` | 2021-12-09 | [8225](https://github.com/airbytehq/airbyte/pull/8225) | Include time_zone to sync. Remove streams for manager account. | -| `0.1.16` | 2021-11-22 | [8178](https://github.com/airbytehq/airbyte/pull/8178) | Clarify setup fields | -| `0.1.15` | 2021-10-07 | [6684](https://github.com/airbytehq/airbyte/pull/6684) | Add new stream `click_view` | -| `0.1.14` | 2021-10-01 | [6565](https://github.com/airbytehq/airbyte/pull/6565) | Fix OAuth Spec File | -| `0.1.13` | 2021-09-27 | [6458](https://github.com/airbytehq/airbyte/pull/6458) | Update OAuth Spec File | -| `0.1.11` | 2021-09-22 | [6373](https://github.com/airbytehq/airbyte/pull/6373) | Fix inconsistent segments.date field type across all streams | -| `0.1.10` | 2021-09-13 | [6022](https://github.com/airbytehq/airbyte/pull/6022) | Annotate Oauth2 flow initialization parameters in connector spec | -| `0.1.9` | 2021-09-07 | [5302](https://github.com/airbytehq/airbyte/pull/5302) | Add custom query stream support | -| `0.1.8` | 2021-08-03 | [5509](https://github.com/airbytehq/airbyte/pull/5509) | Allow additionalProperties in spec.json | -| `0.1.7` | 2021-08-03 | [5422](https://github.com/airbytehq/airbyte/pull/5422) | Correct query to not skip dates | -| `0.1.6` | 2021-08-03 | [5423](https://github.com/airbytehq/airbyte/pull/5423) | Added new stream UserLocationReport | -| `0.1.5` | 2021-08-03 | [5159](https://github.com/airbytehq/airbyte/pull/5159) | Add field `login_customer_id` to spec | -| `0.1.4` | 2021-07-28 | [4962](https://github.com/airbytehq/airbyte/pull/4962) | Support new Report streams | -| `0.1.3` | 2021-07-23 | [4788](https://github.com/airbytehq/airbyte/pull/4788) | Support main streams, fix bug with exception `DATE_RANGE_TOO_NARROW` for incremental streams | -| `0.1.2` | 2021-07-06 | [4539](https://github.com/airbytehq/airbyte/pull/4539) | Add `AIRBYTE_ENTRYPOINT` for Kubernetes support | -| `0.1.1` | 2021-06-23 | [4288](https://github.com/airbytehq/airbyte/pull/4288) | Fix `Bugfix: Correctly declare required parameters` | +| Version | Date | Pull Request | Subject | +|:---------|:-----------|:---------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------| +| `3.3.0` | 2024-01-12 | [34212](https://github.com/airbytehq/airbyte/pull/34212) | Remove metric from query in Ad Group stream for non-manager account | +| `3.2.1` | 2024-01-12 | [34200](https://github.com/airbytehq/airbyte/pull/34200) | Disable raising error for not enabled accounts | +| `3.2.0` | 2024-01-09 | [33707](https://github.com/airbytehq/airbyte/pull/33707) | Add possibility to sync all connected accounts | +| `3.1.0` | 2024-01-09 | [33603](https://github.com/airbytehq/airbyte/pull/33603) | Fix two issues in the custom queries: automatic addition of `segments.date` in the query; incorrect field type for `DATE` fields. | +| `3.0.2` | 2024-01-08 | [33494](https://github.com/airbytehq/airbyte/pull/33494) | Add handling for 401 error while parsing response. Add `metrics.cost_micros` field to Ad Group stream. | +| `3.0.1` | 2023-12-26 | [33769](https://github.com/airbytehq/airbyte/pull/33769) | Run a read function in a separate thread to enforce a time limit for its execution | +| `3.0.0` | 2023-12-07 | [33120](https://github.com/airbytehq/airbyte/pull/33120) | Upgrade API version to v15 | +| `2.0.4` | 2023-11-10 | [32414](https://github.com/airbytehq/airbyte/pull/32414) | Add backoff strategy for read_records method | +| `2.0.3` | 2023-11-02 | [32102](https://github.com/airbytehq/airbyte/pull/32102) | Fix incremental events streams | +| `2.0.2` | 2023-10-31 | [32001](https://github.com/airbytehq/airbyte/pull/32001) | Added handling (retry) for `InternalServerError` while reading the streams | +| `2.0.1` | 2023-10-27 | [31908](https://github.com/airbytehq/airbyte/pull/31908) | Base image migration: remove Dockerfile and use the python-connector-base image | +| `2.0.0` | 2023-10-04 | [31048](https://github.com/airbytehq/airbyte/pull/31048) | Fix schem default streams, change names of streams. | +| `1.0.0` | 2023-09-28 | [30705](https://github.com/airbytehq/airbyte/pull/30705) | Fix schemas for custom queries | +| `0.11.1` | 2023-09-26 | [30758](https://github.com/airbytehq/airbyte/pull/30758) | Exception should not be raises if a stream is not found | +| `0.11.0` | 2023-09-23 | [30704](https://github.com/airbytehq/airbyte/pull/30704) | Update error handling | +| `0.10.0` | 2023-09-19 | [30091](https://github.com/airbytehq/airbyte/pull/30091) | Fix schemas for correct primary and foreign keys | +| `0.9.0` | 2023-09-14 | [28970](https://github.com/airbytehq/airbyte/pull/28970) | Add incremental deletes for Campaign and Ad Group Criterion streams | +| `0.8.1` | 2023-09-13 | [30376](https://github.com/airbytehq/airbyte/pull/30376) | Revert pagination changes from 0.8.0 | +| `0.8.0` | 2023-09-01 | [30071](https://github.com/airbytehq/airbyte/pull/30071) | Delete start_date from required parameters and fix pagination | +| `0.7.4` | 2023-07-28 | [28832](https://github.com/airbytehq/airbyte/pull/28832) | Update field descriptions | +| `0.7.3` | 2023-07-24 | [28510](https://github.com/airbytehq/airbyte/pull/28510) | Set dates with client's timezone | +| `0.7.2` | 2023-07-20 | [28535](https://github.com/airbytehq/airbyte/pull/28535) | UI improvement: Make the query field in custom reports a multi-line string field | +| `0.7.1` | 2023-07-17 | [28365](https://github.com/airbytehq/airbyte/pull/28365) | 0.3.1 and 0.3.2 follow up: make today the end date, not yesterday | +| `0.7.0` | 2023-07-12 | [28246](https://github.com/airbytehq/airbyte/pull/28246) | Add new streams: labels, criterions, biddig strategies | +| `0.6.1` | 2023-07-12 | [28230](https://github.com/airbytehq/airbyte/pull/28230) | Reduce amount of logs produced by the connector while working with big amount of data | +| `0.6.0` | 2023-07-10 | [28078](https://github.com/airbytehq/airbyte/pull/28078) | Add new stream `Campaign Budget` | +| `0.5.0` | 2023-07-07 | [28042](https://github.com/airbytehq/airbyte/pull/28042) | Add metrics & segment to `Campaigns` stream | +| `0.4.3` | 2023-07-05 | [27959](https://github.com/airbytehq/airbyte/pull/27959) | Add `audience` and `user_interest` streams | +| `0.3.3` | 2023-07-03 | [27913](https://github.com/airbytehq/airbyte/pull/27913) | Improve Google Ads exception handling (wrong customer ID) | +| `0.3.2` | 2023-06-29 | [27835](https://github.com/airbytehq/airbyte/pull/27835) | Fix bug introduced in 0.3.1: update query template | +| `0.3.1` | 2023-06-26 | [27711](https://github.com/airbytehq/airbyte/pull/27711) | Refactor date slicing; make start date inclusive | +| `0.3.0` | 2023-06-26 | [27738](https://github.com/airbytehq/airbyte/pull/27738) | License Update: Elv2 | +| `0.2.24` | 2023-06-06 | [27608](https://github.com/airbytehq/airbyte/pull/27608) | Improve Google Ads exception handling | +| `0.2.23` | 2023-06-06 | [26905](https://github.com/airbytehq/airbyte/pull/26905) | Replace deprecated `authSpecification` in the connector specification with `advancedAuth` | +| `0.2.22` | 2023-06-02 | [26948](https://github.com/airbytehq/airbyte/pull/26948) | Refactor error messages; add `pattern_descriptor` for fields in spec | +| `0.2.21` | 2023-05-30 | [25314](https://github.com/airbytehq/airbyte/pull/25314) | Add full refresh custom table `asset_group_listing_group_filter` | +| `0.2.20` | 2023-05-30 | [25624](https://github.com/airbytehq/airbyte/pull/25624) | Add `asset` Resource to full refresh custom tables (GAQL Queries) | +| `0.2.19` | 2023-05-15 | [26209](https://github.com/airbytehq/airbyte/pull/26209) | Handle Token Refresh errors as `config_error` | +| `0.2.18` | 2023-05-15 | [25947](https://github.com/airbytehq/airbyte/pull/25947) | Improve GAQL parser error message if multiple resources provided | +| `0.2.17` | 2023-05-11 | [25987](https://github.com/airbytehq/airbyte/pull/25987) | Categorized Config Errors Accurately | +| `0.2.16` | 2023-05-10 | [25965](https://github.com/airbytehq/airbyte/pull/25965) | Fix Airbyte date-time data-types | +| `0.2.14` | 2023-03-21 | [24945](https://github.com/airbytehq/airbyte/pull/24945) | For custom google query fixed schema type for "data_type: ENUM" and "is_repeated: true" to array of strings | +| `0.2.13` | 2023-03-21 | [24338](https://github.com/airbytehq/airbyte/pull/24338) | Migrate to v13 | +| `0.2.12` | 2023-03-17 | [22985](https://github.com/airbytehq/airbyte/pull/22985) | Specified date formatting in specification | +| `0.2.11` | 2023-03-13 | [23999](https://github.com/airbytehq/airbyte/pull/23999) | Fix incremental sync for Campaigns stream | +| `0.2.10` | 2023-02-11 | [22703](https://github.com/airbytehq/airbyte/pull/22703) | Add support for custom full_refresh streams | +| `0.2.9` | 2023-01-23 | [21705](https://github.com/airbytehq/airbyte/pull/21705) | Fix multibyte issue; Bump google-ads package to 19.0.0 | +| `0.2.8` | 2023-01-18 | [21517](https://github.com/airbytehq/airbyte/pull/21517) | Write fewer logs | +| `0.2.7` | 2023-01-10 | [20755](https://github.com/airbytehq/airbyte/pull/20755) | Add more logs to debug stuck syncs | +| `0.2.6` | 2022-12-22 | [20855](https://github.com/airbytehq/airbyte/pull/20855) | Retry 429 and 5xx errors | +| `0.2.5` | 2022-11-22 | [19700](https://github.com/airbytehq/airbyte/pull/19700) | Fix schema for `campaigns` stream | +| `0.2.4` | 2022-11-09 | [19208](https://github.com/airbytehq/airbyte/pull/19208) | Add TypeTransofrmer to Campaings stream to force proper type casting | +| `0.2.3` | 2022-10-17 | [18069](https://github.com/airbytehq/airbyte/pull/18069) | Add `segments.hour`, `metrics.ctr`, `metrics.conversions` and `metrics.conversions_values` fields to `campaigns` report stream | +| `0.2.2` | 2022-10-21 | [17412](https://github.com/airbytehq/airbyte/pull/17412) | Release with CDK >= 0.2.2 | +| `0.2.1` | 2022-09-29 | [17412](https://github.com/airbytehq/airbyte/pull/17412) | Always use latest CDK version | +| `0.2.0` | 2022-08-23 | [15858](https://github.com/airbytehq/airbyte/pull/15858) | Mark the `query` and `table_name` fields in `custom_queries` as required | +| `0.1.44` | 2022-07-27 | [15084](https://github.com/airbytehq/airbyte/pull/15084) | Fix data type `ad_group_criterion.topic.path` in `display_topics_performance_report` and shifted `campaigns` to non-managers streams | +| `0.1.43` | 2022-07-12 | [14614](https://github.com/airbytehq/airbyte/pull/14614) | Update API version to `v11`, update `google-ads` to 17.0.0 | +| `0.1.42` | 2022-06-08 | [13624](https://github.com/airbytehq/airbyte/pull/13624) | Update `google-ads` to 15.1.1, pin `protobuf==3.20.0` to work on MacOS M1 machines (AMD) | +| `0.1.41` | 2022-06-08 | [13618](https://github.com/airbytehq/airbyte/pull/13618) | Add missing dependency | +| `0.1.40` | 2022-06-02 | [13423](https://github.com/airbytehq/airbyte/pull/13423) | Fix the missing data [issue](https://github.com/airbytehq/airbyte/issues/12999) | +| `0.1.39` | 2022-05-18 | [12914](https://github.com/airbytehq/airbyte/pull/12914) | Fix GAQL query validation and log auth errors instead of failing the sync | +| `0.1.38` | 2022-05-12 | [12807](https://github.com/airbytehq/airbyte/pull/12807) | Documentation updates | +| `0.1.37` | 2022-05-06 | [12651](https://github.com/airbytehq/airbyte/pull/12651) | Improve integration and unit tests | +| `0.1.36` | 2022-04-19 | [12158](https://github.com/airbytehq/airbyte/pull/12158) | Fix `*_labels` streams data type | +| `0.1.35` | 2022-04-18 | [9310](https://github.com/airbytehq/airbyte/pull/9310) | Add new fields to reports | +| `0.1.34` | 2022-03-29 | [11602](https://github.com/airbytehq/airbyte/pull/11602) | Add budget amount to campaigns stream. | +| `0.1.33` | 2022-03-29 | [11513](https://github.com/airbytehq/airbyte/pull/11513) | When `end_date` is configured in the future, use today's date instead. | +| `0.1.32` | 2022-03-24 | [11371](https://github.com/airbytehq/airbyte/pull/11371) | Improve how connection check returns error messages | +| `0.1.31` | 2022-03-23 | [11301](https://github.com/airbytehq/airbyte/pull/11301) | Update docs and spec to clarify usage | +| `0.1.30` | 2022-03-23 | [11221](https://github.com/airbytehq/airbyte/pull/11221) | Add `*_labels` streams to fetch the label text rather than their IDs | +| `0.1.29` | 2022-03-22 | [10919](https://github.com/airbytehq/airbyte/pull/10919) | Fix user location report schema and add to acceptance tests | +| `0.1.28` | 2022-02-25 | [10372](https://github.com/airbytehq/airbyte/pull/10372) | Add network fields to click view stream | +| `0.1.27` | 2022-02-16 | [10315](https://github.com/airbytehq/airbyte/pull/10315) | Make `ad_group_ads` and other streams support incremental sync. | +| `0.1.26` | 2022-02-11 | [10150](https://github.com/airbytehq/airbyte/pull/10150) | Add support for multiple customer IDs. | +| `0.1.25` | 2022-02-04 | [9812](https://github.com/airbytehq/airbyte/pull/9812) | Handle `EXPIRED_PAGE_TOKEN` exception and retry with updated state. | +| `0.1.24` | 2022-02-04 | [9996](https://github.com/airbytehq/airbyte/pull/9996) | Use Google Ads API version V9. | +| `0.1.23` | 2022-01-25 | [8669](https://github.com/airbytehq/airbyte/pull/8669) | Add end date parameter in spec. | +| `0.1.22` | 2022-01-24 | [9608](https://github.com/airbytehq/airbyte/pull/9608) | Reduce stream slice date range. | +| `0.1.21` | 2021-12-28 | [9149](https://github.com/airbytehq/airbyte/pull/9149) | Update title and description | +| `0.1.20` | 2021-12-22 | [9071](https://github.com/airbytehq/airbyte/pull/9071) | Fix: Keyword schema enum | +| `0.1.19` | 2021-12-14 | [8431](https://github.com/airbytehq/airbyte/pull/8431) | Add new streams: Geographic and Keyword | +| `0.1.18` | 2021-12-09 | [8225](https://github.com/airbytehq/airbyte/pull/8225) | Include time_zone to sync. Remove streams for manager account. | +| `0.1.16` | 2021-11-22 | [8178](https://github.com/airbytehq/airbyte/pull/8178) | Clarify setup fields | +| `0.1.15` | 2021-10-07 | [6684](https://github.com/airbytehq/airbyte/pull/6684) | Add new stream `click_view` | +| `0.1.14` | 2021-10-01 | [6565](https://github.com/airbytehq/airbyte/pull/6565) | Fix OAuth Spec File | +| `0.1.13` | 2021-09-27 | [6458](https://github.com/airbytehq/airbyte/pull/6458) | Update OAuth Spec File | +| `0.1.11` | 2021-09-22 | [6373](https://github.com/airbytehq/airbyte/pull/6373) | Fix inconsistent segments.date field type across all streams | +| `0.1.10` | 2021-09-13 | [6022](https://github.com/airbytehq/airbyte/pull/6022) | Annotate Oauth2 flow initialization parameters in connector spec | +| `0.1.9` | 2021-09-07 | [5302](https://github.com/airbytehq/airbyte/pull/5302) | Add custom query stream support | +| `0.1.8` | 2021-08-03 | [5509](https://github.com/airbytehq/airbyte/pull/5509) | Allow additionalProperties in spec.json | +| `0.1.7` | 2021-08-03 | [5422](https://github.com/airbytehq/airbyte/pull/5422) | Correct query to not skip dates | +| `0.1.6` | 2021-08-03 | [5423](https://github.com/airbytehq/airbyte/pull/5423) | Added new stream UserLocationReport | +| `0.1.5` | 2021-08-03 | [5159](https://github.com/airbytehq/airbyte/pull/5159) | Add field `login_customer_id` to spec | +| `0.1.4` | 2021-07-28 | [4962](https://github.com/airbytehq/airbyte/pull/4962) | Support new Report streams | +| `0.1.3` | 2021-07-23 | [4788](https://github.com/airbytehq/airbyte/pull/4788) | Support main streams, fix bug with exception `DATE_RANGE_TOO_NARROW` for incremental streams | +| `0.1.2` | 2021-07-06 | [4539](https://github.com/airbytehq/airbyte/pull/4539) | Add `AIRBYTE_ENTRYPOINT` for Kubernetes support | +| `0.1.1` | 2021-06-23 | [4288](https://github.com/airbytehq/airbyte/pull/4288) | Fix `Bugfix: Correctly declare required parameters` | From 29852cf0810a8674401d4c730cea112de6156f16 Mon Sep 17 00:00:00 2001 From: Mostafa Kamal Date: Mon, 15 Jan 2024 20:41:49 +0600 Subject: [PATCH 094/574] add run method in run.py (#34241) --- .../source-python/source_{{snakeCase name}}/run.py.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-integrations/connector-templates/source-python/source_{{snakeCase name}}/run.py.hbs b/airbyte-integrations/connector-templates/source-python/source_{{snakeCase name}}/run.py.hbs index 2ac28a7c8471f..25c9400301f9b 100644 --- a/airbyte-integrations/connector-templates/source-python/source_{{snakeCase name}}/run.py.hbs +++ b/airbyte-integrations/connector-templates/source-python/source_{{snakeCase name}}/run.py.hbs @@ -8,6 +8,6 @@ import sys from airbyte_cdk.entrypoint import launch from .source import Source{{properCase name}} -if __name__ == "__main__": +def run(): source = Source{{properCase name}}() launch(source, sys.argv[1:]) From f3503ae876cf59afdc742fdf6d0303ad7b378d48 Mon Sep 17 00:00:00 2001 From: Augustin Date: Mon, 15 Jan 2024 19:09:33 +0100 Subject: [PATCH 095/574] airbyte-ci: CLI exposes CI requirements (#34218) --- airbyte-ci/connectors/pipelines/README.md | 48 +++++++++++-------- .../airbyte_ci/connectors/publish/commands.py | 2 + .../airbyte_ci/connectors/test/commands.py | 2 + .../pipelines/airbyte_ci/format/commands.py | 3 +- .../pipelines/airbyte_ci/metadata/commands.py | 2 + .../pipelines/airbyte_ci/test/commands.py | 3 +- .../pipelines/pipelines/cli/airbyte_ci.py | 10 +++- .../pipelines/cli/click_decorators.py | 31 +++++++++++- .../pipelines/models/ci_requirements.py | 33 +++++++++++++ .../connectors/pipelines/pyproject.toml | 2 +- 10 files changed, 110 insertions(+), 26 deletions(-) create mode 100644 airbyte-ci/connectors/pipelines/pipelines/models/ci_requirements.py diff --git a/airbyte-ci/connectors/pipelines/README.md b/airbyte-ci/connectors/pipelines/README.md index 552e103f2bdb7..20e7bdf6ad3ea 100644 --- a/airbyte-ci/connectors/pipelines/README.md +++ b/airbyte-ci/connectors/pipelines/README.md @@ -258,13 +258,14 @@ flowchart TD #### Options -| Option | Multiple | Default value | Description | -| ------------------- | -------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--skip-step/-x` | True | | Skip steps by id e.g. `-x unit -x acceptance` | -| `--fail-fast` | False | False | Abort after any tests fail, rather than continuing to run additional tests. Use this setting to confirm a known bug is fixed (or not), or when you only require a pass/fail result. | -| `--code-tests-only` | True | False | Skip any tests not directly related to code updates. For instance, metadata checks, version bump checks, changelog verification, etc. Use this setting to help focus on code quality during development. | -| `--concurrent-cat` | False | False | Make CAT tests run concurrently using pytest-xdist. Be careful about source or destination API rate limits. | -| `--.=` | True | | You can pass extra parameters for specific test steps. More details in the extra parameters section below | +| Option | Multiple | Default value | Description | +| ------------------------------------------------------- | -------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--skip-step/-x` | True | | Skip steps by id e.g. `-x unit -x acceptance` | +| `--fail-fast` | False | False | Abort after any tests fail, rather than continuing to run additional tests. Use this setting to confirm a known bug is fixed (or not), or when you only require a pass/fail result. | +| `--code-tests-only` | True | False | Skip any tests not directly related to code updates. For instance, metadata checks, version bump checks, changelog verification, etc. Use this setting to help focus on code quality during development. | +| `--concurrent-cat` | False | False | Make CAT tests run concurrently using pytest-xdist. Be careful about source or destination API rate limits. | +| `--.=` | True | | You can pass extra parameters for specific test steps. More details in the extra parameters section below | +| `--ci-requirements` | False | | | Output the CI requirements as a JSON payload. It is used to determine the CI runner to use. Note: @@ -370,6 +371,8 @@ Publish all connectors modified in the head commit: `airbyte-ci connectors --mod | `--metadata-service-bucket-name` | False | | `METADATA_SERVICE_BUCKET_NAME` | The name of the GCS bucket where metadata files will be uploaded. | | `--slack-webhook` | False | | `SLACK_WEBHOOK` | The Slack webhook URL to send notifications to. | | `--slack-channel` | False | | `SLACK_CHANNEL` | The Slack channel name to send notifications to. | +| `--ci-requirements` | False | | | Output the CI requirements as a JSON payload. It is used to determine the CI runner to use. | + I've added an empty "Default" column, and you can fill in the default values as needed. @@ -462,9 +465,10 @@ Available commands: ### Options -| Option | Required | Default | Mapped environment variable | Description | -| ------------ | -------- | ------- | --------------------------- | ---------------------------------------------- | -| `--quiet/-q` | False | False | | Hide formatter execution details in reporting. | +| Option | Required | Default | Mapped environment variable | Description | +| ------------------- | -------- | ------- | --------------------------- | ------------------------------------------------------------------------------------------- | +| `--quiet/-q` | False | False | | Hide formatter execution details in reporting. | +| `--ci-requirements` | False | | | Output the CI requirements as a JSON payload. It is used to determine the CI runner to use. | ### Examples @@ -517,9 +521,10 @@ This command runs the Python tests for a airbyte-ci poetry package. #### Options -| Option | Required | Default | Mapped environment variable | Description | -| ------------------------- | -------- | ------- | --------------------------- | ------------------------------------ | -| `-c/--poetry-run-command` | True | None | | The command to run with `poetry run` | +| Option | Required | Default | Mapped environment variable | Description | +| ------------------------- | -------- | ------- | --------------------------- | ------------------------------------------------------------------------------------------- | +| `-c/--poetry-run-command` | True | None | | The command to run with `poetry run` | +| `--ci-requirements` | False | | | Output the CI requirements as a JSON payload. It is used to determine the CI runner to use. | #### Examples You can pass multiple `-c/--poetry-run-command` options to run multiple commands. @@ -534,14 +539,15 @@ E.G.: running `pytest` on a specific test folder: | Version | PR | Description | | ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| 3.2.0 | [#34050](https://github.com/airbytehq/airbyte/pull/34050) | Connector test steps can take extra parameters | -| 3.1.3 | [#34136](https://github.com/airbytehq/airbyte/pull/34136) | Fix issue where dagger excludes were not being properly applied | -| 3.1.2 | [#33972](https://github.com/airbytehq/airbyte/pull/33972) | Remove secrets scrubbing hack for --is-local and other small tweaks. | -| 3.1.1 | [#33979](https://github.com/airbytehq/airbyte/pull/33979) | Fix AssertionError on report existence again | -| 3.1.0 | [#33994](https://github.com/airbytehq/airbyte/pull/33994) | Log more context information in CI. | -| 3.0.2 | [#33987](https://github.com/airbytehq/airbyte/pull/33987) | Fix type checking issue when running --help | -| 3.0.1 | [#33981](https://github.com/airbytehq/airbyte/pull/33981) | Fix issues with deploying dagster, pin pendulum version in dagster-cli install | -| 3.0.0 | [#33582](https://github.com/airbytehq/airbyte/pull/33582) | Upgrade to Dagger 0.9.5 | +| 3.3.0 | [#34218](https://github.com/airbytehq/airbyte/pull/34218) | Introduce `--ci-requirements` option for client defined CI runners. | +| 3.2.0 | [#34050](https://github.com/airbytehq/airbyte/pull/34050) | Connector test steps can take extra parameters | +| 3.1.3 | [#34136](https://github.com/airbytehq/airbyte/pull/34136) | Fix issue where dagger excludes were not being properly applied | +| 3.1.2 | [#33972](https://github.com/airbytehq/airbyte/pull/33972) | Remove secrets scrubbing hack for --is-local and other small tweaks. | +| 3.1.1 | [#33979](https://github.com/airbytehq/airbyte/pull/33979) | Fix AssertionError on report existence again | +| 3.1.0 | [#33994](https://github.com/airbytehq/airbyte/pull/33994) | Log more context information in CI. | +| 3.0.2 | [#33987](https://github.com/airbytehq/airbyte/pull/33987) | Fix type checking issue when running --help | +| 3.0.1 | [#33981](https://github.com/airbytehq/airbyte/pull/33981) | Fix issues with deploying dagster, pin pendulum version in dagster-cli install | +| 3.0.0 | [#33582](https://github.com/airbytehq/airbyte/pull/33582) | Upgrade to Dagger 0.9.5 | | 2.14.3 | [#33964](https://github.com/airbytehq/airbyte/pull/33964) | Reintroduce mypy with fixes for AssertionError on publish and missing report URL on connector test commit status. | | 2.14.2 | [#33954](https://github.com/airbytehq/airbyte/pull/33954) | Revert mypy changes | | 2.14.1 | [#33956](https://github.com/airbytehq/airbyte/pull/33956) | Exclude pnpm lock files from auto-formatting | diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/publish/commands.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/publish/commands.py index e57b930304b95..0de1d7a2032fa 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/publish/commands.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/publish/commands.py @@ -8,6 +8,7 @@ from pipelines.airbyte_ci.connectors.pipeline import run_connectors_pipelines from pipelines.airbyte_ci.connectors.publish.context import PublishConnectorContext from pipelines.airbyte_ci.connectors.publish.pipeline import reorder_contexts, run_connector_publish_pipeline +from pipelines.cli.click_decorators import click_ci_requirements_option from pipelines.cli.confirm_prompt import confirm from pipelines.cli.dagger_pipeline_command import DaggerPipelineCommand from pipelines.consts import ContextState @@ -15,6 +16,7 @@ @click.command(cls=DaggerPipelineCommand, help="Publish all images for the selected connectors.") +@click_ci_requirements_option() @click.option("--pre-release/--main-release", help="Use this flag if you want to publish pre-release images.", default=True, type=bool) @click.option( "--spec-cache-gcs-credentials", diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/commands.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/commands.py index 07f48bc5ca751..9d97f0c90a7bf 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/commands.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/commands.py @@ -11,6 +11,7 @@ from pipelines.airbyte_ci.connectors.context import ConnectorContext from pipelines.airbyte_ci.connectors.pipeline import run_connectors_pipelines from pipelines.airbyte_ci.connectors.test.pipeline import run_connector_test_pipeline +from pipelines.cli.click_decorators import click_ci_requirements_option from pipelines.cli.dagger_pipeline_command import DaggerPipelineCommand from pipelines.consts import LOCAL_BUILD_PLATFORM, ContextState from pipelines.helpers.execution import argument_parsing @@ -27,6 +28,7 @@ ignore_unknown_options=True, ), ) +@click_ci_requirements_option() @click.option( "--code-tests-only", is_flag=True, diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/format/commands.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/format/commands.py index 59e84e10c4aa5..a1f2d3cc613f5 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/format/commands.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/format/commands.py @@ -14,7 +14,7 @@ import asyncclick as click from pipelines.airbyte_ci.format.configuration import FORMATTERS_CONFIGURATIONS, Formatter from pipelines.airbyte_ci.format.format_command import FormatCommand -from pipelines.cli.click_decorators import click_ignore_unused_kwargs, click_merge_args_into_context_obj +from pipelines.cli.click_decorators import click_ci_requirements_option, click_ignore_unused_kwargs, click_merge_args_into_context_obj from pipelines.helpers.cli import LogOptions, invoke_commands_concurrently, invoke_commands_sequentially, log_command_results from pipelines.models.contexts.click_pipeline_context import ClickPipelineContext, pass_pipeline_context from pipelines.models.steps import StepStatus @@ -25,6 +25,7 @@ help="Commands related to formatting.", ) @click.option("--quiet", "-q", help="Hide details of the formatter execution.", default=False, is_flag=True) +@click_ci_requirements_option() @click_merge_args_into_context_obj @pass_pipeline_context @click_ignore_unused_kwargs diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/commands.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/commands.py index 302da3b11c1bd..ca856d9bbb67e 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/commands.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/commands.py @@ -3,12 +3,14 @@ # import asyncclick as click +from pipelines.cli.click_decorators import click_ci_requirements_option from pipelines.cli.dagger_pipeline_command import DaggerPipelineCommand # MAIN GROUP @click.group(help="Commands related to the metadata service.") +@click_ci_requirements_option() @click.pass_context def metadata(ctx: click.Context) -> None: pass diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/test/commands.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/test/commands.py index 0ea67650eb192..7bf140211e78f 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/test/commands.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/test/commands.py @@ -9,7 +9,7 @@ import asyncclick as click import asyncer -from pipelines.cli.click_decorators import click_ignore_unused_kwargs, click_merge_args_into_context_obj +from pipelines.cli.click_decorators import click_ci_requirements_option, click_ignore_unused_kwargs, click_merge_args_into_context_obj from pipelines.consts import DOCKER_VERSION from pipelines.helpers.utils import sh_dash_c from pipelines.models.contexts.click_pipeline_context import ClickPipelineContext, pass_pipeline_context @@ -36,6 +36,7 @@ async def run_poetry_command(container: dagger.Container, command: str) -> Tuple @click.command() @click.argument("poetry_package_path") +@click_ci_requirements_option() @click.option( "-c", "--poetry-run-command", diff --git a/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py b/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py index c7a463a160c91..8779fee5eab1b 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py +++ b/airbyte-ci/connectors/pipelines/pipelines/cli/airbyte_ci.py @@ -24,7 +24,12 @@ from github import PullRequest from pipelines import main_logger from pipelines.cli.auto_update import __installed_version__, check_for_upgrade, pre_confirm_auto_update_flag -from pipelines.cli.click_decorators import click_append_to_context_object, click_ignore_unused_kwargs, click_merge_args_into_context_obj +from pipelines.cli.click_decorators import ( + CI_REQUIREMENTS_OPTION_NAME, + click_append_to_context_object, + click_ignore_unused_kwargs, + click_merge_args_into_context_obj, +) from pipelines.cli.confirm_prompt import pre_confirm_all_flag from pipelines.cli.lazy_group import LazyGroup from pipelines.cli.telemetry import click_track_command @@ -83,6 +88,9 @@ def check_local_docker_configuration() -> None: def is_dagger_run_enabled_by_default() -> bool: + if CI_REQUIREMENTS_OPTION_NAME in sys.argv: + return False + dagger_run_by_default = [ ["connectors", "test"], ["connectors", "build"], diff --git a/airbyte-ci/connectors/pipelines/pipelines/cli/click_decorators.py b/airbyte-ci/connectors/pipelines/pipelines/cli/click_decorators.py index 47bd4ccb243a0..b88f582c6e37b 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/cli/click_decorators.py +++ b/airbyte-ci/connectors/pipelines/pipelines/cli/click_decorators.py @@ -5,9 +5,14 @@ import functools import inspect from functools import wraps -from typing import Any, Callable, Type +from typing import Any, Callable, Type, TypeVar import asyncclick as click +from pipelines.models.ci_requirements import CIRequirements + +_AnyCallable = Callable[..., Any] +FC = TypeVar("FC", bound="_AnyCallable | click.core.Command") +CI_REQUIREMENTS_OPTION_NAME = "--ci-requirements" def _contains_var_kwarg(f: Callable) -> bool: @@ -121,3 +126,27 @@ def decorated_function(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 return f(*args, **kwargs) return decorated_function + + +def click_ci_requirements_option() -> Callable[[FC], FC]: + """Add a --ci-requirements option to the command. + + Returns: + Callable[[FC], FC]: The decorated command. + """ + + def callback(ctx: click.Context, param: click.Parameter, value: bool) -> None: + if value: + ci_requirements = CIRequirements() + click.echo(ci_requirements.to_json()) + ctx.exit() + + return click.decorators.option( + CI_REQUIREMENTS_OPTION_NAME, + is_flag=True, + expose_value=False, + is_eager=True, + flag_value=True, + help="Show the CI requirements and exit. It used to make airbyte-ci client define the CI runners it will run on.", + callback=callback, + ) diff --git a/airbyte-ci/connectors/pipelines/pipelines/models/ci_requirements.py b/airbyte-ci/connectors/pipelines/pipelines/models/ci_requirements.py new file mode 100644 index 0000000000000..7eb8a9157b573 --- /dev/null +++ b/airbyte-ci/connectors/pipelines/pipelines/models/ci_requirements.py @@ -0,0 +1,33 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import json +from dataclasses import dataclass +from importlib import metadata + +INFRA_SUPPORTED_DAGGER_VERSIONS = { + "0.6.4", + "0.9.5", +} + + +@dataclass +class CIRequirements: + """ + A dataclass to store the CI requirements. + It used to make airbyte-ci client define the CI runners it will run on. + """ + + dagger_version = metadata.version("dagger-io") + + def __post_init__(self) -> None: + if self.dagger_version not in INFRA_SUPPORTED_DAGGER_VERSIONS: + raise ValueError( + f"Unsupported dagger version: {self.dagger_version}. " f"Supported versions are: {INFRA_SUPPORTED_DAGGER_VERSIONS}." + ) + + def to_json(self) -> str: + return json.dumps( + { + "dagger_version": self.dagger_version, + } + ) diff --git a/airbyte-ci/connectors/pipelines/pyproject.toml b/airbyte-ci/connectors/pipelines/pyproject.toml index ef811f62aaca1..4c7be71da3418 100644 --- a/airbyte-ci/connectors/pipelines/pyproject.toml +++ b/airbyte-ci/connectors/pipelines/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pipelines" -version = "3.2.0" +version = "3.3.0" description = "Packaged maintained by the connector operations team to perform CI for connectors' pipelines" authors = ["Airbyte "] From 6537ebceb29621b34d5b331c8eae7eb6b81a661b Mon Sep 17 00:00:00 2001 From: Henri Blancke Date: Mon, 15 Jan 2024 15:28:31 -0500 Subject: [PATCH 096/574] =?UTF-8?q?=F0=9F=90=9B=20Source=20Quickbooks:=20F?= =?UTF-8?q?ix=20refresh=20token=20issue=20by=20upgrading=20airbyte-cdk=20(?= =?UTF-8?q?#32236)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Henri Blancke Co-authored-by: Marcos Marx Co-authored-by: marcosmarxm --- airbyte-integrations/connectors/source-quickbooks/Dockerfile | 2 +- airbyte-integrations/connectors/source-quickbooks/metadata.yaml | 2 +- airbyte-integrations/connectors/source-quickbooks/setup.py | 2 +- .../source-quickbooks/source_quickbooks/manifest.yaml | 1 - docs/integrations/sources/quickbooks.md | 1 + 5 files changed, 4 insertions(+), 4 deletions(-) diff --git a/airbyte-integrations/connectors/source-quickbooks/Dockerfile b/airbyte-integrations/connectors/source-quickbooks/Dockerfile index 11a9b982877c5..18808a53082eb 100644 --- a/airbyte-integrations/connectors/source-quickbooks/Dockerfile +++ b/airbyte-integrations/connectors/source-quickbooks/Dockerfile @@ -34,5 +34,5 @@ COPY source_quickbooks ./source_quickbooks ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] -LABEL io.airbyte.version=3.0.0 +LABEL io.airbyte.version=3.0.1 LABEL io.airbyte.name=airbyte/source-quickbooks diff --git a/airbyte-integrations/connectors/source-quickbooks/metadata.yaml b/airbyte-integrations/connectors/source-quickbooks/metadata.yaml index 27c2f6d7cab4e..0961738252b37 100644 --- a/airbyte-integrations/connectors/source-quickbooks/metadata.yaml +++ b/airbyte-integrations/connectors/source-quickbooks/metadata.yaml @@ -7,7 +7,7 @@ data: connectorSubtype: api connectorType: source definitionId: cf9c4355-b171-4477-8f2d-6c5cc5fc8b7e - dockerImageTag: 3.0.0 + dockerImageTag: 3.0.1 dockerRepository: airbyte/source-quickbooks githubIssueLabel: source-quickbooks icon: quickbooks.svg diff --git a/airbyte-integrations/connectors/source-quickbooks/setup.py b/airbyte-integrations/connectors/source-quickbooks/setup.py index 025726239f79d..47ca1b1285055 100644 --- a/airbyte-integrations/connectors/source-quickbooks/setup.py +++ b/airbyte-integrations/connectors/source-quickbooks/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages, setup MAIN_REQUIREMENTS = [ - "airbyte-cdk>=0.44.0", + "airbyte-cdk>=0.58.8", ] TEST_REQUIREMENTS = [ diff --git a/airbyte-integrations/connectors/source-quickbooks/source_quickbooks/manifest.yaml b/airbyte-integrations/connectors/source-quickbooks/source_quickbooks/manifest.yaml index ac70de7bbc6a4..267f04d857d08 100644 --- a/airbyte-integrations/connectors/source-quickbooks/source_quickbooks/manifest.yaml +++ b/airbyte-integrations/connectors/source-quickbooks/source_quickbooks/manifest.yaml @@ -26,7 +26,6 @@ definitions: client_id: "{{ config['credentials']['client_id'] }}" client_secret: "{{ config['credentials']['client_secret'] }}" refresh_token: "{{ config['credentials']['refresh_token'] }}" - refresh_token_updater: {} retriever: type: SimpleRetriever record_selector: diff --git a/docs/integrations/sources/quickbooks.md b/docs/integrations/sources/quickbooks.md index caf2dfde4b05e..48866deba5bc3 100644 --- a/docs/integrations/sources/quickbooks.md +++ b/docs/integrations/sources/quickbooks.md @@ -105,6 +105,7 @@ This Source is capable of syncing the following [Streams](https://developer.intu | Version | Date | Pull Request | Subject | | :------ | :--------- | :------------------------------------------------------- | :------------------------------------------------------------------- | +| `3.0.1` | 2023-11-06 | [32236](https://github.com/airbytehq/airbyte/pull/32236) | Upgrade to `airbyte-cdk>=0.52.10` to resolve refresh token issues | | `3.0.0` | 2023-09-26 | [30770](https://github.com/airbytehq/airbyte/pull/30770) | Update schema to use `number` instead of `integer` | | `2.0.5` | 2023-09-26 | [30766](https://github.com/airbytehq/airbyte/pull/30766) | Fix improperly named keyword argument | | `2.0.4` | 2023-06-28 | [27803](https://github.com/airbytehq/airbyte/pull/27803) | Update following state breaking changes | From a0623c54b40c10f455f4e8ce4278b78d2ffd8cfb Mon Sep 17 00:00:00 2001 From: Scott Sinclair <252082+pwae@users.noreply.github.com> Date: Tue, 16 Jan 2024 07:40:19 +1100 Subject: [PATCH 097/574] fix link in MSSQL source documentation to pr (#33961) Co-authored-by: Marcos Marx --- docs/integrations/sources/mssql.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/sources/mssql.md b/docs/integrations/sources/mssql.md index 6370453b034d6..1ff83f4091902 100644 --- a/docs/integrations/sources/mssql.md +++ b/docs/integrations/sources/mssql.md @@ -347,7 +347,7 @@ WHERE actor_definition_id ='b5ea17b1-f170-46dc-bc31-cc744ca984c1' AND (configura | 3.5.0 | 2023-12-19 | [33071](https://github.com/airbytehq/airbyte/pull/33071) | Fix SSL configuration parameters | | 3.4.1 | 2024-01-02 | [33755](https://github.com/airbytehq/airbyte/pull/33755) | Encode binary to base64 format | | 3.4.0 | 2023-12-19 | [33481](https://github.com/airbytehq/airbyte/pull/33481) | Remove LEGACY state flag | -| 3.3.2 | 2023-12-14 | [33505](https://github.com/airbytehq/airbyte/pull/33225) | Using the released CDK. | +| 3.3.2 | 2023-12-14 | [33505](https://github.com/airbytehq/airbyte/pull/33505) | Using the released CDK. | | 3.3.1 | 2023-12-12 | [33225](https://github.com/airbytehq/airbyte/pull/33225) | extracting MsSql specific files out of the CDK. | | 3.3.0 | 2023-12-12 | [33018](https://github.com/airbytehq/airbyte/pull/33018) | Migrate to Per-stream/Global states and away from Legacy states | | 3.2.1 | 2023-12-11 | [33330](https://github.com/airbytehq/airbyte/pull/33330) | Parse DatetimeOffset fields with the correct format when used as cursor | From 4a6924e59818565b8330a8a3570e8cd22e78565d Mon Sep 17 00:00:00 2001 From: Anatolii Yatsuk <35109939+tolik0@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:43:53 +0200 Subject: [PATCH 098/574] =?UTF-8?q?=E2=9C=A8=20Source=20Intercom:=20Add=20?= =?UTF-8?q?new=20stream=20Activity=20logs=20stream=20(#33882)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration_tests/abnormal_state.json | 11 ++++++ .../integration_tests/configured_catalog.json | 14 +++++++ .../integration_tests/expected_records.jsonl | 32 ++++++++------- .../incremental_catalog.json | 14 +++++++ .../connectors/source-intercom/metadata.yaml | 2 +- .../connectors/source-intercom/setup.py | 2 +- .../source_intercom/components.py | 1 - .../source_intercom/manifest.yaml | 39 +++++++++++++++++++ .../schemas/activity_logs.json | 37 ++++++++++++++++++ docs/integrations/sources/intercom.md | 1 + 10 files changed, 137 insertions(+), 16 deletions(-) create mode 100644 airbyte-integrations/connectors/source-intercom/source_intercom/schemas/activity_logs.json diff --git a/airbyte-integrations/connectors/source-intercom/integration_tests/abnormal_state.json b/airbyte-integrations/connectors/source-intercom/integration_tests/abnormal_state.json index 2bd1cb003b2c2..e874bc451c678 100755 --- a/airbyte-integrations/connectors/source-intercom/integration_tests/abnormal_state.json +++ b/airbyte-integrations/connectors/source-intercom/integration_tests/abnormal_state.json @@ -73,5 +73,16 @@ "updated_at": 7626086649 } } + }, + { + "type": "STREAM", + "stream": { + "stream_descriptor": { + "name": "activity_logs" + }, + "stream_state": { + "created_at": 7626086649 + } + } } ] diff --git a/airbyte-integrations/connectors/source-intercom/integration_tests/configured_catalog.json b/airbyte-integrations/connectors/source-intercom/integration_tests/configured_catalog.json index 2e0e4e62a618e..66ccdc871d869 100644 --- a/airbyte-integrations/connectors/source-intercom/integration_tests/configured_catalog.json +++ b/airbyte-integrations/connectors/source-intercom/integration_tests/configured_catalog.json @@ -11,6 +11,20 @@ "primary_key": [["id"]], "destination_sync_mode": "append" }, + { + "stream": { + "name": "activity_logs", + "json_schema": {}, + "supported_sync_modes": ["full_refresh", "incremental"], + "source_defined_cursor": true, + "default_cursor_field": ["created_at"], + "source_defined_primary_key": [["id"]] + }, + "sync_mode": "incremental", + "cursor_field": ["created_at"], + "primary_key": [["id"]], + "destination_sync_mode": "append" + }, { "stream": { "name": "companies", diff --git a/airbyte-integrations/connectors/source-intercom/integration_tests/expected_records.jsonl b/airbyte-integrations/connectors/source-intercom/integration_tests/expected_records.jsonl index ed784188abc5e..cb29963e07f5c 100644 --- a/airbyte-integrations/connectors/source-intercom/integration_tests/expected_records.jsonl +++ b/airbyte-integrations/connectors/source-intercom/integration_tests/expected_records.jsonl @@ -11,13 +11,21 @@ {"stream":"admins","data":{"type":"admin","email":"user8.sample.airbyte@outlook.com","id":"6407155","name":"User8 Sample","away_mode_enabled":false,"away_mode_reassign":false,"has_inbox_seat":false,"team_ids":[],"team_priority_level":{}},"emitted_at":1695811366513} {"stream":"admins","data":{"type":"admin","email":"user9.sample.airbyte@outlook.com","id":"6407156","name":"User9 Sample","away_mode_enabled":false,"away_mode_reassign":false,"has_inbox_seat":false,"team_ids":[],"team_priority_level":{}},"emitted_at":1695811366514} {"stream":"admins","data":{"type":"admin","email":"user10.sample.airbyte@outlook.com","id":"6407160","name":"User10 Sample","away_mode_enabled":false,"away_mode_reassign":false,"has_inbox_seat":false,"team_ids":[],"team_priority_level":{}},"emitted_at":1695811366516} +{"stream": "activity_logs", "data": {"id": "f7cf4eba-3a37-44b0-aecf-f347fe116712", "performed_by": {"type": "admin", "id": "4423433", "email": "integration-test@airbyte.io", "ip": "93.74.108.30"}, "metadata": {"admin": {"id": 4423433, "first_name": "John", "last_name": "Lafleur"}, "before": {"permissions": {"access_billing_settings": true, "access_developer_hub": true, "access_product_settings": true, "access_reporting": true, "access_workspace_settings": true, "create_and_edit_bots": true, "export_data": true, "manage_apps_and_integrations": true, "manage_articles": true, "manage_inbox_rules": true, "manage_inbox_views": true, "manage_messages_settings": true, "manage_messenger_settings": true, "manage_saved_replies": true, "manage_tags": true, "manage_teammates": true, "reassign_conversations": true, "redact_conversation_parts": true, "send_messages": true}, "conversation_access": {}}, "after": {"permissions": {"access_billing_settings": true, "access_developer_hub": true, "access_product_settings": true, "access_reporting": true, "access_workspace_settings": true, "create_and_edit_bots": true, "export_data": true, "manage_apps_and_integrations": true, "manage_articles": true, "manage_inbox_rules": true, "manage_inbox_views": true, "manage_messages_settings": true, "manage_messenger_settings": true, "manage_saved_replies": true, "manage_tags": true, "manage_teammates": true, "reassign_conversations": true, "redact_conversation_parts": true, "send_messages": true}, "conversation_access": {"access_type": "all", "assignee_blocked_list": null, "include_unassigned": false}}}, "created_at": 1625657753, "activity_type": "admin_permission_change", "activity_description": "Airbyte Team changed John Lafleur's permissions."}, "emitted_at": 1704967352753} +{"stream": "activity_logs", "data": {"id": "1fb8c7f2-bb57-49c9-bffc-7c49e0e54b40", "performed_by": {"type": "admin", "id": "4423433", "email": "integration-test@airbyte.io", "ip": "93.74.108.30"}, "metadata": {"team": {"id": 5077733, "name": "test", "member_count": 1}}, "created_at": 1625657582, "activity_type": "app_team_creation", "activity_description": "Airbyte Team created a new team, test, with 1 member."}, "emitted_at": 1704967352755} +{"stream": "activity_logs", "data": {"id": "5f569e46-45c3-4f76-93b7-9096bca00431", "performed_by": {"type": "admin", "id": "4423433", "email": "integration-test@airbyte.io", "ip": "93.74.108.30"}, "metadata": {"admin": {"id": 4425337, "first_name": "Jared", "last_name": "Rhizor"}, "before": {"permissions": {"access_billing_settings": false, "access_developer_hub": true, "access_product_settings": true, "access_reporting": true, "access_workspace_settings": true, "create_and_edit_bots": false, "export_data": true, "manage_apps_and_integrations": true, "manage_articles": false, "manage_inbox_rules": true, "manage_inbox_views": false, "manage_messages_settings": false, "manage_messenger_settings": false, "manage_saved_replies": false, "manage_tags": true, "manage_teammates": true, "reassign_conversations": false, "redact_conversation_parts": false, "send_messages": false}, "conversation_access": {}}, "after": {"permissions": {"access_billing_settings": false, "access_developer_hub": true, "access_product_settings": true, "access_reporting": true, "access_workspace_settings": true, "create_and_edit_bots": false, "export_data": true, "manage_apps_and_integrations": true, "manage_articles": false, "manage_inbox_rules": true, "manage_inbox_views": false, "manage_messages_settings": false, "manage_messenger_settings": false, "manage_saved_replies": false, "manage_tags": true, "manage_teammates": true, "reassign_conversations": false, "redact_conversation_parts": false, "send_messages": false}, "conversation_access": {"access_type": "all", "assignee_blocked_list": null, "include_unassigned": false}}}, "created_at": 1625657461, "activity_type": "admin_permission_change", "activity_description": "Airbyte Team changed Jared Rhizor's permissions."}, "emitted_at": 1704967352757} +{"stream": "activity_logs", "data": {"id": "766a7c71-5c41-415e-8984-ca8873b00b78", "performed_by": {"type": "admin", "id": "4423433", "email": "integration-test@airbyte.io", "ip": "unknown"}, "metadata": {"app_package": {"name": "Airbyte", "description": null}}, "created_at": 1634283727, "activity_type": "app_package_installation", "activity_description": "Airbyte Team installed the Airbyte app package for Airbyte [DEV]."}, "emitted_at": 1704967353629} +{"stream": "activity_logs", "data": {"id": "47e43c9a-509e-43f9-9867-16b8fb8f8f88", "performed_by": {"type": "admin", "id": "4423433", "email": "integration-test@airbyte.io", "ip": "unknown"}, "metadata": {"app_package": {"name": "Airbyte", "description": null}}, "created_at": 1634281951, "activity_type": "app_package_installation", "activity_description": "Airbyte Team installed the Airbyte app package for Airbyte [DEV]."}, "emitted_at": 1704967353635} +{"stream": "activity_logs", "data": {"id": "1a4baeb9-4cf7-4c3c-ac88-c8e8a4d2b8d7", "performed_by": {"type": "admin", "id": "4423433", "email": "integration-test@airbyte.io", "ip": "unknown"}, "metadata": {"app_package": {"name": "Airbyte Application", "description": null}}, "created_at": 1634235978, "activity_type": "app_package_installation", "activity_description": "Airbyte Team installed the Airbyte Application app package for Airbyte [DEV]."}, "emitted_at": 1704967353639} +{"stream": "activity_logs", "data": {"id": "2fb77f78-b37e-4db1-8357-b990ea1f9c33", "performed_by": {"type": "admin", "id": "4423433", "email": "integration-test@airbyte.io", "ip": "unknown"}, "metadata": {"app_package": {"name": "Airbyte", "description": null}}, "created_at": 1633429956, "activity_type": "app_package_installation", "activity_description": "Airbyte Team installed the Airbyte app package for Airbyte [DEV]."}, "emitted_at": 1704967353642} +{"stream": "activity_logs", "data": {"id": "db71bd41-665d-4589-876a-900857f462ec", "performed_by": {"type": "admin", "id": "4423433", "email": "integration-test@airbyte.io", "ip": "93.73.161.112"}, "metadata": {"app_package": {"name": "Airbyte Application", "description": null}}, "created_at": 1632482635, "activity_type": "app_package_installation", "activity_description": "Airbyte Team installed the Airbyte Application app package for Airbyte [DEV]."}, "emitted_at": 1704967353644} +{"stream": "activity_logs", "data": {"id": "d8fc8709-3ed7-46fc-ae43-96f7a04e8682", "performed_by": {"type": "admin", "id": "4423433", "email": "integration-test@airbyte.io", "ip": "93.73.161.112"}, "metadata": {"app_package": {"name": "Airbyte App", "description": null}}, "created_at": 1632482041, "activity_type": "app_package_installation", "activity_description": "Airbyte Team installed the Airbyte App app package for Airbyte [DEV]."}, "emitted_at": 1704967353646} +{"stream": "activity_logs", "data": {"id": "940efd94-6625-4033-8cd8-42eec9063b5e", "performed_by": {"type": "admin", "id": "4423433", "email": "integration-test@airbyte.io", "ip": "93.73.161.112"}, "metadata": {"app_package": {"name": "Airbyte", "description": null}}, "created_at": 1632481922, "activity_type": "app_package_installation", "activity_description": "Airbyte Team installed the Airbyte app package for Airbyte [DEV]."}, "emitted_at": 1704967353647} {"stream": "companies", "data": {"type": "company", "company_id": "63ecc5731d460cdc137c906d-qualification-company", "id": "63ecc5731d460cdc137c906c", "app_id": "wjw5eps7", "name": "Test Company 8", "created_at": 1676461427, "updated_at": 1679484652, "monthly_spend": 0, "session_count": 0, "user_count": 1, "size": 49, "website": "www.company8.com", "industry": "Manufacturing", "tags": {"type": "tag.list", "tags": []}, "segments": {"type": "segment.list", "segments": []}, "plan": {}, "custom_attributes": {"creation_source": "api"}}, "emitted_at": 1689152867526} {"stream": "companies", "data": {"type": "company", "company_id": "63ecc52f00fc87e58e8fb1f2-qualification-company", "id": "63ecc52f00fc87e58e8fb1f1", "app_id": "wjw5eps7", "name": "Test Company 7", "created_at": 1676461359, "updated_at": 1679484653, "monthly_spend": 0, "session_count": 0, "user_count": 1, "size": 23, "website": "www.company7.com", "industry": "Production", "tags": {"type": "tag.list", "tags": []}, "segments": {"type": "segment.list", "segments": []}, "plan": {}, "custom_attributes": {"creation_source": "api"}}, "emitted_at": 1689152867529} {"stream": "companies", "data": {"type": "company", "company_id": "63ecc46a811f1737ded479ef-qualification-company", "id": "63ecc46a811f1737ded479ee", "app_id": "wjw5eps7", "name": "Test Company 4", "created_at": 1676461162, "updated_at": 1679484653, "monthly_spend": 0, "session_count": 0, "user_count": 1, "size": 150, "website": "www.company4.com", "industry": "Software", "tags": {"type": "tag.list", "tags": []}, "segments": {"type": "segment.list", "segments": []}, "plan": {}, "custom_attributes": {"creation_source": "api"}}, "emitted_at": 1689152867531} {"stream": "companies", "data": {"type": "company", "company_id": "63ecc5d32059cdacf4ac6171-qualification-company", "id": "63ecc5d32059cdacf4ac6170", "app_id": "wjw5eps7", "name": "Test Company 9", "created_at": 1676461523, "updated_at": 1679484653, "monthly_spend": 0, "session_count": 0, "user_count": 1, "size": 75, "website": "www.company9.com", "industry": "Sales", "tags": {"type": "tag.list", "tags": []}, "segments": {"type": "segment.list", "segments": []}, "plan": {}, "custom_attributes": {"creation_source": "api"}}, "emitted_at": 1689152867536} {"stream": "companies", "data": {"type": "company", "company_id": "63ecc61266325d8ebd24ed11-qualification-company", "id": "63ecc61266325d8ebd24ed10", "app_id": "wjw5eps7", "name": "Test Company 10", "created_at": 1676461586, "updated_at": 1679484652, "monthly_spend": 0, "session_count": 0, "user_count": 1, "size": 38, "website": "www.company10.com", "industry": "IT", "tags": {"type": "tag.list", "tags": []}, "segments": {"type": "segment.list", "segments": []}, "plan": {}, "custom_attributes": {"creation_source": "api"}}, "emitted_at": 1689152867538} -{"stream": "companies", "data": {"type": "company", "company_id": "63ecbfccb064f24a4941d219-qualification-company", "id": "63ecbfccb064f24a4941d218", "app_id": "wjw5eps7", "name": "Test Company", "created_at": 1676459980, "updated_at": 1679484653, "monthly_spend": 0, "session_count": 0, "user_count": 1, "size": 123, "website": "http://test.com", "industry": "IT", "tags": {"type": "tag.list", "tags": [{"type": "tag", "id": "7799571", "name": "Tag1"}, {"type": "tag", "id": "7799570", "name": "Tag2"}, {"type": "tag", "id": "7799640", "name": "Tag10"}]}, "segments": {"type": "segment.list", "segments": []}, "plan": {}, "custom_attributes": {"creation_source": "api"}}, "emitted_at": 1689152867533} -{"stream": "companies", "data": {"type": "company", "company_id": "63ecbfef66325dc8a0ac006f-qualification-company", "id": "63ecbfef66325dc8a0ac006e", "app_id": "wjw5eps7", "name": "Test Company 2", "created_at": 1676460015, "updated_at": 1679484653, "monthly_spend": 0, "session_count": 0, "user_count": 1, "size": 123, "website": "http://test.com", "industry": "IT 123", "tags": {"type": "tag.list", "tags": []}, "segments": {"type": "segment.list", "segments": []}, "plan": {}, "custom_attributes": {"creation_source": "api"}}, "emitted_at": 1689152867540} {"stream": "companies", "data": {"type": "company", "company_id": "63ecc41866325d2e90b0d3c6-qualification-company", "id": "63ecc41866325d2e90b0d3c5", "app_id": "wjw5eps7", "name": "Test Company 3", "created_at": 1676461080, "updated_at": 1679484653, "monthly_spend": 0, "session_count": 0, "user_count": 1, "size": 50, "website": "www.company3.com", "industry": "IT", "tags": {"type": "tag.list", "tags": []}, "segments": {"type": "segment.list", "segments": []}, "plan": {}, "custom_attributes": {"creation_source": "api"}}, "emitted_at": 1689152867542} {"stream": "companies", "data": {"type": "company", "company_id": "63ecc3d60e3c81baaad9f9ef-qualification-company", "id": "63ecc3d60e3c81baaad9f9ee", "app_id": "wjw5eps7", "name": "Company 1", "created_at": 1676461015, "updated_at": 1689068298, "monthly_spend": 0, "session_count": 0, "user_count": 1, "size": 25, "website": "www.company1.com", "industry": "Sales", "tags": {"type": "tag.list", "tags": []}, "segments": {"type": "segment.list", "segments": []}, "plan": {}, "custom_attributes": {"creation_source": "api"}}, "emitted_at": 1689152867548} {"stream": "companies", "data": {"type": "company", "company_id": "63ecc4e99a2c64721f435a23-qualification-company", "id": "63ecc4e99a2c64721f435a22", "app_id": "wjw5eps7", "name": "Test Company 6", "created_at": 1676461289, "updated_at": 1679484652, "monthly_spend": 0, "session_count": 0, "user_count": 1, "size": 55, "website": "www.company6.com", "industry": "Sales", "tags": {"type": "tag.list", "tags": [{"type": "tag", "id": "7799570", "name": "Tag2"}, {"type": "tag", "id": "7799640", "name": "Tag10"}]}, "segments": {"type": "segment.list", "segments": []}, "plan": {}, "custom_attributes": {"creation_source": "api"}}, "emitted_at": 1689152867544} @@ -50,17 +58,15 @@ {"stream": "company_segments", "data": {"type": "segment", "id": "63ea1a43d9c86cceefd8796e", "name": "Revenue", "created_at": 1676286531, "updated_at": 1676462321, "person_type": "user"}, "emitted_at": 1680518982259} {"stream": "company_segments", "data": {"type": "segment", "id": "63ecc7f36d40e8184b5d47a6", "name": "Sales", "created_at": 1676462067, "updated_at": 1676462069, "person_type": "user"}, "emitted_at": 1680518982262} {"stream": "company_segments", "data": {"type": "segment", "id": "6241a4b8c8b709894fa54df1", "name": "Test_1", "created_at": 1648469176, "updated_at": 1676462341, "person_type": "user"}, "emitted_at": 1680518982266} -{"stream": "conversations", "data": {"type": "conversation", "id": "1", "created_at": 1607553243, "updated_at": 1626346673, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "701718739", "delivered_as": "customer_initiated", "subject": "", "body": "

hey there

", "author": {"type": "lead", "id": "5fd150d50697b6d0bbc4a2c2", "name": null, "email": ""}, "attachments": [], "url": "http://localhost:63342/airbyte-python/airbyte-integrations/bases/base-java/build/tmp/expandedArchives/org.jacoco.agent-0.8.5.jar_6a2df60c47de373ea127d14406367999/about.html?_ijt=uosck1k6vmp2dnl4oqib2g3u9d", "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "5fd150d50697b6d0bbc4a2c2"}]}, "first_contact_reply": {"created_at": 1607553243, "type": "conversation", "url": "http://localhost:63342/airbyte-python/airbyte-integrations/bases/base-java/build/tmp/expandedArchives/org.jacoco.agent-0.8.5.jar_6a2df60c47de373ea127d14406367999/about.html?_ijt=uosck1k6vmp2dnl4oqib2g3u9d"}, "admin_assignee_id": null, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": 4317957, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": 4317954, "first_contact_reply_at": 1607553243, "first_assignment_at": null, "first_admin_reply_at": 1625654131, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": 1607553246, "last_admin_reply_at": 1625656000, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 7}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": null, "custom_attributes": {}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1689153694977} -{"stream": "conversations", "data": {"type": "conversation", "id": "59", "created_at": 1676460979, "updated_at": 1689068230, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51952658", "delivered_as": "automated", "subject": "", "body": "

Test 1

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea418c0931f79d99a197ff"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": false, "state": "closed", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 3}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test 1", "custom_attributes": {}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1689153695018} -{"stream": "conversations", "data": {"type": "conversation", "id": "60", "created_at": 1676461133, "updated_at": 1676461134, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51952871", "delivered_as": "automated", "subject": "", "body": "

Test 3

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea41a0eddb9b625fb712c9"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test3", "custom_attributes": {}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1689153694982} -{"stream": "conversations", "data": {"type": "conversation", "id": "61", "created_at": 1676461196, "updated_at": 1676461197, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51952963", "delivered_as": "automated", "subject": "", "body": "

Test 4

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea41a1b0e17c53248c7956"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test 4", "custom_attributes": {}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1689153694985} -{"stream": "conversations", "data": {"type": "conversation", "id": "63", "created_at": 1676461327, "updated_at": 1676461328, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51953153", "delivered_as": "automated", "subject": "", "body": "

Test 6

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea41a2b2d44e63848146e7"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test 6", "custom_attributes": {}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1689153694989} -{"stream": "conversations", "data": {"type": "conversation", "id": "64", "created_at": 1676461395, "updated_at": 1676461396, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51953262", "delivered_as": "automated", "subject": "", "body": "

Test 7

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea41a2c340f850172f2905"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test 7", "custom_attributes": {}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1689153694994} -{"stream": "conversations", "data": {"type": "conversation", "id": "65", "created_at": 1676461499, "updated_at": 1676461499, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51953436", "delivered_as": "automated", "subject": "", "body": "

Test Lead 1

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "5fd150d50697b6d0bbc4a2c2"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test Lead 1", "custom_attributes": {}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1689153694998} -{"stream": "conversations", "data": {"type": "conversation", "id": "66", "created_at": 1676461563, "updated_at": 1676461564, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51953541", "delivered_as": "automated", "subject": "", "body": "

Test 9

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea41a3b0e17c505e52044d"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test 9", "custom_attributes": {}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1689153695003} -{"stream": "conversations", "data": {"type": "conversation", "id": "67", "created_at": 1676461636, "updated_at": 1676461637, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51953649", "delivered_as": "automated", "subject": "", "body": "

Test 10

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea41a7b0e17c5039fbb824"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test 10", "custom_attributes": {}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1689153695007} -{"stream": "conversations", "data": {"type": "conversation", "id": "68", "created_at": 1676461800, "updated_at": 1676461800, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51953852", "delivered_as": "automated", "subject": "", "body": "

Test Lead 5001

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ecc6c2811f17873ed2d007"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test Lead 5001", "custom_attributes": {}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1689153695011} -{"stream": "conversations", "data": {"type": "conversation", "id": "69", "created_at": 1676462031, "updated_at": 1676462031, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51954139", "delivered_as": "automated", "subject": "", "body": "

Test 11

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea41a80931f79b6998e89f"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test 11", "custom_attributes": {}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1689153695015} +{"stream": "conversations", "data": {"type": "conversation", "id": "60", "created_at": 1676461133, "updated_at": 1676461134, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51952871", "delivered_as": "automated", "subject": "", "body": "

Test 3

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea41a0eddb9b625fb712c9"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test3", "custom_attributes": {"Created by": 4423433}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1705349379561} +{"stream": "conversations", "data": {"type": "conversation", "id": "61", "created_at": 1676461196, "updated_at": 1676461197, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51952963", "delivered_as": "automated", "subject": "", "body": "

Test 4

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea41a1b0e17c53248c7956"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test 4", "custom_attributes": {"Created by": 4423433}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1705349379565} +{"stream": "conversations", "data": {"type": "conversation", "id": "63", "created_at": 1676461327, "updated_at": 1676461328, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51953153", "delivered_as": "automated", "subject": "", "body": "

Test 6

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea41a2b2d44e63848146e7"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test 6", "custom_attributes": {"Created by": 4423433}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1705349379569} +{"stream": "conversations", "data": {"type": "conversation", "id": "64", "created_at": 1676461395, "updated_at": 1676461396, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51953262", "delivered_as": "automated", "subject": "", "body": "

Test 7

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea41a2c340f850172f2905"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test 7", "custom_attributes": {"Created by": 4423433}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1705349379573} +{"stream": "conversations", "data": {"type": "conversation", "id": "65", "created_at": 1676461499, "updated_at": 1676461499, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51953436", "delivered_as": "automated", "subject": "", "body": "

Test Lead 1

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "5fd150d50697b6d0bbc4a2c2"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test Lead 1", "custom_attributes": {"Created by": 4423433}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1705349379577} +{"stream": "conversations", "data": {"type": "conversation", "id": "66", "created_at": 1676461563, "updated_at": 1676461564, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51953541", "delivered_as": "automated", "subject": "", "body": "

Test 9

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea41a3b0e17c505e52044d"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test 9", "custom_attributes": {"Created by": 4423433}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1705349379581} +{"stream": "conversations", "data": {"type": "conversation", "id": "67", "created_at": 1676461636, "updated_at": 1676461637, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51953649", "delivered_as": "automated", "subject": "", "body": "

Test 10

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea41a7b0e17c5039fbb824"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test 10", "custom_attributes": {"Created by": 4423433}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1705349379585} +{"stream": "conversations", "data": {"type": "conversation", "id": "68", "created_at": 1676461800, "updated_at": 1676461800, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51953852", "delivered_as": "automated", "subject": "", "body": "

Test Lead 5001

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ecc6c2811f17873ed2d007"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test Lead 5001", "custom_attributes": {"Created by": 4423433}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1705349379589} +{"stream": "conversations", "data": {"type": "conversation", "id": "69", "created_at": 1676462031, "updated_at": 1676462031, "waiting_since": null, "snoozed_until": null, "source": {"type": "conversation", "id": "51954139", "delivered_as": "automated", "subject": "", "body": "

Test 11

", "author": {"type": "admin", "id": "4423433", "name": "Airbyte Team", "email": "integration-test@airbyte.io"}, "attachments": [], "url": null, "redacted": false}, "contacts": {"type": "contact.list", "contacts": [{"type": "contact", "id": "63ea41a80931f79b6998e89f"}]}, "first_contact_reply": null, "admin_assignee_id": 4423433, "team_assignee_id": null, "open": true, "state": "open", "read": false, "tags": {"type": "tag.list", "tags": []}, "priority": "not_priority", "sla_applied": null, "statistics": {"type": "conversation_statistics", "time_to_assignment": null, "time_to_admin_reply": null, "time_to_first_close": null, "time_to_last_close": null, "median_time_to_reply": null, "first_contact_reply_at": null, "first_assignment_at": null, "first_admin_reply_at": null, "first_close_at": null, "last_assignment_at": null, "last_assignment_admin_reply_at": null, "last_contact_reply_at": null, "last_admin_reply_at": null, "last_close_at": null, "last_closed_by_id": null, "count_reopens": 0, "count_assignments": 0, "count_conversation_parts": 2}, "conversation_rating": null, "teammates": {"type": "admin.list", "admins": [{"type": "admin", "id": "4423433"}]}, "title": "Test 11", "custom_attributes": {"Created by": 4423433}, "topics": {"type": "topic.list", "topics": [], "total_count": 0}}, "emitted_at": 1705349379593} {"stream": "conversation_parts", "data": {"type": "conversation_part", "id": "7288120839", "part_type": "comment", "body": "

is this showing up

", "created_at": 1607553246, "updated_at": 1607553246, "notified_at": 1607553246, "assigned_to": null, "author": {"id": "5fd150d50697b6d0bbc4a2c2", "type": "user", "name": null, "email": ""}, "attachments": [], "external_id": null, "redacted": false, "conversation_id": "1"}, "emitted_at": 1688632241806} {"stream": "conversation_parts", "data": {"type": "conversation_part", "id": "7288121348", "part_type": "comment", "body": "

Airbyte [DEV] will reply as soon as they can.

", "created_at": 1607553249, "updated_at": 1607553249, "notified_at": 1607553249, "assigned_to": null, "author": {"id": "4423434", "type": "bot", "name": "Operator", "email": "operator+wjw5eps7@intercom.io"}, "attachments": [], "external_id": null, "redacted": false, "conversation_id": "1"}, "emitted_at": 1688632241811} {"stream": "conversation_parts", "data": {"type": "conversation_part", "id": "7288121392", "part_type": "comment", "body": "

Give the team a way to reach you:

", "created_at": 1607553250, "updated_at": 1607553250, "notified_at": 1607553250, "assigned_to": null, "author": {"id": "4423434", "type": "bot", "name": "Operator", "email": "operator+wjw5eps7@intercom.io"}, "attachments": [], "external_id": null, "redacted": false, "conversation_id": "1"}, "emitted_at": 1688632241815} diff --git a/airbyte-integrations/connectors/source-intercom/integration_tests/incremental_catalog.json b/airbyte-integrations/connectors/source-intercom/integration_tests/incremental_catalog.json index 2c4a3735e86dc..04647c9bf1a79 100644 --- a/airbyte-integrations/connectors/source-intercom/integration_tests/incremental_catalog.json +++ b/airbyte-integrations/connectors/source-intercom/integration_tests/incremental_catalog.json @@ -1,5 +1,19 @@ { "streams": [ + { + "stream": { + "name": "activity_logs", + "json_schema": {}, + "supported_sync_modes": ["full_refresh", "incremental"], + "source_defined_cursor": true, + "default_cursor_field": ["created_at"], + "source_defined_primary_key": [["id"]] + }, + "sync_mode": "incremental", + "cursor_field": ["created_at"], + "primary_key": [["id"]], + "destination_sync_mode": "append" + }, { "stream": { "name": "companies", diff --git a/airbyte-integrations/connectors/source-intercom/metadata.yaml b/airbyte-integrations/connectors/source-intercom/metadata.yaml index 05c8251ba9660..beb243445d313 100644 --- a/airbyte-integrations/connectors/source-intercom/metadata.yaml +++ b/airbyte-integrations/connectors/source-intercom/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: d8313939-3782-41b0-be29-b3ca20d8dd3a - dockerImageTag: 0.3.2 + dockerImageTag: 0.4.0 dockerRepository: airbyte/source-intercom documentationUrl: https://docs.airbyte.com/integrations/sources/intercom githubIssueLabel: source-intercom diff --git a/airbyte-integrations/connectors/source-intercom/setup.py b/airbyte-integrations/connectors/source-intercom/setup.py index f5fce35eb7182..0432b7d7f10a7 100644 --- a/airbyte-integrations/connectors/source-intercom/setup.py +++ b/airbyte-integrations/connectors/source-intercom/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages, setup MAIN_REQUIREMENTS = [ - "airbyte-cdk", + "airbyte-cdk>=0.58.8", # previous versions had a bug with http_method value from the manifest ] TEST_REQUIREMENTS = [ diff --git a/airbyte-integrations/connectors/source-intercom/source_intercom/components.py b/airbyte-integrations/connectors/source-intercom/source_intercom/components.py index 6e87c9b9e8a1f..600ba64945b16 100644 --- a/airbyte-integrations/connectors/source-intercom/source_intercom/components.py +++ b/airbyte-integrations/connectors/source-intercom/source_intercom/components.py @@ -167,7 +167,6 @@ def read_parent_stream( ) for parent_slice in parent_stream_slices_gen: - parent_records_gen = self.parent_stream.read_records( sync_mode=sync_mode, cursor_field=cursor_field, stream_slice=parent_slice, stream_state=stream_state ) diff --git a/airbyte-integrations/connectors/source-intercom/source_intercom/manifest.yaml b/airbyte-integrations/connectors/source-intercom/source_intercom/manifest.yaml index e5c648bb4692a..4dd78b43ea9d9 100644 --- a/airbyte-integrations/connectors/source-intercom/source_intercom/manifest.yaml +++ b/airbyte-integrations/connectors/source-intercom/source_intercom/manifest.yaml @@ -297,7 +297,46 @@ definitions: data_field: "conversations" page_size: 150 + # activity logs stream is incremental based on created_at field + activity_logs: + $ref: "#/definitions/stream_full_refresh" + primary_key: id + $parameters: + name: "activity_logs" + path: "admins/activity_logs" + data_field: "activity_logs" + retriever: + $ref: "#/definitions/retriever" + description: "The Retriever without passing page size option" + paginator: + type: "DefaultPaginator" + url_base: "#/definitions/requester/url_base" + pagination_strategy: + type: "CursorPagination" + cursor_value: "{{ response.get('pages', {}).get('next') }}" + stop_condition: "{{ 'next' not in response.get('pages', {}) }}" + page_token_option: + type: RequestPath + incremental_sync: + type: DatetimeBasedCursor + cursor_field: created_at + cursor_datetime_formats: + - "%s" + datetime_format: "%s" + cursor_granularity: "PT1S" + step: "P30D" + start_datetime: + datetime: "{{ config['start_date'] }}" + datetime_format: "%Y-%m-%dT%H:%M:%SZ" + end_time_option: + field_name: "created_at_before" + inject_into: "request_parameter" + start_time_option: + field_name: "created_at_after" + inject_into: "request_parameter" + streams: + - "#/definitions/activity_logs" - "#/definitions/admins" - "#/definitions/tags" - "#/definitions/teams" diff --git a/airbyte-integrations/connectors/source-intercom/source_intercom/schemas/activity_logs.json b/airbyte-integrations/connectors/source-intercom/source_intercom/schemas/activity_logs.json new file mode 100644 index 0000000000000..3136288524e54 --- /dev/null +++ b/airbyte-integrations/connectors/source-intercom/source_intercom/schemas/activity_logs.json @@ -0,0 +1,37 @@ +{ + "type": "object", + "properties": { + "performed_by": { + "type": ["null", "object"], + "properties": { + "id": { + "type": ["null", "string"] + }, + "type": { + "type": ["null", "string"] + }, + "ip": { + "type": ["null", "string"] + }, + "email": { + "type": ["null", "string"] + } + } + }, + "id": { + "type": ["null", "string"] + }, + "metadata": { + "type": ["null", "object"] + }, + "activity_type": { + "type": ["null", "string"] + }, + "activity_description": { + "type": ["null", "string"] + }, + "created_at": { + "type": ["null", "integer"] + } + } +} diff --git a/docs/integrations/sources/intercom.md b/docs/integrations/sources/intercom.md index 8300aeca49d4a..e3bffdb9aa56c 100644 --- a/docs/integrations/sources/intercom.md +++ b/docs/integrations/sources/intercom.md @@ -74,6 +74,7 @@ The Intercom connector should not run into Intercom API limitations under normal | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:---------------------------------------------------------------------------------------------------------| +| 0.4.0 | 2024-01-11 | [33882](https://github.com/airbytehq/airbyte/pull/33882) | Add new stream `Activity Logs` | | 0.3.2 | 2023-12-07 | [33223](https://github.com/airbytehq/airbyte/pull/33223) | Ignore 404 error for `Conversation Parts` | | 0.3.1 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | | 0.3.0 | 2023-05-25 | [29598](https://github.com/airbytehq/airbyte/pull/29598) | Update custom components to make them compatible with latest cdk version, simplify logic, update schemas | From b402fd979172d17085b82c2e205036fd65738dfc Mon Sep 17 00:00:00 2001 From: attaxia Date: Mon, 15 Jan 2024 21:46:31 +0100 Subject: [PATCH 099/574] Change link in documentation to point to English AWS documentation (#33725) Co-authored-by: Marcos Marx --- docs/deploying-airbyte/on-aws-ec2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying-airbyte/on-aws-ec2.md b/docs/deploying-airbyte/on-aws-ec2.md index cd45e33d1f65a..3b352ce59015a 100644 --- a/docs/deploying-airbyte/on-aws-ec2.md +++ b/docs/deploying-airbyte/on-aws-ec2.md @@ -91,7 +91,7 @@ ssh -i $SSH_KEY -L 8000:localhost:8000 -N -f ec2-user@$INSTANCE_IP ## Get Airbyte logs in CloudWatch -Follow this [guide](https://aws.amazon.com/pt/premiumsupport/knowledge-center/cloudwatch-docker-container-logs-proxy/) to get your logs from your Airbyte Docker containers in CloudWatch. +Follow this [guide](https://aws.amazon.com/en/premiumsupport/knowledge-center/cloudwatch-docker-container-logs-proxy/) to get your logs from your Airbyte Docker containers in CloudWatch. ## Troubleshooting From cbcaa15aeab0db5468d28b832bd73d6adfe3af3e Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jan 2024 10:25:24 +0100 Subject: [PATCH 100/574] airbyte-lib: Add telemetry (#33679) Co-authored-by: alafanechere Co-authored-by: Aaron ("AJ") Steers --- airbyte-lib/airbyte_lib/_executor.py | 11 +++ airbyte-lib/airbyte_lib/caches/base.py | 5 ++ airbyte-lib/airbyte_lib/caches/duckdb.py | 5 ++ airbyte-lib/airbyte_lib/caches/postgres.py | 5 ++ airbyte-lib/airbyte_lib/caches/snowflake.py | 5 ++ airbyte-lib/airbyte_lib/registry.py | 6 +- airbyte-lib/airbyte_lib/source.py | 48 ++++++++--- airbyte-lib/airbyte_lib/telemetry.py | 78 ++++++++++++++++++ airbyte-lib/airbyte_lib/version.py | 10 +++ .../docs/generated/airbyte_lib/caches.html | 46 +++++++---- .../docs/generated/airbyte_lib/factories.html | 7 ++ .../examples/run_test_source_single_stream.py | 19 +++++ .../fixtures/source-test/source_test/run.py | 5 +- .../integration_tests/test_integration.py | 80 +++++++++++++++++++ 14 files changed, 299 insertions(+), 31 deletions(-) create mode 100644 airbyte-lib/airbyte_lib/telemetry.py create mode 100644 airbyte-lib/airbyte_lib/version.py create mode 100644 airbyte-lib/docs/generated/airbyte_lib/factories.html create mode 100644 airbyte-lib/examples/run_test_source_single_stream.py diff --git a/airbyte-lib/airbyte_lib/_executor.py b/airbyte-lib/airbyte_lib/_executor.py index a051feb603661..6d0c6625bb186 100644 --- a/airbyte-lib/airbyte_lib/_executor.py +++ b/airbyte-lib/airbyte_lib/_executor.py @@ -10,6 +10,7 @@ from typing import IO, Any, NoReturn from airbyte_lib.registry import ConnectorMetadata +from airbyte_lib.telemetry import SourceTelemetryInfo, SourceType _LATEST_VERSION = "latest" @@ -40,6 +41,10 @@ def ensure_installation(self) -> None: def install(self) -> None: pass + @abstractmethod + def get_telemetry_info(self) -> SourceTelemetryInfo: + pass + @abstractmethod def uninstall(self) -> None: pass @@ -189,6 +194,9 @@ def execute(self, args: list[str]) -> Iterator[str]: with _stream_from_subprocess([str(connector_path)] + args) as stream: yield from stream + def get_telemetry_info(self) -> SourceTelemetryInfo: + return SourceTelemetryInfo(self.metadata.name, SourceType.VENV, self.target_version) + class PathExecutor(Executor): def ensure_installation(self) -> None: @@ -210,3 +218,6 @@ def uninstall(self) -> NoReturn: def execute(self, args: list[str]) -> Iterator[str]: with _stream_from_subprocess([self.metadata.name] + args) as stream: yield from stream + + def get_telemetry_info(self) -> SourceTelemetryInfo: + return SourceTelemetryInfo(self.metadata.name, SourceType.LOCAL_INSTALL, version=None) diff --git a/airbyte-lib/airbyte_lib/caches/base.py b/airbyte-lib/airbyte_lib/caches/base.py index 298b3856e6317..bc92ea17a7430 100644 --- a/airbyte-lib/airbyte_lib/caches/base.py +++ b/airbyte-lib/airbyte_lib/caches/base.py @@ -24,6 +24,7 @@ from airbyte_lib._file_writers.base import FileWriterBase, FileWriterBatchHandle from airbyte_lib._processors import BatchHandle, RecordProcessor from airbyte_lib.config import CacheConfigBase +from airbyte_lib.telemetry import CacheTelemetryInfo from airbyte_lib.types import SQLTypeConverter @@ -736,3 +737,7 @@ def _table_exists( ) -> bool: """Return true if the given table exists.""" return table_name in self._get_tables_list() + + @abc.abstractmethod + def get_telemetry_info(self) -> CacheTelemetryInfo: + pass diff --git a/airbyte-lib/airbyte_lib/caches/duckdb.py b/airbyte-lib/airbyte_lib/caches/duckdb.py index e3d74d58aeb79..0672756584662 100644 --- a/airbyte-lib/airbyte_lib/caches/duckdb.py +++ b/airbyte-lib/airbyte_lib/caches/duckdb.py @@ -11,6 +11,7 @@ from airbyte_lib._file_writers import ParquetWriter, ParquetWriterConfig from airbyte_lib.caches.base import SQLCacheBase, SQLCacheConfigBase +from airbyte_lib.telemetry import CacheTelemetryInfo class DuckDBCacheConfig(SQLCacheConfigBase, ParquetWriterConfig): @@ -54,6 +55,10 @@ class DuckDBCacheBase(SQLCacheBase): config_class = DuckDBCacheConfig supports_merge_insert = True + @overrides + def get_telemetry_info(self) -> CacheTelemetryInfo: + return CacheTelemetryInfo("duckdb") + @overrides def _setup(self) -> None: """Create the database parent folder if it doesn't yet exist.""" diff --git a/airbyte-lib/airbyte_lib/caches/postgres.py b/airbyte-lib/airbyte_lib/caches/postgres.py index 6cbbd6cc21256..c833190e00ab7 100644 --- a/airbyte-lib/airbyte_lib/caches/postgres.py +++ b/airbyte-lib/airbyte_lib/caches/postgres.py @@ -8,6 +8,7 @@ from airbyte_lib._file_writers import ParquetWriter, ParquetWriterConfig from airbyte_lib.caches.base import SQLCacheBase, SQLCacheConfigBase +from airbyte_lib.telemetry import CacheTelemetryInfo class PostgresCacheConfig(SQLCacheConfigBase, ParquetWriterConfig): @@ -49,3 +50,7 @@ class PostgresCache(SQLCacheBase): config_class = PostgresCacheConfig file_writer_class = ParquetWriter supports_merge_insert = True + + @overrides + def get_telemetry_info(self) -> CacheTelemetryInfo: + return CacheTelemetryInfo("postgres") diff --git a/airbyte-lib/airbyte_lib/caches/snowflake.py b/airbyte-lib/airbyte_lib/caches/snowflake.py index 8118c173f4a9e..d731cbaca6a88 100644 --- a/airbyte-lib/airbyte_lib/caches/snowflake.py +++ b/airbyte-lib/airbyte_lib/caches/snowflake.py @@ -13,6 +13,7 @@ from airbyte_lib._file_writers import ParquetWriter, ParquetWriterConfig from airbyte_lib.caches.base import SQLCacheBase, SQLCacheConfigBase +from airbyte_lib.telemetry import CacheTelemetryInfo if TYPE_CHECKING: @@ -68,3 +69,7 @@ def _write_files_to_new_table( TODO: Override the base implementation to use the COPY command. """ return super()._write_files_to_new_table(files, stream_name, batch_id) + + @overrides + def get_telemetry_info(self) -> CacheTelemetryInfo: + return CacheTelemetryInfo("snowflake") diff --git a/airbyte-lib/airbyte_lib/registry.py b/airbyte-lib/airbyte_lib/registry.py index a8c964578ab66..baa4917959bb8 100644 --- a/airbyte-lib/airbyte_lib/registry.py +++ b/airbyte-lib/airbyte_lib/registry.py @@ -1,12 +1,13 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. -import importlib.metadata import json import os from dataclasses import dataclass import requests +from airbyte_lib.version import get_version + @dataclass class ConnectorMetadata: @@ -15,7 +16,6 @@ class ConnectorMetadata: _cache: dict[str, ConnectorMetadata] | None = None -airbyte_lib_version = importlib.metadata.version("airbyte-lib") REGISTRY_URL = "https://connectors.airbyte.com/files/registries/v0/oss_registry.json" @@ -27,7 +27,7 @@ def _update_cache() -> None: data = json.load(f) else: response = requests.get( - REGISTRY_URL, headers={"User-Agent": f"airbyte-lib-{airbyte_lib_version}"} + REGISTRY_URL, headers={"User-Agent": f"airbyte-lib-{get_version()}"} ) response.raise_for_status() data = response.json() diff --git a/airbyte-lib/airbyte_lib/source.py b/airbyte-lib/airbyte_lib/source.py index 1042203e11f36..0fcf6a535c35e 100644 --- a/airbyte-lib/airbyte_lib/source.py +++ b/airbyte-lib/airbyte_lib/source.py @@ -27,6 +27,12 @@ from airbyte_lib._util import protocol_util # Internal utility functions from airbyte_lib.caches import SQLCacheBase from airbyte_lib.results import ReadResult +from airbyte_lib.telemetry import ( + CacheTelemetryInfo, + SyncState, + send_telemetry, + streaming_cache_info, +) @contextmanager @@ -59,6 +65,7 @@ def __init__( config: Optional[dict[str, Any]] = None, streams: Optional[list[str]] = None, ): + self._processed_records = 0 self.executor = executor self.name = name self.streams: Optional[list[str]] = None @@ -199,7 +206,7 @@ def get_records(self, stream: str) -> Iterator[dict[str, Any]]: ) iterator: Iterable[dict[str, Any]] = protocol_util.airbyte_messages_to_record_dicts( - self._read_with_catalog(configured_catalog), + self._read_with_catalog(streaming_cache_info, configured_catalog), ) yield from iterator # TODO: Refactor to use LazyDataset here @@ -241,7 +248,7 @@ def uninstall(self) -> None: """ self.executor.uninstall() - def _read(self) -> Iterator[AirbyteMessage]: + def _read(self, cache_info: CacheTelemetryInfo) -> Iterable[AirbyteRecordMessage]: """ Call read on the connector. @@ -264,10 +271,11 @@ def _read(self) -> Iterator[AirbyteMessage]: if self.streams is None or s.name in self.streams ], ) - yield from self._read_with_catalog(configured_catalog) + yield from self._read_with_catalog(cache_info, configured_catalog) def _read_with_catalog( self, + cache_info: CacheTelemetryInfo, catalog: ConfiguredAirbyteCatalog, ) -> Iterator[AirbyteMessage]: """ @@ -277,15 +285,31 @@ def _read_with_catalog( * Write the config to a temporary file * execute the connector with read --config --catalog * Listen to the messages and return the AirbyteRecordMessages that come along. + * Send out telemetry on the performed sync (with information about which source was used and the type of the cache) """ - with as_temp_files([self._config, catalog.json()]) as [ - config_file, - catalog_file, - ]: - for msg in self._execute( - ["read", "--config", config_file, "--catalog", catalog_file], - ): - yield msg + source_tracking_information = self.executor.get_telemetry_info() + send_telemetry(source_tracking_information, cache_info, SyncState.STARTED) + try: + with as_temp_files([self._config, catalog.json()]) as [ + config_file, + catalog_file, + ]: + for msg in self._execute( + ["read", "--config", config_file, "--catalog", catalog_file], + ): + yield msg + except Exception as e: + send_telemetry( + source_tracking_information, cache_info, SyncState.FAILED, self._processed_records + ) + raise e + finally: + send_telemetry( + source_tracking_information, + cache_info, + SyncState.SUCCEEDED, + self._processed_records, + ) def _add_to_logs(self, message: str) -> None: self._last_log_messages.append(message) @@ -331,7 +355,7 @@ def read(self, cache: SQLCacheBase | None = None) -> ReadResult: cache = get_default_cache() cache.register_source(source_name=self.name, source_catalog=self.configured_catalog) - cache.process_airbyte_messages(self._tally_records(self._read())) + cache.process_airbyte_messages(self._tally_records(self._read(cache.get_telemetry_info()))) return ReadResult( processed_records=self._processed_records, diff --git a/airbyte-lib/airbyte_lib/telemetry.py b/airbyte-lib/airbyte_lib/telemetry.py new file mode 100644 index 0000000000000..fe797c9d82613 --- /dev/null +++ b/airbyte-lib/airbyte_lib/telemetry.py @@ -0,0 +1,78 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import datetime +import os +from contextlib import suppress +from dataclasses import asdict, dataclass +from enum import Enum +from typing import Any, Optional + +import requests + +from airbyte_lib.version import get_version + + +# TODO: Use production tracking key +TRACKING_KEY = "jxT1qP9WEKwR3vtKMwP9qKhfQEGFtIM1" or str(os.environ.get("AIRBYTE_TRACKING_KEY")) + + +class SourceType(str, Enum): + VENV = "venv" + LOCAL_INSTALL = "local_install" + + +@dataclass +class CacheTelemetryInfo: + type: str + + +streaming_cache_info = CacheTelemetryInfo("streaming") + + +class SyncState(str, Enum): + STARTED = "started" + FAILED = "failed" + SUCCEEDED = "succeeded" + + +@dataclass +class SourceTelemetryInfo: + name: str + type: SourceType + version: Optional[str] + + +def send_telemetry( + source_info: SourceTelemetryInfo, + cache_info: CacheTelemetryInfo, + state: SyncState, + number_of_records: Optional[int] = None, +) -> None: + # If DO_NOT_TRACK is set, we don't send any telemetry + if os.environ.get("DO_NOT_TRACK"): + return + + current_time = datetime.datetime.utcnow().isoformat() + payload: dict[str, Any] = { + "anonymousId": "airbyte-lib-user", + "event": "sync", + "properties": { + "version": get_version(), + "source": asdict(source_info), + "state": state, + "cache": asdict(cache_info), + # explicitly set to 0.0.0.0 to avoid leaking IP addresses + "ip": "0.0.0.0", + "flags": { + "CI": bool(os.environ.get("CI")), + }, + }, + "timestamp": current_time, + } + if number_of_records is not None: + payload["properties"]["number_of_records"] = number_of_records + + # Suppress exceptions if host is unreachable or network is unavailable + with suppress(Exception): + # Do not handle the response, we don't want to block the execution + _ = requests.post("https://api.segment.io/v1/track", auth=(TRACKING_KEY, ""), json=payload) diff --git a/airbyte-lib/airbyte_lib/version.py b/airbyte-lib/airbyte_lib/version.py new file mode 100644 index 0000000000000..9ed83a5ef4569 --- /dev/null +++ b/airbyte-lib/airbyte_lib/version.py @@ -0,0 +1,10 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import importlib.metadata + + +airbyte_lib_version = importlib.metadata.version("airbyte-lib") + + +def get_version() -> str: + return airbyte_lib_version diff --git a/airbyte-lib/docs/generated/airbyte_lib/caches.html b/airbyte-lib/docs/generated/airbyte_lib/caches.html index e6c3e5536a297..2434768cbe633 100644 --- a/airbyte-lib/docs/generated/airbyte_lib/caches.html +++ b/airbyte-lib/docs/generated/airbyte_lib/caches.html @@ -34,7 +34,7 @@
Inherited Members
SQLCacheBase
-
SQLCacheBase
+
SQLCacheBase
type_converter_class
use_singleton_connection
config
@@ -54,6 +54,7 @@
Inherited Members
airbyte_lib.caches.duckdb.DuckDBCacheBase
config_class
supports_merge_insert
+
get_telemetry_info
airbyte_lib._processors.RecordProcessor
@@ -234,12 +235,26 @@
Inherited Members
+
+
+
+
@overrides
+ + def + get_telemetry_info(self) -> airbyte_lib.telemetry.CacheTelemetryInfo: + + +
+ + + +
Inherited Members
SQLCacheBase
-
SQLCacheBase
+
SQLCacheBase
type_converter_class
use_singleton_connection
config
@@ -421,19 +436,6 @@
Inherited Members
-
-
-
@final
- - SQLCacheBase( config: airbyte_lib.caches.base.SQLCacheConfigBase | None = None, file_writer: airbyte_lib._file_writers.base.FileWriterBase | None = None, **kwargs: dict[str, typing.Any]) - - -
- - - - -
type_converter_class: type[airbyte_lib.types.SQLTypeConverter] = @@ -662,6 +664,20 @@

TODO: Refactor to return a L

+
+
+
+
@abc.abstractmethod
+ + def + get_telemetry_info(self) -> airbyte_lib.telemetry.CacheTelemetryInfo: + + +
+ + + +
Inherited Members
diff --git a/airbyte-lib/docs/generated/airbyte_lib/factories.html b/airbyte-lib/docs/generated/airbyte_lib/factories.html new file mode 100644 index 0000000000000..c0d27ca14eaa0 --- /dev/null +++ b/airbyte-lib/docs/generated/airbyte_lib/factories.html @@ -0,0 +1,7 @@ + +
+
+ + + + \ No newline at end of file diff --git a/airbyte-lib/examples/run_test_source_single_stream.py b/airbyte-lib/examples/run_test_source_single_stream.py new file mode 100644 index 0000000000000..b1cc55cd1f5c7 --- /dev/null +++ b/airbyte-lib/examples/run_test_source_single_stream.py @@ -0,0 +1,19 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import os + +import airbyte_lib as ab + + +# preparation (from airbyte-lib main folder): +# python -m venv .venv-source-test +# source .venv-source-test/bin/activate +# pip install -e ./tests/integration_tests/fixtures/source-test +# In separate terminal: +# poetry run python examples/run_test_source.py + +os.environ["AIRBYTE_LOCAL_REGISTRY"] = "./tests/integration_tests/fixtures/registry.json" + +source = ab.get_connector("source-test", config={"apiKey": "test"}) + +print(list(source.read_stream("stream1"))) diff --git a/airbyte-lib/tests/integration_tests/fixtures/source-test/source_test/run.py b/airbyte-lib/tests/integration_tests/fixtures/source-test/source_test/run.py index 518d35e81576a..b200e4a84f109 100644 --- a/airbyte-lib/tests/integration_tests/fixtures/source-test/source_test/run.py +++ b/airbyte-lib/tests/integration_tests/fixtures/source-test/source_test/run.py @@ -115,16 +115,19 @@ def run(): elif args[0] == "check": args = parse_args() config = get_json_file(args["--config"]) - if config.get("apiKey") == "test": + if config.get("apiKey").startswith("test"): print(json.dumps(sample_connection_check_success)) else: print(json.dumps(sample_connection_check_failure)) elif args[0] == "read": args = parse_args() catalog = get_json_file(args["--catalog"]) + config = get_json_file(args["--config"]) for stream in catalog["streams"]: if stream["stream"]["name"] == "stream1": print(json.dumps(sample_record1_stream1)) + if config.get("apiKey") == "test_fail_during_sync": + raise Exception("An error") print(json.dumps(sample_record2_stream1)) elif stream["stream"]["name"] == "stream2": print(json.dumps(sample_record_stream2)) diff --git a/airbyte-lib/tests/integration_tests/test_integration.py b/airbyte-lib/tests/integration_tests/test_integration.py index 2be56ece60a97..0a65abbc41586 100644 --- a/airbyte-lib/tests/integration_tests/test_integration.py +++ b/airbyte-lib/tests/integration_tests/test_integration.py @@ -2,6 +2,7 @@ import os import shutil +from unittest.mock import Mock, call, patch import tempfile from pathlib import Path @@ -11,6 +12,7 @@ from airbyte_lib.caches import PostgresCache, PostgresCacheConfig from airbyte_lib.registry import _update_cache +from airbyte_lib.version import get_version from airbyte_lib.results import ReadResult @@ -25,6 +27,7 @@ def prepare_test_env(): os.system("python -m venv .venv-source-test") os.system("source .venv-source-test/bin/activate && pip install -e ./tests/integration_tests/fixtures/source-test") os.environ["AIRBYTE_LOCAL_REGISTRY"] = "./tests/integration_tests/fixtures/registry.json" + os.environ["DO_NOT_TRACK"] = "true" yield @@ -216,6 +219,83 @@ def test_sync_with_merge_to_postgres(new_pg_cache_config: PostgresCacheConfig, e check_dtype=False, ) +@patch.dict('os.environ', {'DO_NOT_TRACK': ''}) +@patch('airbyte_lib.telemetry.requests') +@patch('airbyte_lib.telemetry.datetime') +@pytest.mark.parametrize( + "raises, api_key, expected_state, expected_number_of_records, request_call_fails, extra_env, expected_flags", + [ + pytest.param(True, "test_fail_during_sync", "failed", 1, False, {}, {"CI": False}, id="fail_during_sync"), + pytest.param(False, "test", "succeeded", 3, False, {}, {"CI": False}, id="succeed_during_sync"), + pytest.param(False, "test", "succeeded", 3, True, {}, {"CI": False}, id="fail_request_without_propagating"), + pytest.param(False, "test", "succeeded", 3, False, {"CI": ""}, {"CI": False}, id="falsy_ci_flag"), + pytest.param(False, "test", "succeeded", 3, False, {"CI": "true"}, {"CI": True}, id="truthy_ci_flag"), + ], +) +def test_tracking(mock_datetime: Mock, mock_requests: Mock, raises: bool, api_key: str, expected_state: str, expected_number_of_records: int, request_call_fails: bool, extra_env: dict[str, str], expected_flags: dict[str, bool]): + """ + Test that the telemetry is sent when the sync is successful. + This is done by mocking the requests.post method and checking that it is called with the right arguments. + """ + now_date = Mock() + mock_datetime.datetime = Mock() + mock_datetime.datetime.utcnow.return_value = now_date + now_date.isoformat.return_value = "2021-01-01T00:00:00.000000" + + mock_post = Mock() + mock_requests.post = mock_post + + source = ab.get_connector("source-test", config={"apiKey": api_key}) + cache = ab.get_default_cache() + + if request_call_fails: + mock_post.side_effect = Exception("test exception") + + with patch.dict('os.environ', extra_env): + if raises: + with pytest.raises(Exception): + source.read(cache) + else: + source.read(cache) + + + mock_post.assert_has_calls([ + call("https://api.segment.io/v1/track", + auth=("jxT1qP9WEKwR3vtKMwP9qKhfQEGFtIM1", ""), + json={ + "anonymousId": "airbyte-lib-user", + "event": "sync", + "properties": { + "version": get_version(), + "source": {'name': 'source-test', 'version': '0.0.1', 'type': 'venv'}, + "state": "started", + "cache": {"type": "duckdb"}, + "ip": "0.0.0.0", + "flags": expected_flags + }, + "timestamp": "2021-01-01T00:00:00.000000", + } + ), + call( + "https://api.segment.io/v1/track", + auth=("jxT1qP9WEKwR3vtKMwP9qKhfQEGFtIM1", ""), + json={ + "anonymousId": "airbyte-lib-user", + "event": "sync", + "properties": { + "version": get_version(), + "source": {'name': 'source-test', 'version': '0.0.1', 'type': 'venv'}, + "state": expected_state, + "number_of_records": expected_number_of_records, + "cache": {"type": "duckdb"}, + "ip": "0.0.0.0", + "flags": expected_flags + }, + "timestamp": "2021-01-01T00:00:00.000000", + } + ) + ]) + def test_sync_to_postgres(new_pg_cache_config: PostgresCacheConfig, expected_test_stream_data: dict[str, list[dict[str, str | int]]]): source = ab.get_connector("source-test", config={"apiKey": "test"}) From e4a7863585eaaf4f50a619d8963032529ced42f1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jan 2024 10:41:14 +0100 Subject: [PATCH 101/574] source-mailchimp: Convert to airbyte-lib (#34157) --- .../connectors/source-mailchimp/main.py | 9 ++------- .../connectors/source-mailchimp/metadata.yaml | 2 +- .../connectors/source-mailchimp/setup.py | 5 +++++ .../source-mailchimp/source_mailchimp/run.py | 14 ++++++++++++++ docs/integrations/sources/mailchimp.md | 1 + 5 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 airbyte-integrations/connectors/source-mailchimp/source_mailchimp/run.py diff --git a/airbyte-integrations/connectors/source-mailchimp/main.py b/airbyte-integrations/connectors/source-mailchimp/main.py index b95b566e6b8a5..c61875fb7a72e 100644 --- a/airbyte-integrations/connectors/source-mailchimp/main.py +++ b/airbyte-integrations/connectors/source-mailchimp/main.py @@ -2,12 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_mailchimp import SourceMailchimp +from source_mailchimp.run import run if __name__ == "__main__": - source = SourceMailchimp() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-mailchimp/metadata.yaml b/airbyte-integrations/connectors/source-mailchimp/metadata.yaml index c6eddb6b6efad..4753d72e21da1 100644 --- a/airbyte-integrations/connectors/source-mailchimp/metadata.yaml +++ b/airbyte-integrations/connectors/source-mailchimp/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: b03a9f3e-22a5-11eb-adc1-0242ac120002 - dockerImageTag: 1.1.0 + dockerImageTag: 1.1.1 dockerRepository: airbyte/source-mailchimp documentationUrl: https://docs.airbyte.com/integrations/sources/mailchimp githubIssueLabel: source-mailchimp diff --git a/airbyte-integrations/connectors/source-mailchimp/setup.py b/airbyte-integrations/connectors/source-mailchimp/setup.py index f2973669a61e6..0773da0844844 100644 --- a/airbyte-integrations/connectors/source-mailchimp/setup.py +++ b/airbyte-integrations/connectors/source-mailchimp/setup.py @@ -9,6 +9,11 @@ setup( + entry_points={ + "console_scripts": [ + "source-mailchimp=source_mailchimp.run:run", + ], + }, name="source_mailchimp", description="Source implementation for Mailchimp.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-mailchimp/source_mailchimp/run.py b/airbyte-integrations/connectors/source-mailchimp/source_mailchimp/run.py new file mode 100644 index 0000000000000..15226fdfeebd0 --- /dev/null +++ b/airbyte-integrations/connectors/source-mailchimp/source_mailchimp/run.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_mailchimp import SourceMailchimp + + +def run(): + source = SourceMailchimp() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/mailchimp.md b/docs/integrations/sources/mailchimp.md index 1d4193aaa96c9..106112d4dc7c0 100644 --- a/docs/integrations/sources/mailchimp.md +++ b/docs/integrations/sources/mailchimp.md @@ -123,6 +123,7 @@ Now that you have set up the Mailchimp source connector, check out the following | Version | Date | Pull Request | Subject | |---------|------------|----------------------------------------------------------|----------------------------------------------------------------------------| +| 1.1.1 | 2024-01-11 | [34157](https://github.com/airbytehq/airbyte/pull/34157) | Prepare for airbyte-lib | | 1.1.0 | 2023-12-20 | [32852](https://github.com/airbytehq/airbyte/pull/32852) | Add optional start_date for incremental streams | | 1.0.0 | 2023-12-19 | [32836](https://github.com/airbytehq/airbyte/pull/32836) | Add airbyte-type to `datetime` columns and remove `._links` column | | 0.10.0 | 2023-11-23 | [32782](https://github.com/airbytehq/airbyte/pull/32782) | Add SegmentMembers stream | From f532d0dcc4d794a4b2afd99b10c46a5c5bd35b08 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jan 2024 10:56:29 +0100 Subject: [PATCH 102/574] source-pipedrive: Convert to airbyte-lib (#34153) Co-authored-by: Serhii Lazebnyi <53845333+lazebnyi@users.noreply.github.com> --- .../connectors/source-pipedrive/Dockerfile | 2 +- .../connectors/source-pipedrive/main.py | 9 ++------- .../connectors/source-pipedrive/metadata.yaml | 2 +- .../connectors/source-pipedrive/setup.py | 5 +++++ .../source-pipedrive/source_pipedrive/run.py | 14 ++++++++++++++ docs/integrations/sources/pipedrive.md | 1 + 6 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 airbyte-integrations/connectors/source-pipedrive/source_pipedrive/run.py diff --git a/airbyte-integrations/connectors/source-pipedrive/Dockerfile b/airbyte-integrations/connectors/source-pipedrive/Dockerfile index f82230e8fdda4..8698c9a3fad56 100644 --- a/airbyte-integrations/connectors/source-pipedrive/Dockerfile +++ b/airbyte-integrations/connectors/source-pipedrive/Dockerfile @@ -34,5 +34,5 @@ COPY source_pipedrive ./source_pipedrive ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] -LABEL io.airbyte.version=2.2.1 +LABEL io.airbyte.version=2.2.2 LABEL io.airbyte.name=airbyte/source-pipedrive diff --git a/airbyte-integrations/connectors/source-pipedrive/main.py b/airbyte-integrations/connectors/source-pipedrive/main.py index fb481bc2e9b2e..64fe456c34fd1 100644 --- a/airbyte-integrations/connectors/source-pipedrive/main.py +++ b/airbyte-integrations/connectors/source-pipedrive/main.py @@ -2,12 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_pipedrive import SourcePipedrive +from source_pipedrive.run import run if __name__ == "__main__": - source = SourcePipedrive() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-pipedrive/metadata.yaml b/airbyte-integrations/connectors/source-pipedrive/metadata.yaml index e38dd09b1c37e..74b572e3fad6e 100644 --- a/airbyte-integrations/connectors/source-pipedrive/metadata.yaml +++ b/airbyte-integrations/connectors/source-pipedrive/metadata.yaml @@ -13,7 +13,7 @@ data: connectorSubtype: api connectorType: source definitionId: d8286229-c680-4063-8c59-23b9b391c700 - dockerImageTag: 2.2.1 + dockerImageTag: 2.2.2 dockerRepository: airbyte/source-pipedrive documentationUrl: https://docs.airbyte.com/integrations/sources/pipedrive githubIssueLabel: source-pipedrive diff --git a/airbyte-integrations/connectors/source-pipedrive/setup.py b/airbyte-integrations/connectors/source-pipedrive/setup.py index 7e72591c87972..5d3c1999b80c8 100644 --- a/airbyte-integrations/connectors/source-pipedrive/setup.py +++ b/airbyte-integrations/connectors/source-pipedrive/setup.py @@ -15,6 +15,11 @@ ] setup( + entry_points={ + "console_scripts": [ + "source-pipedrive=source_pipedrive.run:run", + ], + }, name="source_pipedrive", description="Source implementation for Pipedrive.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-pipedrive/source_pipedrive/run.py b/airbyte-integrations/connectors/source-pipedrive/source_pipedrive/run.py new file mode 100644 index 0000000000000..2ff2b80c12a81 --- /dev/null +++ b/airbyte-integrations/connectors/source-pipedrive/source_pipedrive/run.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_pipedrive import SourcePipedrive + + +def run(): + source = SourcePipedrive() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/pipedrive.md b/docs/integrations/sources/pipedrive.md index 32a0bc0781c93..cb87f1d27fb2b 100644 --- a/docs/integrations/sources/pipedrive.md +++ b/docs/integrations/sources/pipedrive.md @@ -114,6 +114,7 @@ The Pipedrive connector will gracefully handle rate limits. For more information | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:---------------------------------------------------------------------------| +| 2.2.2 | 2024-01-11 | [34153](https://github.com/airbytehq/airbyte/pull/34153) | prepare for airbyte-lib | | 2.2.1 | 2023-11-06 | [31147](https://github.com/airbytehq/airbyte/pull/31147) | Bugfix: handle records with a null data field | | 2.2.0 | 2023-10-25 | [31707](https://github.com/airbytehq/airbyte/pull/31707) | Add new stream mail | | 2.1.0 | 2023-10-10 | [31184](https://github.com/airbytehq/airbyte/pull/31184) | Add new stream goals | From c5b9421c31c2d863c5fd42f4bdac8e109f8fe315 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jan 2024 10:57:08 +0100 Subject: [PATCH 103/574] source-xero: Convert to airbyte-lib (#34154) Co-authored-by: Serhii Lazebnyi <53845333+lazebnyi@users.noreply.github.com> --- .../connectors/source-xero/Dockerfile | 2 +- .../connectors/source-xero/main.py | 9 ++------- .../connectors/source-xero/metadata.yaml | 2 +- .../connectors/source-xero/setup.py | 5 +++++ .../connectors/source-xero/source_xero/run.py | 14 ++++++++++++++ docs/integrations/sources/xero.md | 1 + 6 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 airbyte-integrations/connectors/source-xero/source_xero/run.py diff --git a/airbyte-integrations/connectors/source-xero/Dockerfile b/airbyte-integrations/connectors/source-xero/Dockerfile index f3099f93deb28..b634bf69f6a1a 100644 --- a/airbyte-integrations/connectors/source-xero/Dockerfile +++ b/airbyte-integrations/connectors/source-xero/Dockerfile @@ -34,5 +34,5 @@ COPY source_xero ./source_xero ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] -LABEL io.airbyte.version=0.2.4 +LABEL io.airbyte.version=0.2.5 LABEL io.airbyte.name=airbyte/source-xero diff --git a/airbyte-integrations/connectors/source-xero/main.py b/airbyte-integrations/connectors/source-xero/main.py index ecb627ec90dd0..d765f10d2093b 100644 --- a/airbyte-integrations/connectors/source-xero/main.py +++ b/airbyte-integrations/connectors/source-xero/main.py @@ -2,12 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_xero import SourceXero +from source_xero.run import run if __name__ == "__main__": - source = SourceXero() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-xero/metadata.yaml b/airbyte-integrations/connectors/source-xero/metadata.yaml index da8edb234071b..6edc99384fe70 100644 --- a/airbyte-integrations/connectors/source-xero/metadata.yaml +++ b/airbyte-integrations/connectors/source-xero/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: api connectorType: source definitionId: 6fd1e833-dd6e-45ec-a727-ab917c5be892 - dockerImageTag: 0.2.4 + dockerImageTag: 0.2.5 dockerRepository: airbyte/source-xero githubIssueLabel: source-xero icon: xero.svg diff --git a/airbyte-integrations/connectors/source-xero/setup.py b/airbyte-integrations/connectors/source-xero/setup.py index f44d404f36c22..89541436cfd3b 100644 --- a/airbyte-integrations/connectors/source-xero/setup.py +++ b/airbyte-integrations/connectors/source-xero/setup.py @@ -16,6 +16,11 @@ ] setup( + entry_points={ + "console_scripts": [ + "source-xero=source_xero.run:run", + ], + }, name="source_xero", description="Source implementation for Xero.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-xero/source_xero/run.py b/airbyte-integrations/connectors/source-xero/source_xero/run.py new file mode 100644 index 0000000000000..fb8d5955af03d --- /dev/null +++ b/airbyte-integrations/connectors/source-xero/source_xero/run.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_xero import SourceXero + + +def run(): + source = SourceXero() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/xero.md b/docs/integrations/sources/xero.md index 738378e3e68e8..1e049713fcf77 100644 --- a/docs/integrations/sources/xero.md +++ b/docs/integrations/sources/xero.md @@ -104,6 +104,7 @@ The connector is restricted by Xero [API rate limits](https://developer.xero.com | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:----------------------------------| +| 0.2.5 | 2024-01-11 | [34154](https://github.com/airbytehq/airbyte/pull/34154) | prepare for airbyte-lib | | 0.2.4 | 2023-11-24 | [32837](https://github.com/airbytehq/airbyte/pull/32837) | Handle 403 error | | 0.2.3 | 2023-06-19 | [27471](https://github.com/airbytehq/airbyte/pull/27471) | Update CDK to 0.40 | | 0.2.2 | 2023-06-06 | [27007](https://github.com/airbytehq/airbyte/pull/27007) | Update CDK | From e7f503982f4f8b970ff81d3c28f2ff968ba40b08 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jan 2024 10:57:54 +0100 Subject: [PATCH 104/574] source-iterable: Convert to airbyte-lib (#34208) --- .../connectors/source-iterable/main.py | 9 ++------- .../connectors/source-iterable/metadata.yaml | 2 +- .../connectors/source-iterable/setup.py | 5 +++++ .../source-iterable/source_iterable/run.py | 14 ++++++++++++++ docs/integrations/sources/iterable.md | 1 + 5 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 airbyte-integrations/connectors/source-iterable/source_iterable/run.py diff --git a/airbyte-integrations/connectors/source-iterable/main.py b/airbyte-integrations/connectors/source-iterable/main.py index 3a4a2f7982ffa..eef7d894cbc44 100644 --- a/airbyte-integrations/connectors/source-iterable/main.py +++ b/airbyte-integrations/connectors/source-iterable/main.py @@ -2,12 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_iterable import SourceIterable +from source_iterable.run import run if __name__ == "__main__": - source = SourceIterable() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-iterable/metadata.yaml b/airbyte-integrations/connectors/source-iterable/metadata.yaml index d62ca667d12f6..94c0cd2c6846a 100644 --- a/airbyte-integrations/connectors/source-iterable/metadata.yaml +++ b/airbyte-integrations/connectors/source-iterable/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: 2e875208-0c0b-4ee4-9e92-1cb3156ea799 - dockerImageTag: 0.2.0 + dockerImageTag: 0.2.1 dockerRepository: airbyte/source-iterable documentationUrl: https://docs.airbyte.com/integrations/sources/iterable githubIssueLabel: source-iterable diff --git a/airbyte-integrations/connectors/source-iterable/setup.py b/airbyte-integrations/connectors/source-iterable/setup.py index 5d2e499d31c7c..fd2061fb89fb7 100644 --- a/airbyte-integrations/connectors/source-iterable/setup.py +++ b/airbyte-integrations/connectors/source-iterable/setup.py @@ -16,6 +16,11 @@ setup( + entry_points={ + "console_scripts": [ + "source-iterable=source_iterable.run:run", + ], + }, name="source_iterable", description="Source implementation for Iterable.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-iterable/source_iterable/run.py b/airbyte-integrations/connectors/source-iterable/source_iterable/run.py new file mode 100644 index 0000000000000..c2e01ead95e51 --- /dev/null +++ b/airbyte-integrations/connectors/source-iterable/source_iterable/run.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_iterable import SourceIterable + + +def run(): + source = SourceIterable() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/iterable.md b/docs/integrations/sources/iterable.md index f58adccd0b172..0b68d7856f2f7 100644 --- a/docs/integrations/sources/iterable.md +++ b/docs/integrations/sources/iterable.md @@ -80,6 +80,7 @@ The Iterable source connector supports the following [sync modes](https://docs.a | Version | Date | Pull Request | Subject | | :------ | :--------- | :------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------- | +| 0.2.1 | 2024-01-12 | [1234](https://github.com/airbytehq/airbyte/pull/1234) | prepare for airbyte-lib | | 0.2.0 | 2023-09-29 | [28457](https://github.com/airbytehq/airbyte/pull/30931) | Added `userId` to `email_bounce`, `email_click`, `email_complaint`, `email_open`, `email_send` `email_send_skip`, `email_subscribe`, `email_unsubscribe`, `events` streams | | 0.1.31 | 2023-12-06 | [33106](https://github.com/airbytehq/airbyte/pull/33106) | Base image migration: remove Dockerfile and use the python-connector-base image | | 0.1.30 | 2023-07-19 | [28457](https://github.com/airbytehq/airbyte/pull/28457) | Fixed TypeError for StreamSlice in debug mode | From 500a107c3d4173262cfcac919472cf8ee3aa677c Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jan 2024 11:06:02 +0100 Subject: [PATCH 105/574] Source Typeform: Convert to airbyte-lib (#34145) Co-authored-by: Serhii Lazebnyi <53845333+lazebnyi@users.noreply.github.com> --- .../connectors/source-typeform/.coveragerc | 3 +++ .../connectors/source-typeform/main.py | 9 ++------- .../connectors/source-typeform/metadata.yaml | 2 +- .../connectors/source-typeform/setup.py | 5 +++++ .../source-typeform/source_typeform/run.py | 14 ++++++++++++++ docs/integrations/sources/typeform.md | 1 + 6 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 airbyte-integrations/connectors/source-typeform/.coveragerc create mode 100644 airbyte-integrations/connectors/source-typeform/source_typeform/run.py diff --git a/airbyte-integrations/connectors/source-typeform/.coveragerc b/airbyte-integrations/connectors/source-typeform/.coveragerc new file mode 100644 index 0000000000000..2c98af4de877d --- /dev/null +++ b/airbyte-integrations/connectors/source-typeform/.coveragerc @@ -0,0 +1,3 @@ +[run] +omit = + source_typeform/run.py \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-typeform/main.py b/airbyte-integrations/connectors/source-typeform/main.py index 332857e87cdd4..126dc556ff7db 100644 --- a/airbyte-integrations/connectors/source-typeform/main.py +++ b/airbyte-integrations/connectors/source-typeform/main.py @@ -2,12 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_typeform import SourceTypeform +from source_typeform.run import run if __name__ == "__main__": - source = SourceTypeform() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-typeform/metadata.yaml b/airbyte-integrations/connectors/source-typeform/metadata.yaml index decf1d93ed438..f8996479b0112 100644 --- a/airbyte-integrations/connectors/source-typeform/metadata.yaml +++ b/airbyte-integrations/connectors/source-typeform/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: e7eff203-90bf-43e5-a240-19ea3056c474 - dockerImageTag: 1.2.2 + dockerImageTag: 1.2.3 dockerRepository: airbyte/source-typeform documentationUrl: https://docs.airbyte.com/integrations/sources/typeform githubIssueLabel: source-typeform diff --git a/airbyte-integrations/connectors/source-typeform/setup.py b/airbyte-integrations/connectors/source-typeform/setup.py index 282003bf90772..09b119b93869c 100644 --- a/airbyte-integrations/connectors/source-typeform/setup.py +++ b/airbyte-integrations/connectors/source-typeform/setup.py @@ -10,6 +10,11 @@ TEST_REQUIREMENTS = ["requests-mock~=1.9.3", "pytest~=6.2", "pytest-mock~=3.6.1"] setup( + entry_points={ + "console_scripts": [ + "source-typeform=source_typeform.run:run", + ], + }, name="source_typeform", description="Source implementation for Typeform.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-typeform/source_typeform/run.py b/airbyte-integrations/connectors/source-typeform/source_typeform/run.py new file mode 100644 index 0000000000000..2ebf804b49403 --- /dev/null +++ b/airbyte-integrations/connectors/source-typeform/source_typeform/run.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_typeform import SourceTypeform + + +def run(): + source = SourceTypeform() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/typeform.md b/docs/integrations/sources/typeform.md index c846d5b3131a0..18c31df86cd32 100644 --- a/docs/integrations/sources/typeform.md +++ b/docs/integrations/sources/typeform.md @@ -90,6 +90,7 @@ API rate limits \(2 requests per second\): [https://developer.typeform.com/get-s | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:------------------------------------------------------------------------------------------------| +| 1.2.3 | 2024-01-11 | [34145](https://github.com/airbytehq/airbyte/pull/34145) | prepare for airbyte-lib | | 1.2.2 | 2023-12-12 | [33345](https://github.com/airbytehq/airbyte/pull/33345) | Fix single use refresh token authentication | | 1.2.1 | 2023-12-04 | [32775](https://github.com/airbytehq/airbyte/pull/32775) | Add 499 status code handling | | 1.2.0 | 2023-11-29 | [32745](https://github.com/airbytehq/airbyte/pull/32745) | Add `response_type` field to `responses` schema | From a182869e386444d40b7ab631343000cbe09514c6 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jan 2024 11:06:58 +0100 Subject: [PATCH 106/574] source-gitlab: Convert to airbyte-lib (#34203) Co-authored-by: Serhii Lazebnyi <53845333+lazebnyi@users.noreply.github.com> --- .../connectors/source-gitlab/main.py | 12 ++---------- .../connectors/source-gitlab/metadata.yaml | 2 +- .../connectors/source-gitlab/setup.py | 5 +++++ .../source-gitlab/source_gitlab/run.py | 17 +++++++++++++++++ docs/integrations/sources/gitlab.md | 1 + 5 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 airbyte-integrations/connectors/source-gitlab/source_gitlab/run.py diff --git a/airbyte-integrations/connectors/source-gitlab/main.py b/airbyte-integrations/connectors/source-gitlab/main.py index 12b7cc8416915..1c322c2f2c48a 100644 --- a/airbyte-integrations/connectors/source-gitlab/main.py +++ b/airbyte-integrations/connectors/source-gitlab/main.py @@ -2,15 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_gitlab import SourceGitlab -from source_gitlab.config_migrations import MigrateGroups, MigrateProjects +from source_gitlab.run import run if __name__ == "__main__": - source = SourceGitlab() - MigrateGroups.migrate(sys.argv[1:], source) - MigrateProjects.migrate(sys.argv[1:], source) - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-gitlab/metadata.yaml b/airbyte-integrations/connectors/source-gitlab/metadata.yaml index e79748b9025d6..381278747deee 100644 --- a/airbyte-integrations/connectors/source-gitlab/metadata.yaml +++ b/airbyte-integrations/connectors/source-gitlab/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: 5e6175e5-68e1-4c17-bff9-56103bbb0d80 - dockerImageTag: 2.1.0 + dockerImageTag: 2.1.1 dockerRepository: airbyte/source-gitlab documentationUrl: https://docs.airbyte.com/integrations/sources/gitlab githubIssueLabel: source-gitlab diff --git a/airbyte-integrations/connectors/source-gitlab/setup.py b/airbyte-integrations/connectors/source-gitlab/setup.py index 682fadb8af03f..2d16bcd7d0583 100644 --- a/airbyte-integrations/connectors/source-gitlab/setup.py +++ b/airbyte-integrations/connectors/source-gitlab/setup.py @@ -10,6 +10,11 @@ TEST_REQUIREMENTS = ["requests-mock~=1.9.3", "pytest~=6.1", "requests_mock", "pytest-mock"] setup( + entry_points={ + "console_scripts": [ + "source-gitlab=source_gitlab.run:run", + ], + }, name="source_gitlab", description="Source implementation for Gitlab.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-gitlab/source_gitlab/run.py b/airbyte-integrations/connectors/source-gitlab/source_gitlab/run.py new file mode 100644 index 0000000000000..ddaf36b55b1c3 --- /dev/null +++ b/airbyte-integrations/connectors/source-gitlab/source_gitlab/run.py @@ -0,0 +1,17 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_gitlab import SourceGitlab +from source_gitlab.config_migrations import MigrateGroups, MigrateProjects + + +def run(): + source = SourceGitlab() + MigrateGroups.migrate(sys.argv[1:], source) + MigrateProjects.migrate(sys.argv[1:], source) + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/gitlab.md b/docs/integrations/sources/gitlab.md index 4e919fc6338d3..170f4b94c38db 100644 --- a/docs/integrations/sources/gitlab.md +++ b/docs/integrations/sources/gitlab.md @@ -109,6 +109,7 @@ Gitlab has the [rate limits](https://docs.gitlab.com/ee/user/gitlab_com/index.ht | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 2.1.1 | 2024-01-12 | [34203](https://github.com/airbytehq/airbyte/pull/34203) | prepare for airbyte-lib | | 2.1.0 | 2023-12-20 | [33676](https://github.com/airbytehq/airbyte/pull/33676) | Add fields to Commits (extended_trailers), Groups (emails_enabled, service_access_tokens_expiration_enforced) and Projects (code_suggestions, model_registry_access_level) streams | | 2.0.0 | 2023-10-23 | [31700](https://github.com/airbytehq/airbyte/pull/31700) | Add correct date-time format for Deployments, Projects and Groups Members streams | | 1.8.4 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | From f23513954be774d5f174fcc63e87a20cb40d0e06 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jan 2024 11:07:20 +0100 Subject: [PATCH 107/574] source-paypal-transaction: Convert to airbyte-lib (#34155) Co-authored-by: Serhii Lazebnyi <53845333+lazebnyi@users.noreply.github.com> --- .../connectors/source-paypal-transaction/main.py | 9 ++------- .../source-paypal-transaction/metadata.yaml | 2 +- .../connectors/source-paypal-transaction/setup.py | 5 +++++ .../source_paypal_transaction/run.py | 14 ++++++++++++++ docs/integrations/sources/paypal-transaction.md | 1 + 5 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 airbyte-integrations/connectors/source-paypal-transaction/source_paypal_transaction/run.py diff --git a/airbyte-integrations/connectors/source-paypal-transaction/main.py b/airbyte-integrations/connectors/source-paypal-transaction/main.py index 51be49033dca5..06823a4a71e50 100644 --- a/airbyte-integrations/connectors/source-paypal-transaction/main.py +++ b/airbyte-integrations/connectors/source-paypal-transaction/main.py @@ -2,12 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_paypal_transaction import SourcePaypalTransaction +from source_paypal_transaction.run import run if __name__ == "__main__": - source = SourcePaypalTransaction() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-paypal-transaction/metadata.yaml b/airbyte-integrations/connectors/source-paypal-transaction/metadata.yaml index 1821fdddaddcd..d456b79b905f3 100644 --- a/airbyte-integrations/connectors/source-paypal-transaction/metadata.yaml +++ b/airbyte-integrations/connectors/source-paypal-transaction/metadata.yaml @@ -11,7 +11,7 @@ data: connectorSubtype: api connectorType: source definitionId: d913b0f2-cc51-4e55-a44c-8ba1697b9239 - dockerImageTag: 2.2.0 + dockerImageTag: 2.2.1 dockerRepository: airbyte/source-paypal-transaction documentationUrl: https://docs.airbyte.com/integrations/sources/paypal-transaction githubIssueLabel: source-paypal-transaction diff --git a/airbyte-integrations/connectors/source-paypal-transaction/setup.py b/airbyte-integrations/connectors/source-paypal-transaction/setup.py index 3ecf3c37436cf..a7f633e8ca287 100644 --- a/airbyte-integrations/connectors/source-paypal-transaction/setup.py +++ b/airbyte-integrations/connectors/source-paypal-transaction/setup.py @@ -16,6 +16,11 @@ ] setup( + entry_points={ + "console_scripts": [ + "source-paypal-transaction=source_paypal_transaction.run:run", + ], + }, name="source_paypal_transaction", description="Source implementation for Paypal Transaction.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-paypal-transaction/source_paypal_transaction/run.py b/airbyte-integrations/connectors/source-paypal-transaction/source_paypal_transaction/run.py new file mode 100644 index 0000000000000..1a6d4cc56c0ed --- /dev/null +++ b/airbyte-integrations/connectors/source-paypal-transaction/source_paypal_transaction/run.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_paypal_transaction import SourcePaypalTransaction + + +def run(): + source = SourcePaypalTransaction() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/paypal-transaction.md b/docs/integrations/sources/paypal-transaction.md index 0abf7725b5400..9390c59dac6f7 100644 --- a/docs/integrations/sources/paypal-transaction.md +++ b/docs/integrations/sources/paypal-transaction.md @@ -87,6 +87,7 @@ By default, syncs are performed with a slice period of 7 days. If you see errors | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------| +| 2.2.1 | 2024-01-11 | [34155](https://github.com/airbytehq/airbyte/pull/34155) | prepare for airbyte-lib | | 2.2.0 | 2023-10-25 | [31852](https://github.com/airbytehq/airbyte/pull/31852) | The size of the time_window can be configured | | 2.1.2 | 2023-10-23 | [31759](https://github.com/airbytehq/airbyte/pull/31759) | Keep transaction_id as a string and fetch data in 7-day batches | 2.1.1 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | From 2662f07b3da2927c23d0df2b9cac03db4b432a9e Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jan 2024 12:35:42 +0100 Subject: [PATCH 108/574] source-linkedin-ads: Convert to airbyte-lib (#34152) Co-authored-by: Serhii Lazebnyi <53845333+lazebnyi@users.noreply.github.com> --- .../connectors/source-linkedin-ads/.coveragerc | 3 +++ .../connectors/source-linkedin-ads/main.py | 9 ++------- .../connectors/source-linkedin-ads/metadata.yaml | 2 +- .../connectors/source-linkedin-ads/setup.py | 5 +++++ .../source-linkedin-ads/source_linkedin_ads/run.py | 14 ++++++++++++++ docs/integrations/sources/linkedin-ads.md | 1 + 6 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 airbyte-integrations/connectors/source-linkedin-ads/.coveragerc create mode 100644 airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/run.py diff --git a/airbyte-integrations/connectors/source-linkedin-ads/.coveragerc b/airbyte-integrations/connectors/source-linkedin-ads/.coveragerc new file mode 100644 index 0000000000000..6b0b0af5e2ce4 --- /dev/null +++ b/airbyte-integrations/connectors/source-linkedin-ads/.coveragerc @@ -0,0 +1,3 @@ +[run] +omit = + source_linkedin_ads/run.py \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-linkedin-ads/main.py b/airbyte-integrations/connectors/source-linkedin-ads/main.py index c51fcd1a5cc50..899a7e8614a4e 100644 --- a/airbyte-integrations/connectors/source-linkedin-ads/main.py +++ b/airbyte-integrations/connectors/source-linkedin-ads/main.py @@ -2,12 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_linkedin_ads import SourceLinkedinAds +from source_linkedin_ads.run import run if __name__ == "__main__": - source = SourceLinkedinAds() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-linkedin-ads/metadata.yaml b/airbyte-integrations/connectors/source-linkedin-ads/metadata.yaml index 8954fcbe8dfca..eef04a5cd95b4 100644 --- a/airbyte-integrations/connectors/source-linkedin-ads/metadata.yaml +++ b/airbyte-integrations/connectors/source-linkedin-ads/metadata.yaml @@ -11,7 +11,7 @@ data: connectorSubtype: api connectorType: source definitionId: 137ece28-5434-455c-8f34-69dc3782f451 - dockerImageTag: 0.6.6 + dockerImageTag: 0.6.7 dockerRepository: airbyte/source-linkedin-ads documentationUrl: https://docs.airbyte.com/integrations/sources/linkedin-ads githubIssueLabel: source-linkedin-ads diff --git a/airbyte-integrations/connectors/source-linkedin-ads/setup.py b/airbyte-integrations/connectors/source-linkedin-ads/setup.py index 2021f6cd6fcd8..ceff2ed3bf729 100644 --- a/airbyte-integrations/connectors/source-linkedin-ads/setup.py +++ b/airbyte-integrations/connectors/source-linkedin-ads/setup.py @@ -14,6 +14,11 @@ ] setup( + entry_points={ + "console_scripts": [ + "source-linkedin-ads=source_linkedin_ads.run:run", + ], + }, name="source_linkedin_ads", description="Source implementation for Linkedin Ads.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/run.py b/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/run.py new file mode 100644 index 0000000000000..e37dbe66f17f6 --- /dev/null +++ b/airbyte-integrations/connectors/source-linkedin-ads/source_linkedin_ads/run.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_linkedin_ads import SourceLinkedinAds + + +def run(): + source = SourceLinkedinAds() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/linkedin-ads.md b/docs/integrations/sources/linkedin-ads.md index d82b35738668f..7bb11b6c51009 100644 --- a/docs/integrations/sources/linkedin-ads.md +++ b/docs/integrations/sources/linkedin-ads.md @@ -171,6 +171,7 @@ After 5 unsuccessful attempts - the connector will stop the sync operation. In s | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------| +| 0.6.7 | 2024-01-11 | [34152](https://github.com/airbytehq/airbyte/pull/34152) | prepare for airbyte-lib | | 0.6.6 | 2024-01-15 | [34222](https://github.com/airbytehq/airbyte/pull/34222) | Use stream slices for Analytics streams | | 0.6.5 | 2023-12-15 | [33530](https://github.com/airbytehq/airbyte/pull/33530) | Fix typo in `Pivot Category` list | | 0.6.4 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | From 9078ec6d8e9ca7bcdf181539ca503c4b0022ce3b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jan 2024 12:35:54 +0100 Subject: [PATCH 109/574] source-zendesk-talk: Convert to airbyte-lib (#34204) Co-authored-by: Serhii Lazebnyi <53845333+lazebnyi@users.noreply.github.com> --- .../connectors/source-zendesk-talk/.coveragerc | 3 +++ .../connectors/source-zendesk-talk/main.py | 9 ++------- .../connectors/source-zendesk-talk/metadata.yaml | 2 +- .../connectors/source-zendesk-talk/setup.py | 5 +++++ .../source-zendesk-talk/source_zendesk_talk/run.py | 14 ++++++++++++++ docs/integrations/sources/zendesk-talk.md | 1 + 6 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 airbyte-integrations/connectors/source-zendesk-talk/.coveragerc create mode 100644 airbyte-integrations/connectors/source-zendesk-talk/source_zendesk_talk/run.py diff --git a/airbyte-integrations/connectors/source-zendesk-talk/.coveragerc b/airbyte-integrations/connectors/source-zendesk-talk/.coveragerc new file mode 100644 index 0000000000000..753140399d72b --- /dev/null +++ b/airbyte-integrations/connectors/source-zendesk-talk/.coveragerc @@ -0,0 +1,3 @@ +[run] +omit = + source_zendesk_talk/run.py \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-zendesk-talk/main.py b/airbyte-integrations/connectors/source-zendesk-talk/main.py index 88d4616c2155d..679ec2c79a789 100644 --- a/airbyte-integrations/connectors/source-zendesk-talk/main.py +++ b/airbyte-integrations/connectors/source-zendesk-talk/main.py @@ -2,12 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_zendesk_talk import SourceZendeskTalk +from source_zendesk_talk.run import run if __name__ == "__main__": - source = SourceZendeskTalk() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-zendesk-talk/metadata.yaml b/airbyte-integrations/connectors/source-zendesk-talk/metadata.yaml index 451f6e4dcbc9a..8fb1cde85c387 100644 --- a/airbyte-integrations/connectors/source-zendesk-talk/metadata.yaml +++ b/airbyte-integrations/connectors/source-zendesk-talk/metadata.yaml @@ -11,7 +11,7 @@ data: connectorSubtype: api connectorType: source definitionId: c8630570-086d-4a40-99ae-ea5b18673071 - dockerImageTag: 0.1.10 + dockerImageTag: 0.1.11 dockerRepository: airbyte/source-zendesk-talk documentationUrl: https://docs.airbyte.com/integrations/sources/zendesk-talk githubIssueLabel: source-zendesk-talk diff --git a/airbyte-integrations/connectors/source-zendesk-talk/setup.py b/airbyte-integrations/connectors/source-zendesk-talk/setup.py index e0e910f6461b6..204a1c5cded52 100644 --- a/airbyte-integrations/connectors/source-zendesk-talk/setup.py +++ b/airbyte-integrations/connectors/source-zendesk-talk/setup.py @@ -15,6 +15,11 @@ ] setup( + entry_points={ + "console_scripts": [ + "source-zendesk-talk=source_zendesk_talk.run:run", + ], + }, name="source_zendesk_talk", description="Source implementation for Zendesk Talk.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-zendesk-talk/source_zendesk_talk/run.py b/airbyte-integrations/connectors/source-zendesk-talk/source_zendesk_talk/run.py new file mode 100644 index 0000000000000..154690ce67d1b --- /dev/null +++ b/airbyte-integrations/connectors/source-zendesk-talk/source_zendesk_talk/run.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_zendesk_talk import SourceZendeskTalk + + +def run(): + source = SourceZendeskTalk() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/zendesk-talk.md b/docs/integrations/sources/zendesk-talk.md index 547c00b0c32cb..e4208bd805768 100644 --- a/docs/integrations/sources/zendesk-talk.md +++ b/docs/integrations/sources/zendesk-talk.md @@ -74,6 +74,7 @@ The Zendesk connector should not run into Zendesk API limitations under normal u | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:----------------------------------------------------------------------------| +| 0.1.11 | 2024-01-12 | [34204](https://github.com/airbytehq/airbyte/pull/34204) | prepare for airbyte-lib | | 0.1.10 | 2023-12-04 | [33030](https://github.com/airbytehq/airbyte/pull/33030) | Base image migration: remove Dockerfile and use python-connector-base image | | 0.1.9 | 2023-08-03 | [29031](https://github.com/airbytehq/airbyte/pull/29031) | Reverted `advancedAuth` spec changes | | 0.1.8 | 2023-08-01 | [28910](https://github.com/airbytehq/airbyte/pull/28910) | Updated `advancedAuth` broken references | From 53064662e34e8b918de683fa35cc8fcd65882d13 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jan 2024 12:36:04 +0100 Subject: [PATCH 110/574] Source Mixpanel: Convert to airbyte-lib (#34147) Co-authored-by: Serhii Lazebnyi <53845333+lazebnyi@users.noreply.github.com> --- .../connectors/source-mixpanel/main.py | 11 ++--------- .../connectors/source-mixpanel/metadata.yaml | 2 +- .../connectors/source-mixpanel/setup.py | 5 +++++ .../source-mixpanel/source_mixpanel/run.py | 16 ++++++++++++++++ docs/integrations/sources/mixpanel.md | 1 + 5 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 airbyte-integrations/connectors/source-mixpanel/source_mixpanel/run.py diff --git a/airbyte-integrations/connectors/source-mixpanel/main.py b/airbyte-integrations/connectors/source-mixpanel/main.py index 5c1449d8dcf42..df8cb33fc826e 100644 --- a/airbyte-integrations/connectors/source-mixpanel/main.py +++ b/airbyte-integrations/connectors/source-mixpanel/main.py @@ -2,14 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_mixpanel import SourceMixpanel -from source_mixpanel.config_migrations import MigrateProjectId +from source_mixpanel.run import run if __name__ == "__main__": - source = SourceMixpanel() - MigrateProjectId.migrate(sys.argv[1:], source) - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-mixpanel/metadata.yaml b/airbyte-integrations/connectors/source-mixpanel/metadata.yaml index bceeb8d9eb005..e40a80cfc9053 100644 --- a/airbyte-integrations/connectors/source-mixpanel/metadata.yaml +++ b/airbyte-integrations/connectors/source-mixpanel/metadata.yaml @@ -11,7 +11,7 @@ data: connectorSubtype: api connectorType: source definitionId: 12928b32-bf0a-4f1e-964f-07e12e37153a - dockerImageTag: 2.0.0 + dockerImageTag: 2.0.1 dockerRepository: airbyte/source-mixpanel documentationUrl: https://docs.airbyte.com/integrations/sources/mixpanel githubIssueLabel: source-mixpanel diff --git a/airbyte-integrations/connectors/source-mixpanel/setup.py b/airbyte-integrations/connectors/source-mixpanel/setup.py index 4a9918b16c913..b89f8d01fbd7a 100644 --- a/airbyte-integrations/connectors/source-mixpanel/setup.py +++ b/airbyte-integrations/connectors/source-mixpanel/setup.py @@ -12,6 +12,11 @@ TEST_REQUIREMENTS = ["requests-mock~=1.9.3", "pytest~=6.1", "pytest-mock~=3.6", "requests_mock~=1.8"] setup( + entry_points={ + "console_scripts": [ + "source-mixpanel=source_mixpanel.run:run", + ], + }, name="source_mixpanel", description="Source implementation for Mixpanel.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-mixpanel/source_mixpanel/run.py b/airbyte-integrations/connectors/source-mixpanel/source_mixpanel/run.py new file mode 100644 index 0000000000000..1d512c472c849 --- /dev/null +++ b/airbyte-integrations/connectors/source-mixpanel/source_mixpanel/run.py @@ -0,0 +1,16 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_mixpanel import SourceMixpanel +from source_mixpanel.config_migrations import MigrateProjectId + + +def run(): + source = SourceMixpanel() + MigrateProjectId.migrate(sys.argv[1:], source) + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/mixpanel.md b/docs/integrations/sources/mixpanel.md index 5593a107647bb..b4cc8d6cfaae0 100644 --- a/docs/integrations/sources/mixpanel.md +++ b/docs/integrations/sources/mixpanel.md @@ -55,6 +55,7 @@ Syncing huge date windows may take longer due to Mixpanel's low API rate-limits | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:------------------------------------------------------------------------------------------------------------| +| 2.0.1 | 2024-01-11 | [34147](https://github.com/airbytehq/airbyte/pull/34147) | prepare for airbyte-lib | | 2.0.0 | 2023-10-30 | [31955](https://github.com/airbytehq/airbyte/pull/31955) | Delete the default primary key for the Export stream | | 1.0.1 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | | 1.0.0 | 2023-09-27 | [30025](https://github.com/airbytehq/airbyte/pull/30025) | Fix type of datetime field in engage stream; fix primary key for export stream. | From dad53b72a5ef3f8126b181c9f08a8e7226d7a98d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jan 2024 12:36:13 +0100 Subject: [PATCH 111/574] Source Google Ads: Convert to airbyte-lib (#34007) --- .../connectors/source-google-ads/main.py | 11 ++--------- .../connectors/source-google-ads/metadata.yaml | 2 +- .../connectors/source-google-ads/setup.py | 5 +++++ .../source-google-ads/source_google_ads/run.py | 16 ++++++++++++++++ docs/integrations/sources/google-ads.md | 1 + 5 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 airbyte-integrations/connectors/source-google-ads/source_google_ads/run.py diff --git a/airbyte-integrations/connectors/source-google-ads/main.py b/airbyte-integrations/connectors/source-google-ads/main.py index d18603af20f37..2824c49559439 100644 --- a/airbyte-integrations/connectors/source-google-ads/main.py +++ b/airbyte-integrations/connectors/source-google-ads/main.py @@ -2,14 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_google_ads import SourceGoogleAds -from source_google_ads.config_migrations import MigrateCustomQuery +from source_google_ads.run import run if __name__ == "__main__": - source = SourceGoogleAds() - MigrateCustomQuery.migrate(sys.argv[1:], source) - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-google-ads/metadata.yaml b/airbyte-integrations/connectors/source-google-ads/metadata.yaml index 82d21ed64f367..4c09baf87a5c6 100644 --- a/airbyte-integrations/connectors/source-google-ads/metadata.yaml +++ b/airbyte-integrations/connectors/source-google-ads/metadata.yaml @@ -11,7 +11,7 @@ data: connectorSubtype: api connectorType: source definitionId: 253487c0-2246-43ba-a21f-5116b20a2c50 - dockerImageTag: 3.3.0 + dockerImageTag: 3.3.1 dockerRepository: airbyte/source-google-ads documentationUrl: https://docs.airbyte.com/integrations/sources/google-ads githubIssueLabel: source-google-ads diff --git a/airbyte-integrations/connectors/source-google-ads/setup.py b/airbyte-integrations/connectors/source-google-ads/setup.py index d0694f67fa7ba..7211a092ff545 100644 --- a/airbyte-integrations/connectors/source-google-ads/setup.py +++ b/airbyte-integrations/connectors/source-google-ads/setup.py @@ -14,6 +14,11 @@ TEST_REQUIREMENTS = ["pytest~=6.1", "pytest-mock", "freezegun", "requests-mock"] setup( + entry_points={ + "console_scripts": [ + "source-google-ads=source_google_ads.run:run", + ], + }, name="source_google_ads", description="Source implementation for Google Ads.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-google-ads/source_google_ads/run.py b/airbyte-integrations/connectors/source-google-ads/source_google_ads/run.py new file mode 100644 index 0000000000000..dd759a035015c --- /dev/null +++ b/airbyte-integrations/connectors/source-google-ads/source_google_ads/run.py @@ -0,0 +1,16 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_google_ads import SourceGoogleAds +from source_google_ads.config_migrations import MigrateCustomQuery + + +def run(): + source = SourceGoogleAds() + MigrateCustomQuery.migrate(sys.argv[1:], source) + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/google-ads.md b/docs/integrations/sources/google-ads.md index c509c5dd54dcc..da21953520739 100644 --- a/docs/integrations/sources/google-ads.md +++ b/docs/integrations/sources/google-ads.md @@ -280,6 +280,7 @@ Due to a limitation in the Google Ads API which does not allow getting performan | Version | Date | Pull Request | Subject | |:---------|:-----------|:---------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------| +| `3.3.1` | 2024-01-16 | [34007](https://github.com/airbytehq/airbyte/pull/34007) | prepare for airbyte-lib | | `3.3.0` | 2024-01-12 | [34212](https://github.com/airbytehq/airbyte/pull/34212) | Remove metric from query in Ad Group stream for non-manager account | | `3.2.1` | 2024-01-12 | [34200](https://github.com/airbytehq/airbyte/pull/34200) | Disable raising error for not enabled accounts | | `3.2.0` | 2024-01-09 | [33707](https://github.com/airbytehq/airbyte/pull/33707) | Add possibility to sync all connected accounts | From dc8df8f924775dbfc853d5414fdb4a222293611b Mon Sep 17 00:00:00 2001 From: Marcos Marx Date: Tue, 16 Jan 2024 10:45:23 -0300 Subject: [PATCH 112/574] =?UTF-8?q?=F0=9F=8C=9F=20Source=20Freshservice:?= =?UTF-8?q?=20add=20`requested=5Fitems`=20stream=20(#34272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../connectors/source-freshservice/Dockerfile | 2 +- .../integration_tests/configured_catalog.json | 21 ++++++--- .../source-freshservice/metadata.yaml | 6 +-- .../connectors/source-freshservice/setup.py | 8 +--- .../source_freshservice/manifest.yaml | 28 +++++++++-- .../source_freshservice/schemas/agents.json | 6 +++ .../schemas/requested_items.json | 47 +++++++++++++++++++ docs/integrations/sources/freshservice.md | 1 + 8 files changed, 99 insertions(+), 20 deletions(-) create mode 100644 airbyte-integrations/connectors/source-freshservice/source_freshservice/schemas/requested_items.json diff --git a/airbyte-integrations/connectors/source-freshservice/Dockerfile b/airbyte-integrations/connectors/source-freshservice/Dockerfile index 7732b3d3d243e..b8b34c49cf440 100644 --- a/airbyte-integrations/connectors/source-freshservice/Dockerfile +++ b/airbyte-integrations/connectors/source-freshservice/Dockerfile @@ -34,5 +34,5 @@ COPY source_freshservice ./source_freshservice ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] -LABEL io.airbyte.version=1.2.0 +LABEL io.airbyte.version=1.3.0 LABEL io.airbyte.name=airbyte/source-freshservice diff --git a/airbyte-integrations/connectors/source-freshservice/integration_tests/configured_catalog.json b/airbyte-integrations/connectors/source-freshservice/integration_tests/configured_catalog.json index 88cbb244c9f1f..14bfe7e98d95c 100644 --- a/airbyte-integrations/connectors/source-freshservice/integration_tests/configured_catalog.json +++ b/airbyte-integrations/connectors/source-freshservice/integration_tests/configured_catalog.json @@ -12,28 +12,37 @@ "sync_mode": "incremental", "destination_sync_mode": "overwrite" }, + { + "stream": { + "name": "requested_items", + "json_schema": {}, + "supported_sync_modes": ["full_refresh"], + "source_defined_cursor": true, + "source_defined_primary_key": [["id"]] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "overwrite" + }, { "stream": { "name": "problems", "json_schema": {}, - "supported_sync_modes": ["full_refresh", "incremental"], + "supported_sync_modes": ["full_refresh"], "source_defined_cursor": true, - "default_cursor_field": ["updated_at"], "source_defined_primary_key": [["id"]] }, - "sync_mode": "incremental", + "sync_mode": "full_refresh", "destination_sync_mode": "overwrite" }, { "stream": { "name": "changes", "json_schema": {}, - "supported_sync_modes": ["full_refresh", "incremental"], + "supported_sync_modes": ["full_refresh"], "source_defined_cursor": true, - "default_cursor_field": ["updated_at"], "source_defined_primary_key": [["id"]] }, - "sync_mode": "incremental", + "sync_mode": "full_refresh", "destination_sync_mode": "overwrite" }, { diff --git a/airbyte-integrations/connectors/source-freshservice/metadata.yaml b/airbyte-integrations/connectors/source-freshservice/metadata.yaml index 61c73b4c6b105..672168762c1aa 100644 --- a/airbyte-integrations/connectors/source-freshservice/metadata.yaml +++ b/airbyte-integrations/connectors/source-freshservice/metadata.yaml @@ -1,16 +1,16 @@ data: allowedHosts: hosts: - - TODO # Please change to the hostname of the source. + - ${domain_name}/api/v2 registries: oss: - enabled: false + enabled: true cloud: enabled: false connectorSubtype: api connectorType: source definitionId: 9bb85338-ea95-4c93-b267-6be89125b267 - dockerImageTag: 1.2.0 + dockerImageTag: 1.3.0 dockerRepository: airbyte/source-freshservice githubIssueLabel: source-freshservice icon: freshservice.svg diff --git a/airbyte-integrations/connectors/source-freshservice/setup.py b/airbyte-integrations/connectors/source-freshservice/setup.py index 8ccedf94055a8..422531f33640d 100644 --- a/airbyte-integrations/connectors/source-freshservice/setup.py +++ b/airbyte-integrations/connectors/source-freshservice/setup.py @@ -6,14 +6,10 @@ from setuptools import find_packages, setup MAIN_REQUIREMENTS = [ - "airbyte-cdk~=0.1", + "airbyte-cdk~=0.55.2", ] -TEST_REQUIREMENTS = [ - "pytest~=6.2", - "pytest-mock~=3.6.1", - "connector-acceptance-test", -] +TEST_REQUIREMENTS = ["pytest~=6.2", "pytest-mock~=3.6.1"] setup( name="source_freshservice", diff --git a/airbyte-integrations/connectors/source-freshservice/source_freshservice/manifest.yaml b/airbyte-integrations/connectors/source-freshservice/source_freshservice/manifest.yaml index fb2cbfe386c31..7d8414022f55a 100644 --- a/airbyte-integrations/connectors/source-freshservice/source_freshservice/manifest.yaml +++ b/airbyte-integrations/connectors/source-freshservice/source_freshservice/manifest.yaml @@ -78,12 +78,33 @@ definitions: parent_key: "id" partition_field: "parent_id" + requested_items_stream: + name: "requested_items" + primary_key: "id" + $parameters: + path_extractor: "requested_items" + retriever: + $ref: "#/definitions/retriever" + requester: + $ref: "#/definitions/requester" + path: "tickets/{{ stream_slice.parent_id }}/requested_items" + error_handler: + type: DefaultErrorHandler + response_filters: + - http_codes: [404] + action: IGNORE + error_message: No data collected + partition_router: + type: SubstreamPartitionRouter + parent_stream_configs: + - stream: "#/definitions/tickets_stream" + parent_key: "id" + partition_field: "parent_id" + problems_stream: $ref: "#/definitions/base_stream" name: "problems" primary_key: "id" - incremental_sync: - $ref: "#/definitions/incremental_base" $parameters: path_extractor: "problems" path: "/problems" @@ -92,8 +113,6 @@ definitions: $ref: "#/definitions/base_stream" name: "changes" primary_key: "id" - incremental_sync: - $ref: "#/definitions/incremental_base" $parameters: path_extractor: "changes" path: "/changes" @@ -186,6 +205,7 @@ streams: - "#/definitions/assets_stream" - "#/definitions/purchase_orders_stream" - "#/definitions/software_stream" + - "#/definitions/requested_items_stream" check: type: CheckStream diff --git a/airbyte-integrations/connectors/source-freshservice/source_freshservice/schemas/agents.json b/airbyte-integrations/connectors/source-freshservice/source_freshservice/schemas/agents.json index 005af789f2864..1b2d07f0fe09c 100644 --- a/airbyte-integrations/connectors/source-freshservice/source_freshservice/schemas/agents.json +++ b/airbyte-integrations/connectors/source-freshservice/source_freshservice/schemas/agents.json @@ -27,6 +27,12 @@ "mobile_phone_number": { "type": ["null", "string"] }, + "member_of_pending_approval": { + "type": ["null", "array"] + }, + "observer_of_pending_approval": { + "type": ["null", "array"] + }, "department_ids": { "type": ["null", "array"] }, diff --git a/airbyte-integrations/connectors/source-freshservice/source_freshservice/schemas/requested_items.json b/airbyte-integrations/connectors/source-freshservice/source_freshservice/schemas/requested_items.json new file mode 100644 index 0000000000000..909a9ed2621cd --- /dev/null +++ b/airbyte-integrations/connectors/source-freshservice/source_freshservice/schemas/requested_items.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": true, + "properties": { + "id": { + "type": ["null", "integer"] + }, + "created_at": { + "type": ["null", "string"] + }, + "updated_at": { + "type": ["null", "string"] + }, + "quantity": { + "type": ["null", "integer"] + }, + "stage": { + "type": ["null", "integer"] + }, + "loaned": { + "type": ["null", "boolean"] + }, + "cost_per_request": { + "type": ["null", "number"] + }, + "remarks": { + "type": ["null", "string"] + }, + "delivery_time": { + "type": ["null", "number"] + }, + "is_parent": { + "type": ["null", "boolean"] + }, + "service_item_id": { + "type": ["null", "integer"] + }, + "service_item_name": { + "type": ["null", "string"] + }, + "custom_fields": { + "type": ["null", "object"], + "additionalProperties": true + } + } +} diff --git a/docs/integrations/sources/freshservice.md b/docs/integrations/sources/freshservice.md index 6481eb888c3fc..1a9a30aebdf08 100644 --- a/docs/integrations/sources/freshservice.md +++ b/docs/integrations/sources/freshservice.md @@ -54,6 +54,7 @@ Please read [How to find your API key](https://api.freshservice.com/#authenticat | Version | Date | Pull Request | Subject | | :--- | :--- | :--- | :--- | +| 1.3.0 | 2024-01-15 | [29126](https://github.com/airbytehq/airbyte/pull/29126) | Add `Requested Items` stream | | 1.2.0 | 2023-08-06 | [29126](https://github.com/airbytehq/airbyte/pull/29126) | Migrated to Low-Code CDK | | 1.1.0 | 2023-05-09 | [25929](https://github.com/airbytehq/airbyte/pull/25929) | Add stream for customer satisfaction survey responses endpoint | | 1.0.0 | 2023-05-02 | [25743](https://github.com/airbytehq/airbyte/pull/25743) | Correct data types in tickets, agents and requesters schemas to match Freshservice API | From 31db9f8df724ba4da384a218643148505d66811f Mon Sep 17 00:00:00 2001 From: Marcos Marx Date: Tue, 16 Jan 2024 10:45:44 -0300 Subject: [PATCH 113/574] Source Close.com: add custom fields (#34286) Co-authored-by: James Truty --- .../connectors/source-close-com/Dockerfile | 2 +- .../connectors/source-close-com/metadata.yaml | 2 +- .../source_close_com/__init__.py | 2 +- .../source_close_com/source.py | 29 +++++++++++++++---- docs/integrations/sources/close-com.md | 5 ++-- 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/airbyte-integrations/connectors/source-close-com/Dockerfile b/airbyte-integrations/connectors/source-close-com/Dockerfile index e77535415cb07..44603bb80be52 100644 --- a/airbyte-integrations/connectors/source-close-com/Dockerfile +++ b/airbyte-integrations/connectors/source-close-com/Dockerfile @@ -12,5 +12,5 @@ RUN pip install . ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] -LABEL io.airbyte.version=0.4.3 +LABEL io.airbyte.version=0.5.0 LABEL io.airbyte.name=airbyte/source-close-com diff --git a/airbyte-integrations/connectors/source-close-com/metadata.yaml b/airbyte-integrations/connectors/source-close-com/metadata.yaml index 97847eafefacf..ccb09a7823a91 100644 --- a/airbyte-integrations/connectors/source-close-com/metadata.yaml +++ b/airbyte-integrations/connectors/source-close-com/metadata.yaml @@ -8,7 +8,7 @@ data: connectorSubtype: api connectorType: source definitionId: dfffecb7-9a13-43e9-acdc-b92af7997ca9 - dockerImageTag: 0.4.3 + dockerImageTag: 0.5.0 dockerRepository: airbyte/source-close-com documentationUrl: https://docs.airbyte.com/integrations/sources/close-com githubIssueLabel: source-close-com diff --git a/airbyte-integrations/connectors/source-close-com/source_close_com/__init__.py b/airbyte-integrations/connectors/source-close-com/source_close_com/__init__.py index 290f7d5f74e05..26f244576b385 100644 --- a/airbyte-integrations/connectors/source-close-com/source_close_com/__init__.py +++ b/airbyte-integrations/connectors/source-close-com/source_close_com/__init__.py @@ -22,6 +22,6 @@ from .datetime_incremental_sync import CustomDatetimeIncrementalSync -from .source_lc import SourceCloseCom +from .source import SourceCloseCom __all__ = ["SourceCloseCom", "CustomDatetimeIncrementalSync"] diff --git a/airbyte-integrations/connectors/source-close-com/source_close_com/source.py b/airbyte-integrations/connectors/source-close-com/source_close_com/source.py index 2fe9c2f85ab8f..9754c114bbd8c 100644 --- a/airbyte-integrations/connectors/source-close-com/source_close_com/source.py +++ b/airbyte-integrations/connectors/source-close-com/source_close_com/source.py @@ -58,7 +58,6 @@ def request_params( stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None, ) -> MutableMapping[str, Any]: - params = {} if self.number_of_items_per_page: params.update({"_limit": self.number_of_items_per_page}) @@ -87,8 +86,24 @@ def backoff_time(self, response: requests.Response) -> Optional[float]: return backoff_time -class IncrementalCloseComStream(CloseComStream): +class CloseComStreamCustomFields(CloseComStream): + """Class to get custom fields for close objects that support them.""" + + def get_custom_field_schema(self) -> Mapping[str, Any]: + """Get custom field schema if it exists.""" + resp = requests.request("GET", url=f"{self.url_base}/custom_field/{self.path()}/", headers=self.authenticator.get_auth_header()) + resp.raise_for_status() + resp_json: Mapping[str, Any] = resp.json()["data"] + return {f"custom.{data['id']}": {"type": ["null", "string", "number", "boolean"]} for data in resp_json} + + def get_json_schema(self): + """Override default get_json_schema method to add custom fields to schema.""" + schema = super().get_json_schema() + schema["properties"].update(self.get_custom_field_schema()) + return schema + +class IncrementalCloseComStream(CloseComStream): cursor_field = "date_updated" def get_updated_state( @@ -105,6 +120,10 @@ def get_updated_state( return {self.cursor_field: max(latest_record.get(self.cursor_field, ""), current_stream_state.get(self.cursor_field, ""))} +class IncrementalCloseComStreamCustomFields(CloseComStreamCustomFields, IncrementalCloseComStream): + """Class to get custom fields for close objects using incremental stream.""" + + class CloseComActivitiesStream(IncrementalCloseComStream): """ General class for activities. Define request params based on cursor_field value. @@ -233,7 +252,7 @@ def request_params(self, stream_state=None, **kwargs): return params -class Leads(IncrementalCloseComStream): +class Leads(IncrementalCloseComStreamCustomFields): """ Get leads on a specific date API Docs: https://developer.close.com/#leads @@ -404,7 +423,7 @@ def path(self, **kwargs) -> str: return "user" -class Contacts(CloseComStream): +class Contacts(CloseComStreamCustomFields): """ Get contacts for Close.com account organization API Docs: https://developer.close.com/#contacts @@ -416,7 +435,7 @@ def path(self, **kwargs) -> str: return "contact" -class Opportunities(IncrementalCloseComStream): +class Opportunities(IncrementalCloseComStreamCustomFields): """ Get opportunities on a specific date API Docs: https://developer.close.com/#opportunities diff --git a/docs/integrations/sources/close-com.md b/docs/integrations/sources/close-com.md index bedfdc1169496..d152f845fe9ce 100644 --- a/docs/integrations/sources/close-com.md +++ b/docs/integrations/sources/close-com.md @@ -105,8 +105,9 @@ The Close.com connector is subject to rate limits. For more information on this | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:-------------------------------------------------------------------------------------------------------| -| 0.4.3 | 2023-10-28 | [31534](https://github.com/airbytehq/airbyte/pull/31534) | Fixed Email Activities Stream Pagination | -| 0.4.2 | 2023-08-08 | [29206](https://github.com/airbytehq/airbyte/pull/29206) | Fixed the issue with `DatePicker` format for `start date` | +| 0.5.0 | 2023-11-30 | [32984](https://github.com/airbytehq/airbyte/pull/32984) | Add support for custom fields | +| 0.4.3 | 2023-10-28 | [31534](https://github.com/airbytehq/airbyte/pull/31534) | Fixed Email Activities Stream Pagination | +| 0.4.2 | 2023-08-08 | [29206](https://github.com/airbytehq/airbyte/pull/29206) | Fixed the issue with `DatePicker` format for `start date` | | 0.4.1 | 2023-07-04 | [27950](https://github.com/airbytehq/airbyte/pull/27950) | Add human readable titles to API Key and Start Date fields | | 0.4.0 | 2023-06-27 | [27776](https://github.com/airbytehq/airbyte/pull/27776) | Update the `Email Followup Tasks` stream schema | | 0.3.0 | 2023-05-12 | [26024](https://github.com/airbytehq/airbyte/pull/26024) | Update the `Email sequences` stream schema | From 670ffdd32c5869fed3fd8c6d6d9787de84104965 Mon Sep 17 00:00:00 2001 From: Edward Gao Date: Tue, 16 Jan 2024 06:04:29 -0800 Subject: [PATCH 114/574] fix java cdk utility tasks (#34174) these tasks were only checking within the cdk directory, so they didn't actually accomplish anything. Fix them to walk the entire tree. ... afaict we don't actually use these tasks for anything, but might as well make them work? --- buildSrc/src/main/groovy/airbyte-java-cdk.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/groovy/airbyte-java-cdk.gradle b/buildSrc/src/main/groovy/airbyte-java-cdk.gradle index c97594543b0b8..68f1c95fdeb65 100644 --- a/buildSrc/src/main/groovy/airbyte-java-cdk.gradle +++ b/buildSrc/src/main/groovy/airbyte-java-cdk.gradle @@ -32,7 +32,7 @@ class AirbyteJavaCdkPlugin implements Plugin { @TaskAction public void disableLocalCdkRefs() { // Step through the project tree and set useLocalCdk to false on all connectors - getProject().fileTree(dir: '.', include: '**/build.gradle').forEach(file -> { + getProject().rootProject.fileTree(dir: '.', include: '**/build.gradle').forEach(file -> { String content = file.getText() if (content.contains("useLocalCdk = true")) { content = content.replace("useLocalCdk = true", "useLocalCdk = false") @@ -48,7 +48,7 @@ class AirbyteJavaCdkPlugin implements Plugin { public void assertNotUsingLocalCdk() { List foundPaths = new ArrayList<>() - for (File file : getProject().fileTree(dir: '.', include: '**/build.gradle')) { + for (File file : getProject().rootProject.fileTree(dir: '.', include: '**/build.gradle')) { String content = file.getText() if (content.contains("useLocalCdk = true")) { System.err.println("Found usage of 'useLocalCdk = true' in " + file.getPath()) From 9ddb4325e9174d8c9f68c04a845a58efd8304eec Mon Sep 17 00:00:00 2001 From: Augustin Date: Tue, 16 Jan 2024 15:23:01 +0100 Subject: [PATCH 115/574] airbyte-ci: compute GHA `runs-on` from `--ci-requirements` (#34220) --- .../airbyte-ci-requirements/action.yml | 104 ++++++++++++++++++ .github/workflows/airbyte-ci-tests.yml | 35 ++++-- .github/workflows/cat-tests.yml | 24 +++- .../workflows/connectors_nightly_build.yml | 31 ++++-- .github/workflows/connectors_tests.yml | 32 ++++-- .github/workflows/connectors_weekly_build.yml | 32 ++++-- .github/workflows/format_check.yml | 40 ++++--- .github/workflows/format_fix.yml | 24 +++- ...ata_service_deploy_orchestrator_dagger.yml | 24 +++- .github/workflows/publish_connectors.yml | 33 ++++-- 10 files changed, 315 insertions(+), 64 deletions(-) create mode 100644 .github/actions/airbyte-ci-requirements/action.yml diff --git a/.github/actions/airbyte-ci-requirements/action.yml b/.github/actions/airbyte-ci-requirements/action.yml new file mode 100644 index 0000000000000..d12a7c1b6b10a --- /dev/null +++ b/.github/actions/airbyte-ci-requirements/action.yml @@ -0,0 +1,104 @@ +name: "Get airbyte-ci runner name" +description: "Runs a given airbyte-ci command with the --ci-requirements flag to get the CI requirements for a given command" +inputs: + runner_type: + description: "Type of runner to get requirements for. One of: format, test, nightly, publish" + required: true + runner_size: + description: "One of: format, test, nightly, publish" + required: true + airbyte_ci_command: + description: "airbyte-ci command to get CI requirements for." + required: true + runner_name_prefix: + description: "Prefix of runner name" + required: false + default: ci-runner-connector + github_token: + description: "GitHub token" + required: true + sentry_dsn: + description: "Sentry DSN" + required: false + airbyte_ci_binary_url: + description: "URL to airbyte-ci binary" + required: false + default: https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci + +runs: + using: "composite" + steps: + - name: Check if PR is from a fork + if: github.event_name == 'pull_request' + shell: bash + run: | + if [ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]; then + echo "PR is from a fork. Exiting workflow..." + exit 78 + fi + + - name: Get changed files + uses: tj-actions/changed-files@v39 + id: changes + with: + files_yaml: | + pipelines: + - 'airbyte-ci/connectors/pipelines/**' + + - name: Determine how Airbyte CI should be installed + shell: bash + id: determine-install-mode + run: | + if [[ "${{ github.ref }}" != "refs/heads/master" ]] && [[ "${{ steps.changes.outputs.pipelines_any_changed }}" == "true" ]]; then + echo "Making changes to Airbyte CI on a non-master branch. Airbyte-CI will be installed from source." + echo "install-mode=dev" >> $GITHUB_OUTPUT + else + echo "install-mode=production" >> $GITHUB_OUTPUT + fi + + - name: Install airbyte-ci binary + id: install-airbyte-ci + if: steps.determine-install-mode.outputs.install-mode == 'production' + shell: bash + run: | + curl -sSL ${{ inputs.airbyte_ci_binary_url }} --output airbyte-ci-bin + sudo mv airbyte-ci-bin /usr/local/bin/airbyte-ci + sudo chmod +x /usr/local/bin/airbyte-ci + + - name: Install Python 3.10 + uses: actions/setup-python@v4 + if: steps.determine-install-mode.outputs.install-mode == 'dev' + with: + python-version: "3.10" + token: ${{ inputs.github_token }} + + - name: Install ci-connector-ops package + if: steps.determine-install-mode.outputs.install-mode == 'dev' + shell: bash + run: | + pip install pipx + pipx ensurepath + pipx install airbyte-ci/connectors/pipelines/ + + - name: Get dagger version from airbyte-ci + id: get-dagger-version + shell: bash + run: | + dagger_version=$(airbyte-ci ${{ inputs.airbyte_ci_command }} --ci-requirements | tail -n 1 | jq -r '.dagger_version') + echo "dagger_version=${dagger_version}" >> "$GITHUB_OUTPUT" + + - name: Get runner name + id: get-runner-name + shell: bash + run: | + runner_name_prefix=${{ inputs.runner_name_prefix }} + runner_type=${{ inputs.runner_type }} + runner_size=${{ inputs.runner_size }} + dashed_dagger_version=$(echo "${{ steps.get-dagger-version.outputs.dagger_version }}" | tr '.' '-') + runner_name="${runner_name_prefix}-${runner_type}-${runner_size}-dagger-${dashed_dagger_version}" + echo ${runner_name} + echo "runner_name=${runner_name}" >> "$GITHUB_OUTPUT" +outputs: + runner_name: + description: "Name of self hosted CI runner to use" + value: ${{ steps.get-runner-name.outputs.runner_name }} diff --git a/.github/workflows/airbyte-ci-tests.yml b/.github/workflows/airbyte-ci-tests.yml index 1b5f32810b6f3..41789fd873dab 100644 --- a/.github/workflows/airbyte-ci-tests.yml +++ b/.github/workflows/airbyte-ci-tests.yml @@ -6,20 +6,37 @@ concurrency: on: workflow_dispatch: - inputs: - airbyte_ci_binary_url: - description: "URL to airbyte-ci binary" - required: false - default: https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci pull_request: types: - opened - reopened - synchronize jobs: + get_ci_runner: + runs-on: ubuntu-latest + name: Get CI runner + steps: + - name: Checkout Airbyte + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + fetch-depth: 1 + - name: Get CI runner + id: get_ci_runner + uses: ./.github/actions/airbyte-ci-requirements + with: + runner_type: "test" + runner_size: "large" + airbyte_ci_command: "test" + github_token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} + outputs: + runner_name: ${{ steps.get_ci_runner.outputs.runner_name }} run-airbyte-ci-tests: name: Run Airbyte CI tests - runs-on: "ci-runner-connector-test-large-dagger-0-9-5" + needs: get_ci_runner + runs-on: ${{ needs.get_ci_runner.outputs.runner_name }} steps: - name: Checkout Airbyte uses: actions/checkout@v3 @@ -78,7 +95,6 @@ jobs: sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} github_token: ${{ secrets.GH_PAT_MAINTENANCE_OCTAVIA }} subcommand: "test airbyte-ci/connectors/connector_ops --poetry-run-command='pytest tests'" - airbyte_ci_binary_url: ${{ inputs.airbyte_ci_binary_url || 'https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci' }} tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }} - name: Run airbyte-ci/connectors/pipelines tests @@ -94,7 +110,6 @@ jobs: sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} github_token: ${{ secrets.GH_PAT_MAINTENANCE_OCTAVIA }} subcommand: "test airbyte-ci/connectors/pipelines --poetry-run-command='pytest tests' --poetry-run-command='mypy pipelines --disallow-untyped-defs' --poetry-run-command='ruff check pipelines'" - airbyte_ci_binary_url: ${{ inputs.airbyte_ci_binary_url || 'https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci' }} tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }} - name: Run airbyte-ci/connectors/base_images tests @@ -110,7 +125,6 @@ jobs: sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} github_token: ${{ secrets.GH_PAT_MAINTENANCE_OCTAVIA }} subcommand: "test airbyte-ci/connectors/base_images --poetry-run-command='pytest tests'" - airbyte_ci_binary_url: ${{ inputs.airbyte_ci_binary_url || 'https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci' }} tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }} - name: Run test pipeline for the metadata lib @@ -124,7 +138,6 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }} docker_hub_password: ${{ secrets.DOCKER_HUB_PASSWORD }} - airbyte_ci_binary_url: ${{ inputs.airbyte_ci_binary_url || 'https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci' }} tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }} - name: Run test for the metadata orchestrator @@ -138,7 +151,6 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }} docker_hub_password: ${{ secrets.DOCKER_HUB_PASSWORD }} - airbyte_ci_binary_url: ${{ inputs.airbyte_ci_binary_url || 'https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci' }} tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }} - name: Run airbyte-lib tests @@ -153,5 +165,4 @@ jobs: sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} github_token: ${{ secrets.GH_PAT_MAINTENANCE_OCTAVIA }} subcommand: "test airbyte-lib" - airbyte_ci_binary_url: ${{ inputs.airbyte_ci_binary_url || 'https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci' }} tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }} diff --git a/.github/workflows/cat-tests.yml b/.github/workflows/cat-tests.yml index ebc803b212d64..2f8cda400060b 100644 --- a/.github/workflows/cat-tests.yml +++ b/.github/workflows/cat-tests.yml @@ -14,9 +14,31 @@ on: paths: - airbyte-integrations/bases/connector-acceptance-test/** jobs: + get_ci_runner: + runs-on: ubuntu-latest + name: Get CI runner + steps: + - name: Checkout Airbyte + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + fetch-depth: 1 + - name: Get CI runner + id: get_ci_runner + uses: ./.github/actions/airbyte-ci-requirements + with: + runner_type: "test" + runner_size: "large" + airbyte_ci_command: "test" + github_token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} + outputs: + runner_name: ${{ steps.get_ci_runner.outputs.runner_name }} run-cat-unit-tests: name: Run CAT unit tests - runs-on: "ci-runner-connector-test-large-dagger-0-9-5" + needs: get_ci_runner + runs-on: ${{ needs.get_ci_runner.outputs.runner_name }} steps: - name: Checkout Airbyte uses: actions/checkout@v3 diff --git a/.github/workflows/connectors_nightly_build.yml b/.github/workflows/connectors_nightly_build.yml index d89ad8ad4a6f4..a2724c063d6b7 100644 --- a/.github/workflows/connectors_nightly_build.yml +++ b/.github/workflows/connectors_nightly_build.yml @@ -6,21 +6,38 @@ on: - cron: "0 0 * * *" workflow_dispatch: inputs: - runs-on: - type: string - default: ci-runner-connector-nightly-xlarge-dagger-0-9-5 - required: true test-connectors-options: default: --concurrency=5 --support-level=certified required: true -run-name: "Test connectors: ${{ inputs.test-connectors-options || 'nightly build for Certified connectors' }} - on ${{ inputs.runs-on || 'ci-runner-connector-nightly-xlarge-dagger-0-9-5' }}" +run-name: "Test connectors: ${{ inputs.test-connectors-options || 'nightly build for Certified connectors' }}" jobs: + get_ci_runner: + runs-on: ubuntu-latest + name: Get CI runner + steps: + - name: Checkout Airbyte + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + fetch-depth: 1 + - name: Get CI runner + id: get_ci_runner + uses: ./.github/actions/airbyte-ci-requirements + with: + runner_type: "nightly" + runner_size: "xlarge" + airbyte_ci_command: "connectors test" + github_token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} + outputs: + runner_name: ${{ steps.get_ci_runner.outputs.runner_name }} test_connectors: - name: "Test connectors: ${{ inputs.test-connectors-options || 'nightly build for Certified connectors' }} - on ${{ inputs.runs-on || 'ci-runner-connector-nightly-xlarge-dagger-0-9-5' }}" + name: "Test connectors: ${{ inputs.test-connectors-options || 'nightly build for Certified connectors' }}" timeout-minutes: 720 # 12 hours - runs-on: ${{ inputs.runs-on || 'ci-runner-connector-nightly-xlarge-dagger-0-9-5' }} + runs-on: ${{ needs.get_ci_runner.outputs.runner_name }} steps: - name: Checkout Airbyte uses: actions/checkout@v3 diff --git a/.github/workflows/connectors_tests.yml b/.github/workflows/connectors_tests.yml index 190851778ab8b..0b07c3c4bac5b 100644 --- a/.github/workflows/connectors_tests.yml +++ b/.github/workflows/connectors_tests.yml @@ -17,23 +17,38 @@ on: test-connectors-options: description: "Options to pass to the 'airbyte-ci connectors test' command" default: "--modified" - runner: - description: "The runner to use for this job" - default: "ci-runner-connector-test-large-dagger-0-9-5" - airbyte_ci_binary_url: - description: "The URL to download the airbyte-ci binary from" - required: false - default: https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci pull_request: types: - opened - synchronize - ready_for_review jobs: + get_ci_runner: + runs-on: ubuntu-latest + name: Get CI runner + steps: + - name: Checkout Airbyte + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + fetch-depth: 1 + - name: Get CI runner + id: get_ci_runner + uses: ./.github/actions/airbyte-ci-requirements + with: + runner_type: "test" + runner_size: "large" + airbyte_ci_command: "connectors test" + github_token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} + outputs: + runner_name: ${{ steps.get_ci_runner.outputs.runner_name }} connectors_ci: name: Connectors CI + needs: get_ci_runner + runs-on: ${{ needs.get_ci_runner.outputs.runner_name }} timeout-minutes: 1440 # 24 hours - runs-on: ${{ inputs.runner || 'ci-runner-connector-test-large-dagger-0-9-5'}} steps: - name: Checkout Airbyte uses: actions/checkout@v3 @@ -71,7 +86,6 @@ jobs: s3_build_cache_access_key_id: ${{ secrets.SELF_RUNNER_AWS_ACCESS_KEY_ID }} s3_build_cache_secret_key: ${{ secrets.SELF_RUNNER_AWS_SECRET_ACCESS_KEY }} subcommand: "connectors ${{ github.event.inputs.test-connectors-options }} test" - airbyte_ci_binary_url: ${{ github.event.inputs.airbyte_ci_binary_url }} tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }} - name: Test connectors [PULL REQUESTS] if: github.event_name == 'pull_request' diff --git a/.github/workflows/connectors_weekly_build.yml b/.github/workflows/connectors_weekly_build.yml index 55ecd461bdd22..8ec55657715b7 100644 --- a/.github/workflows/connectors_weekly_build.yml +++ b/.github/workflows/connectors_weekly_build.yml @@ -6,21 +6,39 @@ on: - cron: "0 12 * * 0" workflow_dispatch: inputs: - runs-on: - type: string - default: ci-runner-connector-nightly-xlarge-dagger-0-9-5 - required: true test-connectors-options: default: --concurrency=3 --support-level=community required: true -run-name: "Test connectors: ${{ inputs.test-connectors-options || 'weekly build for Community connectors' }} - on ${{ inputs.runs-on || 'ci-runner-connector-nightly-xlarge-dagger-0-9-5' }}" +run-name: "Test connectors: ${{ inputs.test-connectors-options || 'weekly build for Community connectors' }}" jobs: + get_ci_runner: + runs-on: ubuntu-latest + name: Get CI runner + steps: + - name: Checkout Airbyte + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + fetch-depth: 1 + - name: Get CI runner + id: get_ci_runner + uses: ./.github/actions/airbyte-ci-requirements + with: + runner_type: "test" + runner_size: "large" + airbyte_ci_command: "connectors test" + github_token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} + outputs: + runner_name: ${{ steps.get_ci_runner.outputs.runner_name }} test_connectors: - name: "Test connectors: ${{ inputs.test-connectors-options || 'weekly build for Community connectors' }} - on ${{ inputs.runs-on || 'ci-runner-connector-nightly-xlarge-dagger-0-9-5' }}" + name: "Test connectors: ${{ inputs.test-connectors-options || 'weekly build for Community connectors' }}" timeout-minutes: 8640 # 6 days - runs-on: ${{ inputs.runs-on || 'ci-runner-connector-nightly-xlarge-dagger-0-9-5' }} + needs: get_ci_runner + runs-on: ${{ needs.get_ci_runner.outputs.runner_name }} steps: - name: Checkout Airbyte uses: actions/checkout@v3 diff --git a/.github/workflows/format_check.yml b/.github/workflows/format_check.yml index 44ceac22e0b87..59e38842801b1 100644 --- a/.github/workflows/format_check.yml +++ b/.github/workflows/format_check.yml @@ -2,13 +2,6 @@ name: Check for formatting errors run-name: Check for formatting errors on ${{ github.ref }} on: workflow_dispatch: - inputs: - airbyte-ci-binary-url: - description: "URL to airbyte-ci binary" - required: false - # Pin to a specific version of airbyte-ci to avoid transient failures - # Mentioned in issue https://github.com/airbytehq/airbyte/issues/34041 - default: https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/2.14.1/airbyte-ci push: branches: @@ -16,17 +9,39 @@ on: pull_request: jobs: + get_ci_runner: + runs-on: ubuntu-latest + name: Get CI runner + steps: + - name: Checkout Airbyte + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + fetch-depth: 1 + - name: Get CI runner + id: get_ci_runner + uses: ./.github/actions/airbyte-ci-requirements + with: + runner_type: "format" + runner_size: "medium" + airbyte_ci_command: "format" + github_token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} + outputs: + runner_name: ${{ steps.get_ci_runner.outputs.runner_name }} format-check: - runs-on: "ci-runner-connector-format-medium-dagger-0-6-4" # IMPORTANT: This name must match the require check name on the branch protection settings name: "Check for formatting errors" + needs: get_ci_runner + runs-on: ${{ needs.get_ci_runner.outputs.runner_name }} steps: - name: Checkout Airbyte uses: actions/checkout@v3 with: ref: ${{ github.head_ref }} token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} - + fetch-depth: 1 - name: Run airbyte-ci format check [MASTER] id: airbyte_ci_format_check_all_master if: github.ref == 'refs/heads/master' @@ -42,9 +57,6 @@ jobs: github_token: ${{ secrets.GH_PAT_MAINTENANCE_OCTAVIA }} tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }} subcommand: "format check all" - # Pin to a specific version of airbyte-ci to avoid transient failures - # Mentioned in issue https://github.com/airbytehq/airbyte/issues/34041 - airbyte_ci_binary_url: https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/2.14.1/airbyte-ci - name: Run airbyte-ci format check [PULL REQUEST] id: airbyte_ci_format_check_all_pr @@ -61,9 +73,6 @@ jobs: github_token: ${{ secrets.GH_PAT_MAINTENANCE_OCTAVIA }} tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }} subcommand: "format check all" - # Pin to a specific version of airbyte-ci to avoid transient failures - # Mentioned in issue https://github.com/airbytehq/airbyte/issues/34041 - airbyte_ci_binary_url: https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/2.14.1/airbyte-ci - name: Run airbyte-ci format check [WORKFLOW DISPATCH] id: airbyte_ci_format_check_all_manual @@ -80,7 +89,6 @@ jobs: github_token: ${{ secrets.GH_PAT_MAINTENANCE_OCTAVIA }} tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }} subcommand: "format check all" - airbyte_ci_binary_url: ${{ github.event.inputs.airbyte-ci-binary-url }} - name: Match GitHub User to Slack User [MASTER] if: github.ref == 'refs/heads/master' diff --git a/.github/workflows/format_fix.yml b/.github/workflows/format_fix.yml index fdc33d8fd405b..35425a30f28bb 100644 --- a/.github/workflows/format_fix.yml +++ b/.github/workflows/format_fix.yml @@ -9,9 +9,31 @@ concurrency: on: workflow_dispatch: jobs: + get_ci_runner: + runs-on: ubuntu-latest + name: Get CI runner + steps: + - name: Checkout Airbyte + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + fetch-depth: 1 + - name: Get CI runner + id: get_ci_runner + uses: ./.github/actions/airbyte-ci-requirements + with: + runner_type: "format" + runner_size: "large" + airbyte_ci_command: "format" + github_token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} + outputs: + runner_name: ${{ steps.get_ci_runner.outputs.runner_name }} format-fix: - runs-on: "ci-runner-connector-format-medium-dagger-0-9-5" name: "Run airbyte-ci format fix all" + needs: get_ci_runner + runs-on: ${{ needs.get_ci_runner.outputs.runner_name }} steps: - name: Checkout Airbyte uses: actions/checkout@v3 diff --git a/.github/workflows/metadata_service_deploy_orchestrator_dagger.yml b/.github/workflows/metadata_service_deploy_orchestrator_dagger.yml index 34d8a42490dd2..eaaab555b4e3d 100644 --- a/.github/workflows/metadata_service_deploy_orchestrator_dagger.yml +++ b/.github/workflows/metadata_service_deploy_orchestrator_dagger.yml @@ -8,9 +8,31 @@ on: paths: - "airbyte-ci/connectors/metadata_service/orchestrator/**" jobs: + get_ci_runner: + runs-on: ubuntu-latest + name: Get CI runner + steps: + - name: Checkout Airbyte + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + fetch-depth: 1 + - name: Get CI runner + id: get_ci_runner + uses: ./.github/actions/airbyte-ci-requirements + with: + runner_type: "test" # We don't have a specific runner for metadata, let's use the test one + runner_size: "large" + airbyte_ci_command: "metadata" + github_token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} + outputs: + runner_name: ${{ steps.get_ci_runner.outputs.runner_name }} connector_metadata_service_deploy_orchestrator: name: Connector metadata service deploy orchestrator - runs-on: ci-runner-connector-test-large-dagger-0-9-5 + needs: get_ci_runner + runs-on: ${{ needs.get_ci_runner.outputs.runner_name }} steps: - name: Checkout Airbyte uses: actions/checkout@v2 diff --git a/.github/workflows/publish_connectors.yml b/.github/workflows/publish_connectors.yml index 228faae2abd94..f0d10033f4a2d 100644 --- a/.github/workflows/publish_connectors.yml +++ b/.github/workflows/publish_connectors.yml @@ -14,18 +14,32 @@ on: publish-options: description: "Options to pass to the 'airbyte-ci connectors publish' command. Use --pre-release or --main-release depending on whether you want to publish a dev image or not. " default: "--pre-release" - runs-on: - type: string - default: ci-runner-connector-publish-large-dagger-0-9-5 - required: true - airbyte-ci-binary-url: - description: "URL to airbyte-ci binary" - required: false - default: https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci jobs: + get_ci_runner: + runs-on: ubuntu-latest + name: Get CI runner + steps: + - name: Checkout Airbyte + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + fetch-depth: 1 + - name: Get CI runner + id: get_ci_runner + uses: ./.github/actions/airbyte-ci-requirements + with: + runner_type: "publish" + runner_size: "large" + airbyte_ci_command: "connectors publish" + github_token: ${{ secrets.GH_PAT_APPROVINGTON_OCTAVIA }} + sentry_dsn: ${{ secrets.SENTRY_AIRBYTE_CI_DSN }} + outputs: + runner_name: ${{ steps.get_ci_runner.outputs.runner_name }} publish_connectors: name: Publish connectors - runs-on: ${{ inputs.runs-on || 'ci-runner-connector-publish-large-dagger-0-9-5' }} + needs: get_ci_runner + runs-on: ${{ needs.get_ci_runner.outputs.runner_name }} steps: - name: Checkout Airbyte uses: actions/checkout@v3 @@ -70,7 +84,6 @@ jobs: s3_build_cache_secret_key: ${{ secrets.SELF_RUNNER_AWS_SECRET_ACCESS_KEY }} tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }} subcommand: "connectors ${{ github.event.inputs.connectors-options }} publish ${{ github.event.inputs.publish-options }}" - airbyte_ci_binary_url: ${{ github.event.inputs.airbyte-ci-binary-url }} set-instatus-incident-on-failure: name: Create Instatus Incident on Failure From bc39ff5524239ff7313bb045ddf2f4933b88eba2 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jan 2024 15:33:44 +0100 Subject: [PATCH 116/574] source-instagram: Convert to airbyte-lib (#34254) --- .../connectors/source-instagram/main.py | 9 ++------- .../connectors/source-instagram/metadata.yaml | 2 +- .../connectors/source-instagram/setup.py | 5 +++++ .../source-instagram/source_instagram/run.py | 14 ++++++++++++++ docs/integrations/sources/instagram.md | 3 ++- 5 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 airbyte-integrations/connectors/source-instagram/source_instagram/run.py diff --git a/airbyte-integrations/connectors/source-instagram/main.py b/airbyte-integrations/connectors/source-instagram/main.py index 7dfe307855193..0a871930a0156 100644 --- a/airbyte-integrations/connectors/source-instagram/main.py +++ b/airbyte-integrations/connectors/source-instagram/main.py @@ -2,12 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_instagram import SourceInstagram +from source_instagram.run import run if __name__ == "__main__": - source = SourceInstagram() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-instagram/metadata.yaml b/airbyte-integrations/connectors/source-instagram/metadata.yaml index 2d86b473717d6..0c669ca2fe5d4 100644 --- a/airbyte-integrations/connectors/source-instagram/metadata.yaml +++ b/airbyte-integrations/connectors/source-instagram/metadata.yaml @@ -7,7 +7,7 @@ data: connectorSubtype: api connectorType: source definitionId: 6acf6b55-4f1e-4fca-944e-1a3caef8aba8 - dockerImageTag: 3.0.1 + dockerImageTag: 3.0.2 dockerRepository: airbyte/source-instagram githubIssueLabel: source-instagram icon: instagram.svg diff --git a/airbyte-integrations/connectors/source-instagram/setup.py b/airbyte-integrations/connectors/source-instagram/setup.py index 50805eca08439..cfaf2e20122fe 100644 --- a/airbyte-integrations/connectors/source-instagram/setup.py +++ b/airbyte-integrations/connectors/source-instagram/setup.py @@ -19,6 +19,11 @@ ] setup( + entry_points={ + "console_scripts": [ + "source-instagram=source_instagram.run:run", + ], + }, name="source_instagram", description="Source implementation for Instagram.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-instagram/source_instagram/run.py b/airbyte-integrations/connectors/source-instagram/source_instagram/run.py new file mode 100644 index 0000000000000..c012b2e2292a1 --- /dev/null +++ b/airbyte-integrations/connectors/source-instagram/source_instagram/run.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_instagram import SourceInstagram + + +def run(): + source = SourceInstagram() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/instagram.md b/docs/integrations/sources/instagram.md index b3f8b2fcf0135..9d6e0eb3e4ca2 100644 --- a/docs/integrations/sources/instagram.md +++ b/docs/integrations/sources/instagram.md @@ -113,6 +113,7 @@ Instagram limits the number of requests that can be made at a time. See Facebook | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------| +| 3.0.2 | 2024-01-15 | [34254](https://github.com/airbytehq/airbyte/pull/34254) | prepare for airbyte-lib | | 3.0.1 | 2024-01-08 | [33989](https://github.com/airbytehq/airbyte/pull/33989) | Remove metrics from video feed | | 3.0.0 | 2024-01-05 | [33930](https://github.com/airbytehq/airbyte/pull/33930) | Upgrade to API v18.0 | | 2.0.1 | 2024-01-03 | [33889](https://github.com/airbytehq/airbyte/pull/33889) | Change requested metrics for stream `media_insights` | @@ -141,4 +142,4 @@ Instagram limits the number of requests that can be made at a time. See Facebook | 0.1.7 | 2021-07-19 | [4805](https://github.com/airbytehq/airbyte/pull/4805) | Add support for previous `STATE` format | | 0.1.6 | 2021-07-07 | [4210](https://github.com/airbytehq/airbyte/pull/4210) | Refactor connector to use CDK: - improve error handling - fix sync fail with HTTP status 400 - integrate SAT | - \ No newline at end of file + From e0adbe87bcbe5320fd9820bfe4058a28ab39e0e5 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jan 2024 15:35:47 +0100 Subject: [PATCH 117/574] source-marketo: Convert to airbyte-lib (#34246) --- .../connectors/source-marketo/main.py | 9 ++------- .../connectors/source-marketo/metadata.yaml | 2 +- .../connectors/source-marketo/setup.py | 5 +++++ .../source-marketo/source_marketo/run.py | 14 ++++++++++++++ docs/integrations/sources/marketo.md | 3 ++- 5 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 airbyte-integrations/connectors/source-marketo/source_marketo/run.py diff --git a/airbyte-integrations/connectors/source-marketo/main.py b/airbyte-integrations/connectors/source-marketo/main.py index 127c4d2c05ad0..4b7b8e8d1708c 100644 --- a/airbyte-integrations/connectors/source-marketo/main.py +++ b/airbyte-integrations/connectors/source-marketo/main.py @@ -2,12 +2,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - -import sys - -from airbyte_cdk.entrypoint import launch -from source_marketo import SourceMarketo +from source_marketo.run import run if __name__ == "__main__": - source = SourceMarketo() - launch(source, sys.argv[1:]) + run() diff --git a/airbyte-integrations/connectors/source-marketo/metadata.yaml b/airbyte-integrations/connectors/source-marketo/metadata.yaml index b8b9a21e5952c..6fb5daa2c2a96 100644 --- a/airbyte-integrations/connectors/source-marketo/metadata.yaml +++ b/airbyte-integrations/connectors/source-marketo/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: 9e0556f4-69df-4522-a3fb-03264d36b348 - dockerImageTag: 1.2.4 + dockerImageTag: 1.2.5 dockerRepository: airbyte/source-marketo documentationUrl: https://docs.airbyte.com/integrations/sources/marketo githubIssueLabel: source-marketo diff --git a/airbyte-integrations/connectors/source-marketo/setup.py b/airbyte-integrations/connectors/source-marketo/setup.py index 1588bd2fc2a59..b8dfcde912ade 100644 --- a/airbyte-integrations/connectors/source-marketo/setup.py +++ b/airbyte-integrations/connectors/source-marketo/setup.py @@ -17,6 +17,11 @@ ] setup( + entry_points={ + "console_scripts": [ + "source-marketo=source_marketo.run:run", + ], + }, name="source_marketo", description="Source implementation for Marketo.", author="Airbyte", diff --git a/airbyte-integrations/connectors/source-marketo/source_marketo/run.py b/airbyte-integrations/connectors/source-marketo/source_marketo/run.py new file mode 100644 index 0000000000000..0831c3167f5f7 --- /dev/null +++ b/airbyte-integrations/connectors/source-marketo/source_marketo/run.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + + +import sys + +from airbyte_cdk.entrypoint import launch +from source_marketo import SourceMarketo + + +def run(): + source = SourceMarketo() + launch(source, sys.argv[1:]) diff --git a/docs/integrations/sources/marketo.md b/docs/integrations/sources/marketo.md index 4be65376be1b3..5e5f7ad883fd5 100644 --- a/docs/integrations/sources/marketo.md +++ b/docs/integrations/sources/marketo.md @@ -117,6 +117,7 @@ If the 50,000 limit is too stringent, contact Marketo support for a quota increa | Version | Date | Pull Request | Subject | |:---------|:-----------|:---------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------| +| 1.2.5 | 2024-01-15 | [34246](https://github.com/airbytehq/airbyte/pull/34246) | prepare for airbyte-lib | | `1.2.4` | 2024-01-08 | [33999](https://github.com/airbytehq/airbyte/pull/33999) | Fix for `Export daily quota exceeded` | | `1.2.3` | 2023-08-02 | [28999](https://github.com/airbytehq/airbyte/pull/28999) | Fix for ` _csv.Error: line contains NUL` | | `1.2.2` | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | @@ -139,4 +140,4 @@ If the 50,000 limit is too stringent, contact Marketo support for a quota increa | `0.1.3` | 2021-12-10 | [8429](https://github.com/airbytehq/airbyte/pull/8578) | Updated titles and descriptions | | `0.1.2` | 2021-12-03 | [8483](https://github.com/airbytehq/airbyte/pull/8483) | Improve field conversion to conform schema | | `0.1.1` | 2021-11-29 | [0000](https://github.com/airbytehq/airbyte/pull/0000) | Fix timestamp value format issue | -| `0.1.0` | 2021-09-06 | [5863](https://github.com/airbytehq/airbyte/pull/5863) | Release Marketo CDK Connector | \ No newline at end of file +| `0.1.0` | 2021-09-06 | [5863](https://github.com/airbytehq/airbyte/pull/5863) | Release Marketo CDK Connector | From 446eae351037c014f1db7542cb780a6c0aec84bd Mon Sep 17 00:00:00 2001 From: Augustin Date: Tue, 16 Jan 2024 15:44:34 +0100 Subject: [PATCH 118/574] airbyte-ci: Introduce `--only-step` option for connector tests (#34276) --- airbyte-ci/connectors/pipelines/README.md | 7 +- .../airbyte_ci/connectors/test/commands.py | 17 ++++- .../pipelines/helpers/execution/run_steps.py | 67 ++++++++++++++++++- .../connectors/pipelines/pyproject.toml | 2 +- .../test_execution/test_run_steps.py | 63 +++++++++++++++++ 5 files changed, 150 insertions(+), 6 deletions(-) diff --git a/airbyte-ci/connectors/pipelines/README.md b/airbyte-ci/connectors/pipelines/README.md index 20e7bdf6ad3ea..cb91ce4b03d0d 100644 --- a/airbyte-ci/connectors/pipelines/README.md +++ b/airbyte-ci/connectors/pipelines/README.md @@ -223,6 +223,9 @@ Test certified connectors: Test connectors changed on the current branch: `airbyte-ci connectors --modified test` +Run acceptance test only on the modified connectors, just run its full refresh tests: +`airbyte-ci connectors --modified test --only-step="acceptance" --acceptance.-k=test_full_refresh` + #### What it runs ```mermaid @@ -261,11 +264,12 @@ flowchart TD | Option | Multiple | Default value | Description | | ------------------------------------------------------- | -------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--skip-step/-x` | True | | Skip steps by id e.g. `-x unit -x acceptance` | +| `--only-step/-k` | True | | Only run specific steps by id e.g. `-k unit -k acceptance` | | `--fail-fast` | False | False | Abort after any tests fail, rather than continuing to run additional tests. Use this setting to confirm a known bug is fixed (or not), or when you only require a pass/fail result. | | `--code-tests-only` | True | False | Skip any tests not directly related to code updates. For instance, metadata checks, version bump checks, changelog verification, etc. Use this setting to help focus on code quality during development. | | `--concurrent-cat` | False | False | Make CAT tests run concurrently using pytest-xdist. Be careful about source or destination API rate limits. | | `--.=` | True | | You can pass extra parameters for specific test steps. More details in the extra parameters section below | -| `--ci-requirements` | False | | | Output the CI requirements as a JSON payload. It is used to determine the CI runner to use. +| `--ci-requirements` | False | | | Output the CI requirements as a JSON payload. It is used to determine the CI runner to use. Note: @@ -539,6 +543,7 @@ E.G.: running `pytest` on a specific test folder: | Version | PR | Description | | ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| 3.4.0 | [#34276](https://github.com/airbytehq/airbyte/pull/34276) | Introduce `--only-step` option for connector tests. | | 3.3.0 | [#34218](https://github.com/airbytehq/airbyte/pull/34218) | Introduce `--ci-requirements` option for client defined CI runners. | | 3.2.0 | [#34050](https://github.com/airbytehq/airbyte/pull/34050) | Connector test steps can take extra parameters | | 3.1.3 | [#34136](https://github.com/airbytehq/airbyte/pull/34136) | Fix issue where dagger excludes were not being properly applied | diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/commands.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/commands.py index 9d97f0c90a7bf..245f6156228b4 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/commands.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/commands.py @@ -53,10 +53,19 @@ @click.option( "--skip-step", "-x", + "skip_steps", multiple=True, type=click.Choice([step_id.value for step_id in CONNECTOR_TEST_STEP_ID]), help="Skip a step by name. Can be used multiple times to skip multiple steps.", ) +@click.option( + "--only-step", + "-k", + "only_steps", + multiple=True, + type=click.Choice([step_id.value for step_id in CONNECTOR_TEST_STEP_ID]), + help="Only run specific step by name. Can be used multiple times to keep multiple steps.", +) @click.argument( "extra_params", nargs=-1, type=click.UNPROCESSED, callback=argument_parsing.build_extra_params_mapping(CONNECTOR_TEST_STEP_ID) ) @@ -66,7 +75,8 @@ async def test( code_tests_only: bool, fail_fast: bool, concurrent_cat: bool, - skip_step: List[str], + skip_steps: List[str], + only_steps: List[str], extra_params: Dict[CONNECTOR_TEST_STEP_ID, STEP_PARAMS], ) -> bool: """Runs a test pipeline for the selected connectors. @@ -74,6 +84,8 @@ async def test( Args: ctx (click.Context): The click context. """ + if only_steps and skip_steps: + raise click.UsageError("Cannot use both --only-step and --skip-step at the same time.") if ctx.obj["is_ci"]: fail_if_missing_docker_hub_creds(ctx) if ctx.obj["is_ci"] and ctx.obj["pull_request"] and ctx.obj["pull_request"].draft: @@ -89,7 +101,8 @@ async def test( run_step_options = RunStepOptions( fail_fast=fail_fast, - skip_steps=[CONNECTOR_TEST_STEP_ID(step_id) for step_id in skip_step], + skip_steps=[CONNECTOR_TEST_STEP_ID(step_id) for step_id in skip_steps], + keep_steps=[CONNECTOR_TEST_STEP_ID(step_id) for step_id in only_steps], step_params=extra_params, ) connectors_tests_contexts = [ diff --git a/airbyte-ci/connectors/pipelines/pipelines/helpers/execution/run_steps.py b/airbyte-ci/connectors/pipelines/pipelines/helpers/execution/run_steps.py index 8fb320d9fd5e7..5db187af307c3 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/helpers/execution/run_steps.py +++ b/airbyte-ci/connectors/pipelines/pipelines/helpers/execution/run_steps.py @@ -8,7 +8,7 @@ import inspect from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Tuple, Union +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Set, Tuple, Union import anyio import asyncer @@ -27,16 +27,78 @@ class InvalidStepConfiguration(Exception): pass +def _get_dependency_graph(steps: STEP_TREE) -> Dict[str, List[str]]: + """ + Get the dependency graph of a step tree. + """ + dependency_graph: Dict[str, List[str]] = {} + for step in steps: + if isinstance(step, StepToRun): + dependency_graph[step.id] = step.depends_on + elif isinstance(step, list): + nested_dependency_graph = _get_dependency_graph(list(step)) + dependency_graph = {**dependency_graph, **nested_dependency_graph} + else: + raise Exception(f"Unexpected step type: {type(step)}") + + return dependency_graph + + +def _get_transitive_dependencies_for_step_id( + dependency_graph: Dict[str, List[str]], step_id: str, visited: Optional[Set[str]] = None +) -> List[str]: + """Get the transitive dependencies for a step id. + + Args: + dependency_graph (Dict[str, str]): The dependency graph to use. + step_id (str): The step id to get the transitive dependencies for. + visited (Optional[Set[str]], optional): The set of visited step ids. Defaults to None. + + Returns: + List[str]: List of transitive dependencies as step ids. + """ + if visited is None: + visited = set() + + if step_id not in visited: + visited.add(step_id) + + dependencies: List[str] = dependency_graph.get(step_id, []) + for dependency in dependencies: + dependencies.extend(_get_transitive_dependencies_for_step_id(dependency_graph, dependency, visited)) + + return dependencies + else: + return [] + + @dataclass class RunStepOptions: """Options for the run_step function.""" fail_fast: bool = True skip_steps: List[str] = field(default_factory=list) + keep_steps: List[str] = field(default_factory=list) log_step_tree: bool = True concurrency: int = 10 step_params: Dict[CONNECTOR_TEST_STEP_ID, STEP_PARAMS] = field(default_factory=dict) + def __post_init__(self) -> None: + if self.skip_steps and self.keep_steps: + raise ValueError("Cannot use both skip_steps and keep_steps at the same time") + + def get_step_ids_to_skip(self, runnables: STEP_TREE) -> List[str]: + if self.skip_steps: + return self.skip_steps + if self.keep_steps: + step_ids_to_keep = set(self.keep_steps) + dependency_graph = _get_dependency_graph(runnables) + all_step_ids = set(dependency_graph.keys()) + for step_id in self.keep_steps: + step_ids_to_keep.update(_get_transitive_dependencies_for_step_id(dependency_graph, step_id)) + return list(all_step_ids - step_ids_to_keep) + return [] + @dataclass(frozen=True) class StepToRun: @@ -217,6 +279,7 @@ async def run_steps( if not runnables: return results + step_ids_to_skip = options.get_step_ids_to_skip(runnables) # Log the step tree if options.log_step_tree: main_logger.info(f"STEP TREE: {runnables}") @@ -232,7 +295,7 @@ async def run_steps( steps_to_evaluate, remaining_steps = _get_next_step_group(runnables) # Remove any skipped steps - steps_to_run, results = _filter_skipped_steps(steps_to_evaluate, options.skip_steps, results) + steps_to_run, results = _filter_skipped_steps(steps_to_evaluate, step_ids_to_skip, results) # Run all steps in list concurrently semaphore = anyio.Semaphore(options.concurrency) diff --git a/airbyte-ci/connectors/pipelines/pyproject.toml b/airbyte-ci/connectors/pipelines/pyproject.toml index 4c7be71da3418..3315d923151aa 100644 --- a/airbyte-ci/connectors/pipelines/pyproject.toml +++ b/airbyte-ci/connectors/pipelines/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pipelines" -version = "3.3.0" +version = "3.4.0" description = "Packaged maintained by the connector operations team to perform CI for connectors' pipelines" authors = ["Airbyte "] diff --git a/airbyte-ci/connectors/pipelines/tests/test_helpers/test_execution/test_run_steps.py b/airbyte-ci/connectors/pipelines/tests/test_helpers/test_execution/test_run_steps.py index cc2f2e4cb7c68..b1975799ae354 100644 --- a/airbyte-ci/connectors/pipelines/tests/test_helpers/test_execution/test_run_steps.py +++ b/airbyte-ci/connectors/pipelines/tests/test_helpers/test_execution/test_run_steps.py @@ -359,3 +359,66 @@ async def test_run_steps_with_params(): TestStep.accept_extra_params = True await run_steps(steps, options=options) assert steps[0].step.params_as_cli_options == ["--param1=value1"] + + +class TestRunStepOptions: + def test_init(self): + options = RunStepOptions() + assert options.fail_fast is True + assert options.concurrency == 10 + assert options.skip_steps == [] + assert options.step_params == {} + + options = RunStepOptions(fail_fast=False, concurrency=1, skip_steps=["step1"], step_params={"step1": {"--param1": ["value1"]}}) + assert options.fail_fast is False + assert options.concurrency == 1 + assert options.skip_steps == ["step1"] + assert options.step_params == {"step1": {"--param1": ["value1"]}} + + with pytest.raises(ValueError): + RunStepOptions(skip_steps=["step1"], keep_steps=["step2"]) + + @pytest.mark.parametrize( + "step_tree, options, expected_skipped_ids", + [ + ( + [ + [StepToRun(id="step1", step=TestStep(test_context)), StepToRun(id="step2", step=TestStep(test_context))], + StepToRun(id="step3", step=TestStep(test_context)), + StepToRun(id="step4", step=TestStep(test_context), depends_on=["step3", "step1"]), + StepToRun(id="step5", step=TestStep(test_context)), + ], + RunStepOptions(keep_steps=["step4"]), + {"step2", "step5"}, + ), + ( + [ + [StepToRun(id="step1", step=TestStep(test_context)), StepToRun(id="step2", step=TestStep(test_context))], + StepToRun(id="step3", step=TestStep(test_context)), + [ + StepToRun(id="step4", step=TestStep(test_context), depends_on=["step1"]), + StepToRun(id="step6", step=TestStep(test_context), depends_on=["step4", "step5"]), + ], + StepToRun(id="step5", step=TestStep(test_context), depends_on=["step3"]), + ], + RunStepOptions(keep_steps=["step6"]), + {"step2"}, + ), + ( + [ + [StepToRun(id="step1", step=TestStep(test_context)), StepToRun(id="step2", step=TestStep(test_context))], + StepToRun(id="step3", step=TestStep(test_context)), + [ + StepToRun(id="step4", step=TestStep(test_context), depends_on=["step1"]), + StepToRun(id="step6", step=TestStep(test_context), depends_on=["step4", "step5"]), + ], + StepToRun(id="step5", step=TestStep(test_context), depends_on=["step3"]), + ], + RunStepOptions(skip_steps=["step1"]), + {"step1"}, + ), + ], + ) + def test_get_step_ids_to_skip(self, step_tree, options, expected_skipped_ids): + skipped_ids = options.get_step_ids_to_skip(step_tree) + assert set(skipped_ids) == expected_skipped_ids From 88f937dab05031a2cd92fa4f970c4124b8249333 Mon Sep 17 00:00:00 2001 From: Aske Ching Date: Tue, 16 Jan 2024 17:24:57 +0100 Subject: [PATCH 119/574] Fix typo in docs (#34293) --- docs/using-airbyte/getting-started/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using-airbyte/getting-started/readme.md b/docs/using-airbyte/getting-started/readme.md index fbef7470ef116..62715935cf1b9 100644 --- a/docs/using-airbyte/getting-started/readme.md +++ b/docs/using-airbyte/getting-started/readme.md @@ -28,7 +28,7 @@ To use Airbyte Open Source, you can use on the following options to deploy it on - [On Aws](/deploying-airbyte/on-aws-ec2.md) - [On Azure VM Cloud Shell](/deploying-airbyte/on-azure-vm-cloud-shell.md) - [On Digital Ocean Droplet](/deploying-airbyte/on-digitalocean-droplet.md) -- [On GCP.md](/deploying-airbyte/on-gcp-compute-engine.md) +- [On GCP](/deploying-airbyte/on-gcp-compute-engine.md) - [On Kubernetes](/deploying-airbyte/on-kubernetes-via-helm.md) - [On OCI VM](/deploying-airbyte/on-oci-vm.md) - [On Restack](/deploying-airbyte/on-restack.md) From 0935449af996deb90e595eb6260de828aa8fd797 Mon Sep 17 00:00:00 2001 From: Stephane Geneix <147216312+stephane-airbyte@users.noreply.github.com> Date: Tue, 16 Jan 2024 08:30:43 -0800 Subject: [PATCH 120/574] fix precommit format to ignore new airbyte-ci versions (#34234) if there's a new version of airbyte-ci available, we fail the pre-push hook and force an update. For some reason, that doesn't even satisfy pre-push hooks on graphite. I really think it's an unnecessary check during hooks, and only adds friction --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c41a749100cb..4fd1a68cdc3bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: hooks: - id: format-fix-all-on-push always_run: true - entry: airbyte-ci format fix all + entry: airbyte-ci --disable-update-check format fix all language: system name: Run airbyte-ci format fix on git push (~30s) pass_filenames: false From 1a16deb461a7d509e9ca1e69b5b139edf0f9b021 Mon Sep 17 00:00:00 2001 From: Ella Rohm-Ensing Date: Tue, 16 Jan 2024 12:57:58 -0600 Subject: [PATCH 121/574] dagster: update to 1.5, remove unnecessary resources for test sensor (#34067) Co-authored-by: Ben Church --- .../orchestrator/orchestrator/__init__.py | 13 +- .../metadata_service/orchestrator/poetry.lock | 3083 ++++++++--------- .../orchestrator/pyproject.toml | 14 +- airbyte-ci/connectors/pipelines/README.md | 1 + .../pipelines/airbyte_ci/metadata/pipeline.py | 4 +- .../connectors/pipelines/pyproject.toml | 2 +- 6 files changed, 1509 insertions(+), 1608 deletions(-) diff --git a/airbyte-ci/connectors/metadata_service/orchestrator/orchestrator/__init__.py b/airbyte-ci/connectors/metadata_service/orchestrator/orchestrator/__init__.py index edec67baa4541..336652744150b 100644 --- a/airbyte-ci/connectors/metadata_service/orchestrator/orchestrator/__init__.py +++ b/airbyte-ci/connectors/metadata_service/orchestrator/orchestrator/__init__.py @@ -113,13 +113,18 @@ ), } -CONNECTOR_TEST_REPORT_RESOURCE_TREE = { - **SLACK_RESOURCE_TREE, - **GITHUB_RESOURCE_TREE, +CONNECTOR_TEST_REPORT_SENSOR_RESOURCE_TREE = { **GCS_RESOURCE_TREE, "latest_nightly_complete_file_blobs": gcs_directory_blobs.configured( {"gcs_bucket": {"env": "CI_REPORT_BUCKET"}, "prefix": NIGHTLY_FOLDER, "match_regex": f".*{NIGHTLY_COMPLETE_REPORT_FILE_NAME}$"} ), +} + +CONNECTOR_TEST_REPORT_RESOURCE_TREE = { + **SLACK_RESOURCE_TREE, + **GITHUB_RESOURCE_TREE, + **GCS_RESOURCE_TREE, + **CONNECTOR_TEST_REPORT_SENSOR_RESOURCE_TREE, "latest_nightly_test_output_file_blobs": gcs_directory_blobs.configured( { "gcs_bucket": {"env": "CI_REPORT_BUCKET"}, @@ -155,7 +160,7 @@ ), new_gcs_blobs_sensor( job=generate_nightly_reports, - resources_def=CONNECTOR_TEST_REPORT_RESOURCE_TREE, + resources_def=CONNECTOR_TEST_REPORT_SENSOR_RESOURCE_TREE, gcs_blobs_resource_key="latest_nightly_complete_file_blobs", interval=(1 * 60 * 60), ), diff --git a/airbyte-ci/connectors/metadata_service/orchestrator/poetry.lock b/airbyte-ci/connectors/metadata_service/orchestrator/poetry.lock index 616f8c9d5095d..d350d91b4064d 100644 --- a/airbyte-ci/connectors/metadata_service/orchestrator/poetry.lock +++ b/airbyte-ci/connectors/metadata_service/orchestrator/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "alembic" -version = "1.11.2" +version = "1.13.1" description = "A database migration tool for SQLAlchemy." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "alembic-1.11.2-py3-none-any.whl", hash = "sha256:7981ab0c4fad4fe1be0cf183aae17689fe394ff874fd2464adb774396faf0796"}, - {file = "alembic-1.11.2.tar.gz", hash = "sha256:678f662130dc540dac12de0ea73de9f89caea9dbea138f60ef6263149bf84657"}, + {file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"}, + {file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"}, ] [package.dependencies] @@ -17,7 +17,7 @@ SQLAlchemy = ">=1.3.0" typing-extensions = ">=4" [package.extras] -tz = ["python-dateutil"] +tz = ["backports.zoneinfo"] [[package]] name = "aniso8601" @@ -35,24 +35,25 @@ dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"] [[package]] name = "anyio" -version = "3.7.1" +version = "4.2.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, + {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, + {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, ] [package.dependencies] -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (<0.22)"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] [[package]] name = "appdirs" @@ -67,21 +68,22 @@ files = [ [[package]] name = "attrs" -version = "23.1.0" +version = "23.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] [package.extras] cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] +dev = ["attrs[tests]", "pre-commit"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [[package]] name = "backoff" @@ -114,140 +116,130 @@ lxml = ["lxml"] [[package]] name = "build" -version = "0.10.0" +version = "1.0.3" description = "A simple, correct Python build frontend" optional = false python-versions = ">= 3.7" files = [ - {file = "build-0.10.0-py3-none-any.whl", hash = "sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171"}, - {file = "build-0.10.0.tar.gz", hash = "sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269"}, + {file = "build-1.0.3-py3-none-any.whl", hash = "sha256:589bf99a67df7c9cf07ec0ac0e5e2ea5d4b37ac63301c4986d1acb126aa83f8f"}, + {file = "build-1.0.3.tar.gz", hash = "sha256:538aab1b64f9828977f84bc63ae570b060a8ed1be419e7870b8b4fc5e6ea553b"}, ] [package.dependencies] colorama = {version = "*", markers = "os_name == \"nt\""} +importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} packaging = ">=19.0" pyproject_hooks = "*" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] -docs = ["furo (>=2021.08.31)", "sphinx (>=4.0,<5.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)"] -test = ["filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "toml (>=0.10.0)", "wheel (>=0.36.0)"] -typing = ["importlib-metadata (>=5.1)", "mypy (==0.991)", "tomli", "typing-extensions (>=3.7.4.3)"] +docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"] +test = ["filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "setuptools (>=56.0.0)", "setuptools (>=67.8.0)", "wheel (>=0.36.0)"] +typing = ["importlib-metadata (>=5.1)", "mypy (>=1.5.0,<1.6.0)", "tomli", "typing-extensions (>=3.7.4.3)"] virtualenv = ["virtualenv (>=20.0.35)"] [[package]] name = "cachecontrol" -version = "0.12.14" +version = "0.13.1" description = "httplib2 caching for requests" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "CacheControl-0.12.14-py2.py3-none-any.whl", hash = "sha256:1c2939be362a70c4e5f02c6249462b3b7a24441e4f1ced5e9ef028172edf356a"}, - {file = "CacheControl-0.12.14.tar.gz", hash = "sha256:d1087f45781c0e00616479bfd282c78504371ca71da017b49df9f5365a95feba"}, + {file = "cachecontrol-0.13.1-py3-none-any.whl", hash = "sha256:95dedbec849f46dda3137866dc28b9d133fc9af55f5b805ab1291833e4457aa4"}, + {file = "cachecontrol-0.13.1.tar.gz", hash = "sha256:f012366b79d2243a6118309ce73151bf52a38d4a5dac8ea57f09bd29087e506b"}, ] [package.dependencies] -lockfile = {version = ">=0.9", optional = true, markers = "extra == \"filecache\""} +filelock = {version = ">=3.8.0", optional = true, markers = "extra == \"filecache\""} msgpack = ">=0.5.2" -requests = "*" +requests = ">=2.16.0" [package.extras] -filecache = ["lockfile (>=0.9)"] +dev = ["CacheControl[filecache,redis]", "black", "build", "cherrypy", "mypy", "pytest", "pytest-cov", "sphinx", "tox", "types-redis", "types-requests"] +filecache = ["filelock (>=3.8.0)"] redis = ["redis (>=2.10.5)"] [[package]] name = "cachetools" -version = "5.3.1" +version = "5.3.2" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, - {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, + {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, + {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, ] [[package]] name = "certifi" -version = "2023.7.22" +version = "2023.11.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] [[package]] name = "cffi" -version = "1.15.1" +version = "1.16.0" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, ] [package.dependencies] @@ -255,112 +247,127 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.2.0" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, - {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] name = "cleo" -version = "2.0.1" +version = "2.1.0" description = "Cleo allows you to create beautiful and testable command-line interfaces." optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "cleo-2.0.1-py3-none-any.whl", hash = "sha256:6eb133670a3ed1f3b052d53789017b6e50fca66d1287e6e6696285f4cb8ea448"}, - {file = "cleo-2.0.1.tar.gz", hash = "sha256:eb4b2e1f3063c11085cebe489a6e9124163c226575a3c3be69b2e51af4a15ec5"}, + {file = "cleo-2.1.0-py3-none-any.whl", hash = "sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e"}, + {file = "cleo-2.1.0.tar.gz", hash = "sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523"}, ] [package.dependencies] crashtest = ">=0.4.1,<0.5.0" -rapidfuzz = ">=2.2.0,<3.0.0" +rapidfuzz = ">=3.0.0,<4.0.0" [[package]] name = "click" -version = "8.1.6" +version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, - {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -407,48 +414,49 @@ files = [ [[package]] name = "croniter" -version = "1.4.1" +version = "2.0.1" description = "croniter provides iteration for datetime object with cron like format" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "croniter-1.4.1-py2.py3-none-any.whl", hash = "sha256:9595da48af37ea06ec3a9f899738f1b2c1c13da3c38cea606ef7cd03ea421128"}, - {file = "croniter-1.4.1.tar.gz", hash = "sha256:1a6df60eacec3b7a0aa52a8f2ef251ae3dd2a7c7c8b9874e73e791636d55a361"}, + {file = "croniter-2.0.1-py2.py3-none-any.whl", hash = "sha256:4cb064ce2d8f695b3b078be36ff50115cf8ac306c10a7e8653ee2a5b534673d7"}, + {file = "croniter-2.0.1.tar.gz", hash = "sha256:d199b2ec3ea5e82988d1f72022433c5f9302b3b3ea9e6bfd6a1518f6ea5e700a"}, ] [package.dependencies] python-dateutil = "*" +pytz = ">2021.1" [[package]] name = "cryptography" -version = "41.0.3" +version = "41.0.7" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, - {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, - {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, - {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, - {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, - {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, - {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, + {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, + {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, + {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, ] [package.dependencies] @@ -466,31 +474,31 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "dagit" -version = "1.4.4" +version = "1.5.14" description = "Web UI for dagster." optional = false python-versions = "*" files = [ - {file = "dagit-1.4.4-py3-none-any.whl", hash = "sha256:cf10a16546c6e81618af9cc6cbe8a1914c8e60df191c1fdd38c3ce8e874f64a5"}, - {file = "dagit-1.4.4.tar.gz", hash = "sha256:83778973f07b97ae415ecc67c86ee502395e7d882d474827a4e914766122dbf3"}, + {file = "dagit-1.5.14-py3-none-any.whl", hash = "sha256:2353eb039c99409adc2935593cbdf23cc10d0436ac68e54c548e7203b9412528"}, + {file = "dagit-1.5.14.tar.gz", hash = "sha256:7ce254bdee417e8e63730258f4de2551894dba3fa86b58a4236a26561d92d560"}, ] [package.dependencies] -dagster-webserver = "1.4.4" +dagster-webserver = "1.5.14" [package.extras] -notebook = ["dagster-webserver[notebook] (==1.4.4)"] -test = ["dagster-webserver[test] (==1.4.4)"] +notebook = ["dagster-webserver[notebook] (==1.5.14)"] +test = ["dagster-webserver[test] (==1.5.14)"] [[package]] name = "dagster" -version = "1.4.4" -description = "The data orchestration platform built for productivity." +version = "1.5.14" +description = "Dagster is an orchestration platform for the development, production, and observation of data assets." optional = false python-versions = "*" files = [ - {file = "dagster-1.4.4-py3-none-any.whl", hash = "sha256:8790005fef7d21e65bdf206908706b486181365b908242edf6d0d06a97901a75"}, - {file = "dagster-1.4.4.tar.gz", hash = "sha256:4e4d07609489b3499ab4d3f0b24796f860c57f35d5234d73bc6869f1dda39d47"}, + {file = "dagster-1.5.14-py3-none-any.whl", hash = "sha256:951df17927a4dd5d594ccd349c9f3b82f1c671b28ac47b4ec8bf5e3ee57ad254"}, + {file = "dagster-1.5.14.tar.gz", hash = "sha256:511ecbdbbab794853791badec77580162022cc2362a2e2b5f70661ec1ac1ce79"}, ] [package.dependencies] @@ -498,15 +506,16 @@ alembic = ">=1.2.1,<1.6.3 || >1.6.3,<1.7.0 || >1.7.0,<1.11.0 || >1.11.0" click = ">=5.0" coloredlogs = ">=6.1,<=14.0" croniter = ">=0.3.34" +dagster-pipes = "1.5.14" docstring-parser = "*" grpcio = ">=1.44.0" grpcio-health-checking = ">=1.44.0" Jinja2 = "*" packaging = ">=20.9" -pendulum = "*" -protobuf = ">=3.20.0" +pendulum = ">=0.7.0,<3" +protobuf = ">=3.20.0,<5" psutil = {version = ">=1.0", markers = "platform_system == \"Windows\""} -pydantic = "<1.10.7 || >1.10.7,<2.0.0" +pydantic = ">1.10.0,<1.10.7 || >1.10.7,<3" python-dateutil = "*" python-dotenv = "*" pytz = "*" @@ -514,60 +523,59 @@ pywin32 = {version = "!=226", markers = "platform_system == \"Windows\""} PyYAML = ">=5.1" requests = "*" setuptools = "*" -sqlalchemy = ">=1.0" +sqlalchemy = ">=1.0,<3" tabulate = "*" -tomli = "*" +tomli = "<3" toposort = ">=1.0" -tqdm = "*" -typing-extensions = ">=4.4.0" -universal-pathlib = "<0.1.0" +tqdm = "<5" +typing-extensions = ">=4.4.0,<5" +universal-pathlib = "*" watchdog = ">=0.8.3" [package.extras] -black = ["black[jupyter] (==22.12.0)"] docker = ["docker"] mypy = ["mypy (==0.991)"] -pyright = ["pandas-stubs", "pyright (==1.1.316)", "types-PyYAML", "types-backports", "types-certifi", "types-chardet", "types-croniter", "types-cryptography", "types-mock", "types-paramiko", "types-pkg-resources", "types-pyOpenSSL", "types-python-dateutil", "types-pytz", "types-requests", "types-simplejson", "types-six", "types-sqlalchemy (==1.4.53.34)", "types-tabulate", "types-toml", "types-tzlocal"] -ruff = ["ruff (==0.0.277)"] -test = ["buildkite-test-collector", "docker", "grpcio-tools (>=1.44.0)", "mock (==3.0.5)", "morefs[asynclocal]", "objgraph", "pytest (==7.0.1)", "pytest-cov (==2.10.1)", "pytest-dependency (==0.5.1)", "pytest-mock (==3.3.1)", "pytest-rerunfailures (==10.0)", "pytest-runner (==5.2)", "pytest-xdist (==2.1.0)", "responses (<=0.23.1)", "syrupy (<4)", "tox (==3.25.0)", "yamllint"] +pyright = ["pandas-stubs", "pyright (==1.1.339)", "types-PyYAML", "types-backports", "types-certifi", "types-chardet", "types-croniter", "types-cryptography", "types-mock", "types-paramiko", "types-pkg-resources", "types-pyOpenSSL", "types-python-dateutil", "types-pytz", "types-requests", "types-simplejson", "types-six", "types-sqlalchemy (==1.4.53.34)", "types-tabulate", "types-toml", "types-tzlocal"] +ruff = ["ruff (==0.1.7)"] +test = ["buildkite-test-collector", "docker", "grpcio-tools (>=1.44.0)", "mock (==3.0.5)", "morefs[asynclocal]", "mypy-protobuf", "objgraph", "pytest (>=7.0.1)", "pytest-cov (==2.10.1)", "pytest-dependency (==0.5.1)", "pytest-mock (==3.3.1)", "pytest-rerunfailures (==10.0)", "pytest-runner (==5.2)", "pytest-xdist (==3.3.1)", "responses (<=0.23.1)", "syrupy (<4)", "tox (==3.25.0)"] [[package]] name = "dagster-cloud" -version = "1.4.4" +version = "1.5.14" description = "" optional = false python-versions = "*" files = [ - {file = "dagster_cloud-1.4.4-py3-none-any.whl", hash = "sha256:fe0c1a098530d33cdb440dc29d6ae55fdcc02eb1e7ce3a6ea4582342881a6842"}, - {file = "dagster_cloud-1.4.4.tar.gz", hash = "sha256:047cf1dacac012311252cfb505f1229e912e3e175a9cbe0549ae6b3facfd5417"}, + {file = "dagster-cloud-1.5.14.tar.gz", hash = "sha256:d21c3f445d775feb1ba28049c75b2098e11bab27dc0f23633595875367b96d8b"}, + {file = "dagster_cloud-1.5.14-py3-none-any.whl", hash = "sha256:928c66649efd86acd959482741eb4b7eb92522f68c4cc49620f2d7a259d70aff"}, ] [package.dependencies] -dagster = "1.4.4" -dagster-cloud-cli = "1.4.4" -pex = "*" +dagster = "1.5.14" +dagster-cloud-cli = "1.5.14" +pex = ">=2.1.132" questionary = "*" requests = "*" typer = {version = "*", extras = ["all"]} [package.extras] -docker = ["dagster-docker (==0.20.4)", "docker"] -ecs = ["boto3", "dagster-aws (==0.20.4)"] -kubernetes = ["dagster-k8s (==0.20.4)", "kubernetes"] +docker = ["dagster-docker (==0.21.14)", "docker"] +ecs = ["boto3", "dagster-aws (==0.21.14)"] +kubernetes = ["dagster-k8s (==0.21.14)", "kubernetes"] pex = ["boto3"] sandbox = ["supervisor"] serverless = ["boto3"] -tests = ["black", "dagster-cloud-test-infra", "dagster-k8s (==0.20.4)", "docker", "httpretty", "isort", "kubernetes", "moto[all]", "mypy", "paramiko", "pylint", "pytest", "types-PyYAML", "types-requests"] +tests = ["dagster-cloud-test-infra", "dagster-dbt (==0.21.14)", "dagster-k8s (==0.21.14)", "dbt-core", "dbt-duckdb", "dbt-postgres", "dbt-snowflake", "docker", "httpretty", "isort", "kubernetes", "moto[all]", "mypy", "paramiko", "psutil", "pylint", "pytest", "types-PyYAML", "types-requests"] [[package]] name = "dagster-cloud-cli" -version = "1.4.4" +version = "1.5.14" description = "" optional = false python-versions = "*" files = [ - {file = "dagster_cloud_cli-1.4.4-py3-none-any.whl", hash = "sha256:f38f230bb21a4535765762f92b5d06438a507da7bab57fe7db91c27cc70fe60f"}, - {file = "dagster_cloud_cli-1.4.4.tar.gz", hash = "sha256:6ae9f5bd1b9235108c6131551752953a88613e71c20d9b4086597c8a9966f2a4"}, + {file = "dagster-cloud-cli-1.5.14.tar.gz", hash = "sha256:75045561a77e97c6b223b71d0fa7c2fe371c72e9818696849ec5b15df9a3e60c"}, + {file = "dagster_cloud_cli-1.5.14-py3-none-any.whl", hash = "sha256:6f9150ff2a8e0c2ddde1f831f0cee17932c0cc0e2e1b27cd701f61b2d7f42749"}, ] [package.dependencies] @@ -583,18 +591,18 @@ tests = ["freezegun"] [[package]] name = "dagster-gcp" -version = "0.20.4" +version = "0.21.14" description = "Package for GCP-specific Dagster framework op and resource components." optional = false python-versions = "*" files = [ - {file = "dagster-gcp-0.20.4.tar.gz", hash = "sha256:b3c76ea8398a41016e58374cd9699514ae1903e503b426347dea17adca0ea758"}, - {file = "dagster_gcp-0.20.4-py3-none-any.whl", hash = "sha256:2cb241f47e98cfbc3f3c2af64e7260923c6ba717929f672f4a039ec988b0de61"}, + {file = "dagster-gcp-0.21.14.tar.gz", hash = "sha256:caa8196d51f56cba658179a28ad6efb844493f705d4318d05b5edee82c1966c1"}, + {file = "dagster_gcp-0.21.14-py3-none-any.whl", hash = "sha256:28a7cebcbedc7b54b67f04281424a36cd89f722bdf5411173187855e2938cb69"}, ] [package.dependencies] -dagster = "1.4.4" -dagster-pandas = "0.20.4" +dagster = "1.5.14" +dagster-pandas = "0.21.14" db-dtypes = "*" google-api-python-client = "*" google-cloud-bigquery = "*" @@ -606,68 +614,78 @@ pyarrow = ["pyarrow"] [[package]] name = "dagster-graphql" -version = "1.4.4" +version = "1.5.14" description = "The GraphQL frontend to python dagster." optional = false python-versions = "*" files = [ - {file = "dagster-graphql-1.4.4.tar.gz", hash = "sha256:7ca85756393aa6a4d0c2a43044e3a0d3e3a61bffb527fa82c936126296bfb5c6"}, - {file = "dagster_graphql-1.4.4-py3-none-any.whl", hash = "sha256:f919459f1edb8be2e1d02a28fa3600869a27be5d52d66eb253902e155d1a5a04"}, + {file = "dagster-graphql-1.5.14.tar.gz", hash = "sha256:208c66bfd68021c1606b97f830e87d8f1c16051cbaaa9a1570682344a2c60999"}, + {file = "dagster_graphql-1.5.14-py3-none-any.whl", hash = "sha256:991b9f3342a6dc139b84e9f836acec4f47c925993b2208685c6518e4620e7015"}, ] [package.dependencies] -dagster = "1.4.4" -gql = {version = ">=3.0.0", extras = ["requests"]} -graphene = ">=3" +dagster = "1.5.14" +gql = {version = ">=3,<4", extras = ["requests"]} +graphene = ">=3,<4" requests = "*" starlette = "*" -urllib3 = "<2.0.0" [[package]] name = "dagster-pandas" -version = "0.20.4" +version = "0.21.14" description = "Utilities and examples for working with pandas and dagster, an opinionated framework for expressing data pipelines" optional = false python-versions = "*" files = [ - {file = "dagster-pandas-0.20.4.tar.gz", hash = "sha256:954055ce711017e151f3a3f0466d99d55ffc16bf4554e357777d7a02e3413993"}, - {file = "dagster_pandas-0.20.4-py3-none-any.whl", hash = "sha256:f5e37ad885cd44e79f06eae412792b6284f9a0568f4ba606f895fe467cccaf74"}, + {file = "dagster-pandas-0.21.14.tar.gz", hash = "sha256:f3a961086f8386939248a97ed3991996307c9fbe8cbcb2bad2af372635d33347"}, + {file = "dagster_pandas-0.21.14-py3-none-any.whl", hash = "sha256:eadd0f380f3331f7e6d135990c3f154e1f9b2b147d098c2dd019308335ac0ab9"}, ] [package.dependencies] -dagster = "1.4.4" +dagster = "1.5.14" pandas = "*" +[[package]] +name = "dagster-pipes" +version = "1.5.14" +description = "Toolkit for Dagster integrations with transform logic outside of Dagster" +optional = false +python-versions = "*" +files = [ + {file = "dagster-pipes-1.5.14.tar.gz", hash = "sha256:a3302565cfcdb7d7e697f813a6950f96259c4190cb19b76f256932b937d3a80d"}, + {file = "dagster_pipes-1.5.14-py3-none-any.whl", hash = "sha256:f65883a618595f49c84172b4e93d4fcba9e29e5d102f8dc064af5c56f51ab1a6"}, +] + [[package]] name = "dagster-slack" -version = "0.20.4" +version = "0.21.14" description = "A Slack client resource for posting to Slack" optional = false python-versions = "*" files = [ - {file = "dagster-slack-0.20.4.tar.gz", hash = "sha256:c0a8dcedd722f4d0f15eb4322d6a0160f0360e24e1bfffc612624f967b99e3d2"}, - {file = "dagster_slack-0.20.4-py3-none-any.whl", hash = "sha256:4e418012bd94fda8303044282aedaec1d11ce697f7495161f23b745885223914"}, + {file = "dagster-slack-0.21.14.tar.gz", hash = "sha256:7a77776f1eb088fe66a061c92f61aab294d42d0e867c97fb0a82132b1bbf8f52"}, + {file = "dagster_slack-0.21.14-py3-none-any.whl", hash = "sha256:5c6e2344c4fc4036184881d0a1809b446e24e88753cbf3333bc93787962832bc"}, ] [package.dependencies] -dagster = "1.4.4" +dagster = "1.5.14" slack-sdk = "*" [[package]] name = "dagster-webserver" -version = "1.4.4" +version = "1.5.14" description = "Web UI for dagster." optional = false python-versions = "*" files = [ - {file = "dagster_webserver-1.4.4-py3-none-any.whl", hash = "sha256:80ebb430617a1949c7d3019fd2cc29178467d1d6b8136bd09b64fb13ba09103a"}, - {file = "dagster_webserver-1.4.4.tar.gz", hash = "sha256:3b1b0316d5937478f8ff734c2de10e2f5ae3da500fbdea47948947496fc60646"}, + {file = "dagster-webserver-1.5.14.tar.gz", hash = "sha256:33f692f382bced3e05d8a78bc15fc086aa15de542c455efa2675fd9ca1f3e7b1"}, + {file = "dagster_webserver-1.5.14-py3-none-any.whl", hash = "sha256:d75a3e64ea6b5ea77e738905bf662e9fb1302789661421826c15cd4fe00685bd"}, ] [package.dependencies] click = ">=7.0,<9.0" -dagster = "1.4.4" -dagster-graphql = "1.4.4" +dagster = "1.5.14" +dagster-graphql = "1.5.14" starlette = "*" uvicorn = {version = "*", extras = ["standard"]} @@ -677,13 +695,13 @@ test = ["starlette[full]"] [[package]] name = "db-dtypes" -version = "1.1.1" +version = "1.2.0" description = "Pandas Data Types for SQL systems (BigQuery, Spanner)" optional = false python-versions = ">=3.7" files = [ - {file = "db-dtypes-1.1.1.tar.gz", hash = "sha256:ab485c85fef2454f3182427def0b0a3ab179b2871542787d33ba519d62078883"}, - {file = "db_dtypes-1.1.1-py2.py3-none-any.whl", hash = "sha256:23be34ea2bc91065447ecea4d5f107e46d1de223d152e69fa73673a62d5bd27d"}, + {file = "db-dtypes-1.2.0.tar.gz", hash = "sha256:3531bb1fb8b5fbab33121fe243ccc2ade16ab2524f4c113b05cc702a1908e6ea"}, + {file = "db_dtypes-1.2.0-py2.py3-none-any.whl", hash = "sha256:6320bddd31d096447ef749224d64aab00972ed20e4392d86f7d8b81ad79f7ff0"}, ] [package.dependencies] @@ -694,20 +712,20 @@ pyarrow = ">=3.0.0" [[package]] name = "deepdiff" -version = "6.3.1" +version = "6.7.1" description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." optional = false python-versions = ">=3.7" files = [ - {file = "deepdiff-6.3.1-py3-none-any.whl", hash = "sha256:eae2825b2e1ea83df5fc32683d9aec5a56e38b756eb2b280e00863ce4def9d33"}, - {file = "deepdiff-6.3.1.tar.gz", hash = "sha256:e8c1bb409a2caf1d757799add53b3a490f707dd792ada0eca7cac1328055097a"}, + {file = "deepdiff-6.7.1-py3-none-any.whl", hash = "sha256:58396bb7a863cbb4ed5193f548c56f18218060362311aa1dc36397b2f25108bd"}, + {file = "deepdiff-6.7.1.tar.gz", hash = "sha256:b367e6fa6caac1c9f500adc79ada1b5b1242c50d5f716a1a4362030197847d30"}, ] [package.dependencies] ordered-set = ">=4.0.2,<4.2.0" [package.extras] -cli = ["click (==8.1.3)", "pyyaml (==6.0)"] +cli = ["click (==8.1.3)", "pyyaml (==6.0.1)"] optimize = ["orjson"] [[package]] @@ -729,13 +747,13 @@ dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] [[package]] name = "distlib" -version = "0.3.7" +version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] @@ -762,67 +780,80 @@ files = [ [[package]] name = "dulwich" -version = "0.21.5" +version = "0.21.7" description = "Python Git Library" optional = false python-versions = ">=3.7" files = [ - {file = "dulwich-0.21.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8864719bc176cdd27847332a2059127e2f7bab7db2ff99a999873cb7fff54116"}, - {file = "dulwich-0.21.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3800cdc17d144c1f7e114972293bd6c46688f5bcc2c9228ed0537ded72394082"}, - {file = "dulwich-0.21.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e2f676bfed8146966fe934ee734969d7d81548fbd250a8308582973670a9dab1"}, - {file = "dulwich-0.21.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db330fb59fe3b9d253bdf0e49a521739db83689520c4921ab1c5242aaf77b82"}, - {file = "dulwich-0.21.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e8f6d4f4f4d01dd1d3c968e486d4cd77f96f772da7265941bc506de0944ddb9"}, - {file = "dulwich-0.21.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1cc0c9ba19ac1b2372598802bc9201a9c45e5d6f1f7a80ec40deeb10acc4e9ae"}, - {file = "dulwich-0.21.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61e10242b5a7a82faa8996b2c76239cfb633620b02cdd2946e8af6e7eb31d651"}, - {file = "dulwich-0.21.5-cp310-cp310-win32.whl", hash = "sha256:7f357639b56146a396f48e5e0bc9bbaca3d6d51c8340bd825299272b588fff5f"}, - {file = "dulwich-0.21.5-cp310-cp310-win_amd64.whl", hash = "sha256:891d5c73e2b66d05dbb502e44f027dc0dbbd8f6198bc90dae348152e69d0befc"}, - {file = "dulwich-0.21.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45d6198e804b539708b73a003419e48fb42ff2c3c6dd93f63f3b134dff6dd259"}, - {file = "dulwich-0.21.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c2a565d4e704d7f784cdf9637097141f6d47129c8fffc2fac699d57cb075a169"}, - {file = "dulwich-0.21.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:823091d6b6a1ea07dc4839c9752198fb39193213d103ac189c7669736be2eaff"}, - {file = "dulwich-0.21.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2c9931b657f2206abec0964ec2355ee2c1e04d05f8864e823ffa23c548c4548"}, - {file = "dulwich-0.21.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dc358c2ee727322a09b7c6da43d47a1026049dbd3ad8d612eddca1f9074b298"}, - {file = "dulwich-0.21.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6155ab7388ee01c670f7c5d8003d4e133eebebc7085a856c007989f0ba921b36"}, - {file = "dulwich-0.21.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a605e10d72f90a39ea2e634fbfd80f866fc4df29a02ea6db52ae92e5fd4a2003"}, - {file = "dulwich-0.21.5-cp311-cp311-win32.whl", hash = "sha256:daa607370722c3dce99a0022397c141caefb5ed32032a4f72506f4817ea6405b"}, - {file = "dulwich-0.21.5-cp311-cp311-win_amd64.whl", hash = "sha256:5e56b2c1911c344527edb2bf1a4356e2fb7e086b1ba309666e1e5c2224cdca8a"}, - {file = "dulwich-0.21.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:85d3401d08b1ec78c7d58ae987c4bb7b768a438f3daa74aeb8372bebc7fb16fa"}, - {file = "dulwich-0.21.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90479608e49db93d8c9e4323bc0ec5496678b535446e29d8fd67dc5bbb5d51bf"}, - {file = "dulwich-0.21.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9a6bf99f57bcac4c77fc60a58f1b322c91cc4d8c65dc341f76bf402622f89cb"}, - {file = "dulwich-0.21.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3e68b162af2aae995355e7920f89d50d72b53d56021e5ac0a546d493b17cbf7e"}, - {file = "dulwich-0.21.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0ab86d6d42e385bf3438e70f3c9b16de68018bd88929379e3484c0ef7990bd3c"}, - {file = "dulwich-0.21.5-cp37-cp37m-win32.whl", hash = "sha256:f2eeca6d61366cf5ee8aef45bed4245a67d4c0f0d731dc2383eabb80fa695683"}, - {file = "dulwich-0.21.5-cp37-cp37m-win_amd64.whl", hash = "sha256:1b20a3656b48c941d49c536824e1e5278a695560e8de1a83b53a630143c4552e"}, - {file = "dulwich-0.21.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3932b5e17503b265a85f1eda77ede647681c3bab53bc9572955b6b282abd26ea"}, - {file = "dulwich-0.21.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6616132d219234580de88ceb85dd51480dc43b1bdc05887214b8dd9cfd4a9d40"}, - {file = "dulwich-0.21.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:eaf6c7fb6b13495c19c9aace88821c2ade3c8c55b4e216cd7cc55d3e3807d7fa"}, - {file = "dulwich-0.21.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be12a46f73023970125808a4a78f610c055373096c1ecea3280edee41613eba8"}, - {file = "dulwich-0.21.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baecef0d8b9199822c7912876a03a1af17833f6c0d461efb62decebd45897e49"}, - {file = "dulwich-0.21.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:82f632afb9c7c341a875d46aaa3e6c5e586c7a64ce36c9544fa400f7e4f29754"}, - {file = "dulwich-0.21.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82cdf482f8f51fcc965ffad66180b54a9abaea9b1e985a32e1acbfedf6e0e363"}, - {file = "dulwich-0.21.5-cp38-cp38-win32.whl", hash = "sha256:c8ded43dc0bd2e65420eb01e778034be5ca7f72e397a839167eda7dcb87c4248"}, - {file = "dulwich-0.21.5-cp38-cp38-win_amd64.whl", hash = "sha256:2aba0fdad2a19bd5bb3aad6882580cb33359c67b48412ccd4cfccd932012b35e"}, - {file = "dulwich-0.21.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fd4ad079758514375f11469e081723ba8831ce4eaa1a64b41f06a3a866d5ac34"}, - {file = "dulwich-0.21.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7fe62685bf356bfb4d0738f84a3fcf0d1fc9e11fee152e488a20b8c66a52429e"}, - {file = "dulwich-0.21.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aae448da7d80306dda4fc46292fed7efaa466294571ab3448be16714305076f1"}, - {file = "dulwich-0.21.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b24cb1fad0525dba4872e9381bc576ea2a6dcdf06b0ed98f8e953e3b1d719b89"}, - {file = "dulwich-0.21.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e39b7c2c9bda6acae83b25054650a8bb7e373e886e2334721d384e1479bf04b"}, - {file = "dulwich-0.21.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26456dba39d1209fca17187db06967130e27eeecad2b3c2bbbe63467b0bf09d6"}, - {file = "dulwich-0.21.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:281310644e02e3aa6d76bcaffe2063b9031213c4916b5f1a6e68c25bdecfaba4"}, - {file = "dulwich-0.21.5-cp39-cp39-win32.whl", hash = "sha256:4814ca3209dabe0fe7719e9545fbdad7f8bb250c5a225964fe2a31069940c4cf"}, - {file = "dulwich-0.21.5-cp39-cp39-win_amd64.whl", hash = "sha256:c922a4573267486be0ef85216f2da103fb38075b8465dc0e90457843884e4860"}, - {file = "dulwich-0.21.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e52b20c4368171b7d32bd3ab0f1d2402e76ad4f2ea915ff9aa73bc9fa2b54d6d"}, - {file = "dulwich-0.21.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeb736d777ee21f2117a90fc453ee181aa7eedb9e255b5ef07c51733f3fe5cb6"}, - {file = "dulwich-0.21.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e8a79c1ed7166f32ad21974fa98d11bf6fd74e94a47e754c777c320e01257c6"}, - {file = "dulwich-0.21.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:b943517e30bd651fbc275a892bb96774f3893d95fe5a4dedd84496a98eaaa8ab"}, - {file = "dulwich-0.21.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:32493a456358a3a6c15bbda07106fc3d4cc50834ee18bc7717968d18be59b223"}, - {file = "dulwich-0.21.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa44b812d978fc22a04531f5090c3c369d5facd03fa6e0501d460a661800c7f"}, - {file = "dulwich-0.21.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f46bcb6777e5f9f4af24a2bd029e88b77316269d24ce66be590e546a0d8f7b7"}, - {file = "dulwich-0.21.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a917fd3b4493db3716da2260f16f6b18f68d46fbe491d851d154fc0c2d984ae4"}, - {file = "dulwich-0.21.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:684c52cff867d10c75a7238151ca307582b3d251bbcd6db9e9cffbc998ef804e"}, - {file = "dulwich-0.21.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9019189d7a8f7394df6a22cd5b484238c5776e42282ad5d6d6c626b4c5f43597"}, - {file = "dulwich-0.21.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:494024f74c2eef9988adb4352b3651ac1b6c0466176ec62b69d3d3672167ba68"}, - {file = "dulwich-0.21.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f9b6ac1b1c67fc6083c42b7b6cd3b211292c8a6517216c733caf23e8b103ab6d"}, - {file = "dulwich-0.21.5.tar.gz", hash = "sha256:70955e4e249ddda6e34a4636b90f74e931e558f993b17c52570fa6144b993103"}, + {file = "dulwich-0.21.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d4c0110798099bb7d36a110090f2688050703065448895c4f53ade808d889dd3"}, + {file = "dulwich-0.21.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2bc12697f0918bee324c18836053644035362bb3983dc1b210318f2fed1d7132"}, + {file = "dulwich-0.21.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:471305af74790827fcbafe330fc2e8bdcee4fb56ca1177c8c481b1c8f806c4a4"}, + {file = "dulwich-0.21.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d54c9d0e845be26f65f954dff13a1cd3f2b9739820c19064257b8fd7435ab263"}, + {file = "dulwich-0.21.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12d61334a575474e707614f2e93d6ed4cdae9eb47214f9277076d9e5615171d3"}, + {file = "dulwich-0.21.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e274cebaf345f0b1e3b70197f2651de92b652386b68020cfd3bf61bc30f6eaaa"}, + {file = "dulwich-0.21.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:817822f970e196e757ae01281ecbf21369383285b9f4a83496312204cf889b8c"}, + {file = "dulwich-0.21.7-cp310-cp310-win32.whl", hash = "sha256:7836da3f4110ce684dcd53489015fb7fa94ed33c5276e3318b8b1cbcb5b71e08"}, + {file = "dulwich-0.21.7-cp310-cp310-win_amd64.whl", hash = "sha256:4a043b90958cec866b4edc6aef5fe3c2c96a664d0b357e1682a46f6c477273c4"}, + {file = "dulwich-0.21.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ce8db196e79c1f381469410d26fb1d8b89c6b87a4e7f00ff418c22a35121405c"}, + {file = "dulwich-0.21.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:62bfb26bdce869cd40be443dfd93143caea7089b165d2dcc33de40f6ac9d812a"}, + {file = "dulwich-0.21.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c01a735b9a171dcb634a97a3cec1b174cfbfa8e840156870384b633da0460f18"}, + {file = "dulwich-0.21.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa4d14767cf7a49c9231c2e52cb2a3e90d0c83f843eb6a2ca2b5d81d254cf6b9"}, + {file = "dulwich-0.21.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bca4b86e96d6ef18c5bc39828ea349efb5be2f9b1f6ac9863f90589bac1084d"}, + {file = "dulwich-0.21.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7b5624b02ef808cdc62dabd47eb10cd4ac15e8ac6df9e2e88b6ac6b40133673"}, + {file = "dulwich-0.21.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c3a539b4696a42fbdb7412cb7b66a4d4d332761299d3613d90a642923c7560e1"}, + {file = "dulwich-0.21.7-cp311-cp311-win32.whl", hash = "sha256:675a612ce913081beb0f37b286891e795d905691dfccfb9bf73721dca6757cde"}, + {file = "dulwich-0.21.7-cp311-cp311-win_amd64.whl", hash = "sha256:460ba74bdb19f8d498786ae7776745875059b1178066208c0fd509792d7f7bfc"}, + {file = "dulwich-0.21.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4c51058ec4c0b45dc5189225b9e0c671b96ca9713c1daf71d622c13b0ab07681"}, + {file = "dulwich-0.21.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4bc4c5366eaf26dda3fdffe160a3b515666ed27c2419f1d483da285ac1411de0"}, + {file = "dulwich-0.21.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a0650ec77d89cb947e3e4bbd4841c96f74e52b4650830112c3057a8ca891dc2f"}, + {file = "dulwich-0.21.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f18f0a311fb7734b033a3101292b932158cade54b74d1c44db519e42825e5a2"}, + {file = "dulwich-0.21.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c589468e5c0cd84e97eb7ec209ab005a2cb69399e8c5861c3edfe38989ac3a8"}, + {file = "dulwich-0.21.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d62446797163317a397a10080c6397ffaaca51a7804c0120b334f8165736c56a"}, + {file = "dulwich-0.21.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e84cc606b1f581733df4350ca4070e6a8b30be3662bbb81a590b177d0c996c91"}, + {file = "dulwich-0.21.7-cp312-cp312-win32.whl", hash = "sha256:c3d1685f320907a52c40fd5890627945c51f3a5fa4bcfe10edb24fec79caadec"}, + {file = "dulwich-0.21.7-cp312-cp312-win_amd64.whl", hash = "sha256:6bd69921fdd813b7469a3c77bc75c1783cc1d8d72ab15a406598e5a3ba1a1503"}, + {file = "dulwich-0.21.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7d8ab29c660125db52106775caa1f8f7f77a69ed1fe8bc4b42bdf115731a25bf"}, + {file = "dulwich-0.21.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0d2e4485b98695bf95350ce9d38b1bb0aaac2c34ad00a0df789aa33c934469b"}, + {file = "dulwich-0.21.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e138d516baa6b5bafbe8f030eccc544d0d486d6819b82387fc0e285e62ef5261"}, + {file = "dulwich-0.21.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f34bf9b9fa9308376263fd9ac43143c7c09da9bc75037bb75c6c2423a151b92c"}, + {file = "dulwich-0.21.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2e2c66888207b71cd1daa2acb06d3984a6bc13787b837397a64117aa9fc5936a"}, + {file = "dulwich-0.21.7-cp37-cp37m-win32.whl", hash = "sha256:10893105c6566fc95bc2a67b61df7cc1e8f9126d02a1df6a8b2b82eb59db8ab9"}, + {file = "dulwich-0.21.7-cp37-cp37m-win_amd64.whl", hash = "sha256:460b3849d5c3d3818a80743b4f7a0094c893c559f678e56a02fff570b49a644a"}, + {file = "dulwich-0.21.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:74700e4c7d532877355743336c36f51b414d01e92ba7d304c4f8d9a5946dbc81"}, + {file = "dulwich-0.21.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c92e72c43c9e9e936b01a57167e0ea77d3fd2d82416edf9489faa87278a1cdf7"}, + {file = "dulwich-0.21.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d097e963eb6b9fa53266146471531ad9c6765bf390849230311514546ed64db2"}, + {file = "dulwich-0.21.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:808e8b9cc0aa9ac74870b49db4f9f39a52fb61694573f84b9c0613c928d4caf8"}, + {file = "dulwich-0.21.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1957b65f96e36c301e419d7adaadcff47647c30eb072468901bb683b1000bc5"}, + {file = "dulwich-0.21.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4b09bc3a64fb70132ec14326ecbe6e0555381108caff3496898962c4136a48c6"}, + {file = "dulwich-0.21.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5882e70b74ac3c736a42d3fdd4f5f2e6570637f59ad5d3e684760290b58f041"}, + {file = "dulwich-0.21.7-cp38-cp38-win32.whl", hash = "sha256:29bb5c1d70eba155ded41ed8a62be2f72edbb3c77b08f65b89c03976292f6d1b"}, + {file = "dulwich-0.21.7-cp38-cp38-win_amd64.whl", hash = "sha256:25c3ab8fb2e201ad2031ddd32e4c68b7c03cb34b24a5ff477b7a7dcef86372f5"}, + {file = "dulwich-0.21.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8929c37986c83deb4eb500c766ee28b6670285b512402647ee02a857320e377c"}, + {file = "dulwich-0.21.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc1e11be527ac06316539b57a7688bcb1b6a3e53933bc2f844397bc50734e9ae"}, + {file = "dulwich-0.21.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0fc3078a1ba04c588fabb0969d3530efd5cd1ce2cf248eefb6baf7cbc15fc285"}, + {file = "dulwich-0.21.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dcbd29ba30ba2c5bfbab07a61a5f20095541d5ac66d813056c122244df4ac0"}, + {file = "dulwich-0.21.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8869fc8ec3dda743e03d06d698ad489b3705775fe62825e00fa95aa158097fc0"}, + {file = "dulwich-0.21.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d96ca5e0dde49376fbcb44f10eddb6c30284a87bd03bb577c59bb0a1f63903fa"}, + {file = "dulwich-0.21.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0064363bd5e814359657ae32517fa8001e8573d9d040bd997908d488ab886ed"}, + {file = "dulwich-0.21.7-cp39-cp39-win32.whl", hash = "sha256:869eb7be48243e695673b07905d18b73d1054a85e1f6e298fe63ba2843bb2ca1"}, + {file = "dulwich-0.21.7-cp39-cp39-win_amd64.whl", hash = "sha256:404b8edeb3c3a86c47c0a498699fc064c93fa1f8bab2ffe919e8ab03eafaaad3"}, + {file = "dulwich-0.21.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e598d743c6c0548ebcd2baf94aa9c8bfacb787ea671eeeb5828cfbd7d56b552f"}, + {file = "dulwich-0.21.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a2d76c96426e791556836ef43542b639def81be4f1d6d4322cd886c115eae1"}, + {file = "dulwich-0.21.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6c88acb60a1f4d31bd6d13bfba465853b3df940ee4a0f2a3d6c7a0778c705b7"}, + {file = "dulwich-0.21.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ecd315847dea406a4decfa39d388a2521e4e31acde3bd9c2609c989e817c6d62"}, + {file = "dulwich-0.21.7-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d05d3c781bc74e2c2a2a8f4e4e2ed693540fbe88e6ac36df81deac574a6dad99"}, + {file = "dulwich-0.21.7-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6de6f8de4a453fdbae8062a6faa652255d22a3d8bce0cd6d2d6701305c75f2b3"}, + {file = "dulwich-0.21.7-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e25953c7acbbe4e19650d0225af1c0c0e6882f8bddd2056f75c1cc2b109b88ad"}, + {file = "dulwich-0.21.7-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4637cbd8ed1012f67e1068aaed19fcc8b649bcf3e9e26649826a303298c89b9d"}, + {file = "dulwich-0.21.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:858842b30ad6486aacaa607d60bab9c9a29e7c59dc2d9cb77ae5a94053878c08"}, + {file = "dulwich-0.21.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739b191f61e1c4ce18ac7d520e7a7cbda00e182c3489552408237200ce8411ad"}, + {file = "dulwich-0.21.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:274c18ec3599a92a9b67abaf110e4f181a4f779ee1aaab9e23a72e89d71b2bd9"}, + {file = "dulwich-0.21.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2590e9b431efa94fc356ae33b38f5e64f1834ec3a94a6ac3a64283b206d07aa3"}, + {file = "dulwich-0.21.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed60d1f610ef6437586f7768254c2a93820ccbd4cfdac7d182cf2d6e615969bb"}, + {file = "dulwich-0.21.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8278835e168dd097089f9e53088c7a69c6ca0841aef580d9603eafe9aea8c358"}, + {file = "dulwich-0.21.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffc27fb063f740712e02b4d2f826aee8bbed737ed799962fef625e2ce56e2d29"}, + {file = "dulwich-0.21.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:61e3451bd3d3844f2dca53f131982553be4d1b1e1ebd9db701843dd76c4dba31"}, + {file = "dulwich-0.21.7.tar.gz", hash = "sha256:a9e9c66833cea580c3ac12927e4b9711985d76afca98da971405d414de60e968"}, ] [package.dependencies] @@ -836,13 +867,13 @@ pgp = ["gpg"] [[package]] name = "exceptiongroup" -version = "1.1.2" +version = "1.2.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, - {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] @@ -863,30 +894,45 @@ files = [ pyreadline = {version = "*", markers = "platform_system == \"Windows\""} pyrepl = ">=0.8.2" +[[package]] +name = "fastjsonschema" +version = "2.19.1" +description = "Fastest Python implementation of JSON schema" +optional = false +python-versions = "*" +files = [ + {file = "fastjsonschema-2.19.1-py3-none-any.whl", hash = "sha256:3672b47bc94178c9f23dbb654bf47440155d4db9df5f7bc47643315f9c405cd0"}, + {file = "fastjsonschema-2.19.1.tar.gz", hash = "sha256:e3126a94bdc4623d3de4485f8d468a12f02a67921315ddc87836d6e456dc789d"}, +] + +[package.extras] +devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] + [[package]] name = "filelock" -version = "3.12.2" +version = "3.13.1" description = "A platform independent file lock." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, - {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, ] [package.extras] -docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] [[package]] name = "fsspec" -version = "2023.6.0" +version = "2023.12.2" description = "File-system specification" optional = false python-versions = ">=3.8" files = [ - {file = "fsspec-2023.6.0-py3-none-any.whl", hash = "sha256:1cbad1faef3e391fba6dc005ae9b5bdcbf43005c9167ce78c915549c352c869a"}, - {file = "fsspec-2023.6.0.tar.gz", hash = "sha256:d0b2f935446169753e7a5c5c55681c54ea91996cc67be93c39a154fb3a2742af"}, + {file = "fsspec-2023.12.2-py3-none-any.whl", hash = "sha256:d800d87f72189a745fa3d6b033b9dc4a34ad069f60ca60b943a63599f5501960"}, + {file = "fsspec-2023.12.2.tar.gz", hash = "sha256:8548d39e8810b59c38014934f6b31e57f40c1b20f911f4cc2b85389c7e9bf0cb"}, ] [package.extras] @@ -1018,26 +1064,18 @@ beautifulsoup4 = "*" [[package]] name = "google-api-core" -version = "2.11.1" +version = "2.15.0" description = "Google API client core library" optional = false python-versions = ">=3.7" files = [ - {file = "google-api-core-2.11.1.tar.gz", hash = "sha256:25d29e05a0058ed5f19c61c0a78b1b53adea4d9364b464d014fbda941f6d1c9a"}, - {file = "google_api_core-2.11.1-py3-none-any.whl", hash = "sha256:d92a5a92dc36dd4f4b9ee4e55528a90e432b059f93aee6ad857f9de8cc7ae94a"}, + {file = "google-api-core-2.15.0.tar.gz", hash = "sha256:abc978a72658f14a2df1e5e12532effe40f94f868f6e23d95133bd6abcca35ca"}, + {file = "google_api_core-2.15.0-py3-none-any.whl", hash = "sha256:2aa56d2be495551e66bbff7f729b790546f87d5c90e74781aa77233bcb395a8a"}, ] [package.dependencies] google-auth = ">=2.14.1,<3.0.dev0" googleapis-common-protos = ">=1.56.2,<2.0.dev0" -grpcio = [ - {version = ">=1.33.2,<2.0dev", optional = true, markers = "extra == \"grpc\""}, - {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, -] -grpcio-status = [ - {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "extra == \"grpc\""}, - {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, -] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" requests = ">=2.18.0,<3.0.0.dev0" @@ -1048,13 +1086,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-api-python-client" -version = "2.95.0" +version = "2.113.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" files = [ - {file = "google-api-python-client-2.95.0.tar.gz", hash = "sha256:d2731ede12f79e53fbe11fdb913dfe986440b44c0a28431c78a8ec275f4c1541"}, - {file = "google_api_python_client-2.95.0-py2.py3-none-any.whl", hash = "sha256:a8aab2da678f42a01f2f52108f787fef4310f23f9dd917c4e64664c3f0c885ba"}, + {file = "google-api-python-client-2.113.0.tar.gz", hash = "sha256:bcffbc8ffbad631f699cf85aa91993f3dc03060b234ca9e6e2f9135028bd9b52"}, + {file = "google_api_python_client-2.113.0-py2.py3-none-any.whl", hash = "sha256:25659d488df6c8a69615b2a510af0e63b4c47ab2cb87d71c1e13b28715906e27"}, ] [package.dependencies] @@ -1066,21 +1104,19 @@ uritemplate = ">=3.0.1,<5" [[package]] name = "google-auth" -version = "2.22.0" +version = "2.26.1" description = "Google Authentication Library" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "google-auth-2.22.0.tar.gz", hash = "sha256:164cba9af4e6e4e40c3a4f90a1a6c12ee56f14c0b4868d1ca91b32826ab334ce"}, - {file = "google_auth-2.22.0-py2.py3-none-any.whl", hash = "sha256:d61d1b40897407b574da67da1a833bdc10d5a11642566e506565d1b1a46ba873"}, + {file = "google-auth-2.26.1.tar.gz", hash = "sha256:54385acca5c0fbdda510cd8585ba6f3fcb06eeecf8a6ecca39d3ee148b092590"}, + {file = "google_auth-2.26.1-py2.py3-none-any.whl", hash = "sha256:2c8b55e3e564f298122a02ab7b97458ccfcc5617840beb5d0ac757ada92c9780"}, ] [package.dependencies] cachetools = ">=2.0.0,<6.0" pyasn1-modules = ">=0.2.1" rsa = ">=3.1.4,<5" -six = ">=1.9.0" -urllib3 = "<2.0" [package.extras] aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] @@ -1091,64 +1127,58 @@ requests = ["requests (>=2.20.0,<3.0.0.dev0)"] [[package]] name = "google-auth-httplib2" -version = "0.1.0" +version = "0.2.0" description = "Google Authentication Library: httplib2 transport" optional = false python-versions = "*" files = [ - {file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"}, - {file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"}, + {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"}, + {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"}, ] [package.dependencies] google-auth = "*" -httplib2 = ">=0.15.0" -six = "*" +httplib2 = ">=0.19.0" [[package]] name = "google-cloud-bigquery" -version = "3.11.4" +version = "3.14.1" description = "Google BigQuery API client library" optional = false python-versions = ">=3.7" files = [ - {file = "google-cloud-bigquery-3.11.4.tar.gz", hash = "sha256:697df117241a2283bcbb93b21e10badc14e51c9a90800d2a7e1a3e1c7d842974"}, - {file = "google_cloud_bigquery-3.11.4-py2.py3-none-any.whl", hash = "sha256:5fa7897743a0ed949ade25a0942fc9e7557d8fce307c6f8a76d1b604cf27f1b1"}, + {file = "google-cloud-bigquery-3.14.1.tar.gz", hash = "sha256:aa15bd86f79ea76824c7d710f5ae532323c4b3ba01ef4abff42d4ee7a2e9b142"}, + {file = "google_cloud_bigquery-3.14.1-py2.py3-none-any.whl", hash = "sha256:a8ded18455da71508db222b7c06197bc12b6dbc6ed5b0b64e7007b76d7016957"}, ] [package.dependencies] -google-api-core = {version = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0dev", extras = ["grpc"]} +google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0dev" google-cloud-core = ">=1.6.0,<3.0.0dev" google-resumable-media = ">=0.6.0,<3.0dev" -grpcio = [ - {version = ">=1.47.0,<2.0dev", markers = "python_version < \"3.11\""}, - {version = ">=1.49.1,<2.0dev", markers = "python_version >= \"3.11\""}, -] packaging = ">=20.0.0" -proto-plus = ">=1.15.0,<2.0.0dev" -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" python-dateutil = ">=2.7.2,<3.0dev" requests = ">=2.21.0,<3.0.0dev" [package.extras] -all = ["Shapely (>=1.8.4,<2.0dev)", "db-dtypes (>=0.3.0,<2.0.0dev)", "geopandas (>=0.9.0,<1.0dev)", "google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "ipykernel (>=6.0.0)", "ipython (>=7.23.1,!=8.1.0)", "ipywidgets (>=7.7.0)", "opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)", "pandas (>=1.1.0)", "pyarrow (>=3.0.0)", "tqdm (>=4.7.4,<5.0.0dev)"] +all = ["Shapely (>=1.8.4,<3.0.0dev)", "db-dtypes (>=0.3.0,<2.0.0dev)", "geopandas (>=0.9.0,<1.0dev)", "google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "importlib-metadata (>=1.0.0)", "ipykernel (>=6.0.0)", "ipython (>=7.23.1,!=8.1.0)", "ipywidgets (>=7.7.0)", "opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)", "pandas (>=1.1.0)", "proto-plus (>=1.15.0,<2.0.0dev)", "protobuf (>=3.19.5,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<5.0.0dev)", "pyarrow (>=3.0.0)", "tqdm (>=4.7.4,<5.0.0dev)"] +bigquery-v2 = ["proto-plus (>=1.15.0,<2.0.0dev)", "protobuf (>=3.19.5,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<5.0.0dev)"] bqstorage = ["google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "pyarrow (>=3.0.0)"] -geopandas = ["Shapely (>=1.8.4,<2.0dev)", "geopandas (>=0.9.0,<1.0dev)"] +geopandas = ["Shapely (>=1.8.4,<3.0.0dev)", "geopandas (>=0.9.0,<1.0dev)"] ipython = ["ipykernel (>=6.0.0)", "ipython (>=7.23.1,!=8.1.0)"] ipywidgets = ["ipykernel (>=6.0.0)", "ipywidgets (>=7.7.0)"] opentelemetry = ["opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)"] -pandas = ["db-dtypes (>=0.3.0,<2.0.0dev)", "pandas (>=1.1.0)", "pyarrow (>=3.0.0)"] +pandas = ["db-dtypes (>=0.3.0,<2.0.0dev)", "importlib-metadata (>=1.0.0)", "pandas (>=1.1.0)", "pyarrow (>=3.0.0)"] tqdm = ["tqdm (>=4.7.4,<5.0.0dev)"] [[package]] name = "google-cloud-core" -version = "2.3.3" +version = "2.4.1" description = "Google Cloud API client core library" optional = false python-versions = ">=3.7" files = [ - {file = "google-cloud-core-2.3.3.tar.gz", hash = "sha256:37b80273c8d7eee1ae816b3a20ae43585ea50506cb0e60f3cf5be5f87f1373cb"}, - {file = "google_cloud_core-2.3.3-py2.py3-none-any.whl", hash = "sha256:fbd11cad3e98a7e5b0343dc07cb1039a5ffd7a5bb96e1f1e27cee4bda4a90863"}, + {file = "google-cloud-core-2.4.1.tar.gz", hash = "sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073"}, + {file = "google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61"}, ] [package.dependencies] @@ -1156,24 +1186,25 @@ google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0dev" google-auth = ">=1.25.0,<3.0dev" [package.extras] -grpc = ["grpcio (>=1.38.0,<2.0dev)"] +grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] [[package]] name = "google-cloud-storage" -version = "2.10.0" +version = "2.14.0" description = "Google Cloud Storage API client library" optional = false python-versions = ">=3.7" files = [ - {file = "google-cloud-storage-2.10.0.tar.gz", hash = "sha256:934b31ead5f3994e5360f9ff5750982c5b6b11604dc072bc452c25965e076dc7"}, - {file = "google_cloud_storage-2.10.0-py2.py3-none-any.whl", hash = "sha256:9433cf28801671de1c80434238fb1e7e4a1ba3087470e90f70c928ea77c2b9d7"}, + {file = "google-cloud-storage-2.14.0.tar.gz", hash = "sha256:2d23fcf59b55e7b45336729c148bb1c464468c69d5efbaee30f7201dd90eb97e"}, + {file = "google_cloud_storage-2.14.0-py2.py3-none-any.whl", hash = "sha256:8641243bbf2a2042c16a6399551fbb13f062cbc9a2de38d6c0bb5426962e9dbd"}, ] [package.dependencies] google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0dev" -google-auth = ">=1.25.0,<3.0dev" +google-auth = ">=2.23.3,<3.0dev" google-cloud-core = ">=2.3.0,<3.0dev" -google-resumable-media = ">=2.3.2" +google-crc32c = ">=1.0,<2.0dev" +google-resumable-media = ">=2.6.0" requests = ">=2.18.0,<3.0.0dev" [package.extras] @@ -1278,31 +1309,31 @@ protobuf = ">=3.0.0b3" [[package]] name = "google-resumable-media" -version = "2.5.0" +version = "2.7.0" description = "Utilities for Google Media Downloads and Resumable Uploads" optional = false python-versions = ">= 3.7" files = [ - {file = "google-resumable-media-2.5.0.tar.gz", hash = "sha256:218931e8e2b2a73a58eb354a288e03a0fd5fb1c4583261ac6e4c078666468c93"}, - {file = "google_resumable_media-2.5.0-py2.py3-none-any.whl", hash = "sha256:da1bd943e2e114a56d85d6848497ebf9be6a14d3db23e9fc57581e7c3e8170ec"}, + {file = "google-resumable-media-2.7.0.tar.gz", hash = "sha256:5f18f5fa9836f4b083162064a1c2c98c17239bfda9ca50ad970ccf905f3e625b"}, + {file = "google_resumable_media-2.7.0-py2.py3-none-any.whl", hash = "sha256:79543cfe433b63fd81c0844b7803aba1bb8950b47bedf7d980c38fa123937e08"}, ] [package.dependencies] google-crc32c = ">=1.0,<2.0dev" [package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)"] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "google-auth (>=1.22.0,<2.0dev)"] requests = ["requests (>=2.18.0,<3.0.0dev)"] [[package]] name = "googleapis-common-protos" -version = "1.60.0" +version = "1.62.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" files = [ - {file = "googleapis-common-protos-1.60.0.tar.gz", hash = "sha256:e73ebb404098db405ba95d1e1ae0aa91c3e15a71da031a2eeb6b2e23e7bc3708"}, - {file = "googleapis_common_protos-1.60.0-py2.py3-none-any.whl", hash = "sha256:69f9bbcc6acde92cab2db95ce30a70bd2b81d20b12eff3f1aabaffcbe8a93918"}, + {file = "googleapis-common-protos-1.62.0.tar.gz", hash = "sha256:83f0ece9f94e5672cced82f592d2a5edf527a96ed1794f0bab36d5735c996277"}, + {file = "googleapis_common_protos-1.62.0-py2.py3-none-any.whl", hash = "sha256:4750113612205514f9f6aa4cb00d523a94f3e8c06c5ad2fee466387dc4875f07"}, ] [package.dependencies] @@ -1313,32 +1344,33 @@ grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] [[package]] name = "gql" -version = "3.4.1" +version = "3.5.0" description = "GraphQL client for Python" optional = false python-versions = "*" files = [ - {file = "gql-3.4.1-py2.py3-none-any.whl", hash = "sha256:315624ca0f4d571ef149d455033ebd35e45c1a13f18a059596aeddcea99135cf"}, - {file = "gql-3.4.1.tar.gz", hash = "sha256:11dc5d8715a827f2c2899593439a4f36449db4f0eafa5b1ea63948f8a2f8c545"}, + {file = "gql-3.5.0-py2.py3-none-any.whl", hash = "sha256:70dda5694a5b194a8441f077aa5fb70cc94e4ec08016117523f013680901ecb7"}, + {file = "gql-3.5.0.tar.gz", hash = "sha256:ccb9c5db543682b28f577069950488218ed65d4ac70bb03b6929aaadaf636de9"}, ] [package.dependencies] +anyio = ">=3.0,<5" backoff = ">=1.11.1,<3.0" graphql-core = ">=3.2,<3.3" requests = {version = ">=2.26,<3", optional = true, markers = "extra == \"requests\""} -requests-toolbelt = {version = ">=0.9.1,<1", optional = true, markers = "extra == \"requests\""} -urllib3 = {version = ">=1.26,<2", optional = true, markers = "extra == \"requests\""} +requests-toolbelt = {version = ">=1.0.0,<2", optional = true, markers = "extra == \"requests\""} yarl = ">=1.6,<2.0" [package.extras] -aiohttp = ["aiohttp (>=3.7.1,<3.9.0)"] -all = ["aiohttp (>=3.7.1,<3.9.0)", "botocore (>=1.21,<2)", "requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26,<2)", "websockets (>=10,<11)", "websockets (>=9,<10)"] +aiohttp = ["aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)"] +all = ["aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "botocore (>=1.21,<2)", "httpx (>=0.23.1,<1)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "websockets (>=10,<12)"] botocore = ["botocore (>=1.21,<2)"] -dev = ["aiofiles", "aiohttp (>=3.7.1,<3.9.0)", "black (==22.3.0)", "botocore (>=1.21,<2)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "isort (==4.3.21)", "mock (==4.0.2)", "mypy (==0.910)", "parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "sphinx (>=3.0.0,<4)", "sphinx-argparse (==0.2.5)", "sphinx-rtd-theme (>=0.4,<1)", "types-aiofiles", "types-mock", "types-requests", "urllib3 (>=1.26,<2)", "vcrpy (==4.0.2)", "websockets (>=10,<11)", "websockets (>=9,<10)"] -requests = ["requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26,<2)"] -test = ["aiofiles", "aiohttp (>=3.7.1,<3.9.0)", "botocore (>=1.21,<2)", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26,<2)", "vcrpy (==4.0.2)", "websockets (>=10,<11)", "websockets (>=9,<10)"] -test-no-transport = ["aiofiles", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "vcrpy (==4.0.2)"] -websockets = ["websockets (>=10,<11)", "websockets (>=9,<10)"] +dev = ["aiofiles", "aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "black (==22.3.0)", "botocore (>=1.21,<2)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "httpx (>=0.23.1,<1)", "isort (==4.3.21)", "mock (==4.0.2)", "mypy (==0.910)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "sphinx (>=5.3.0,<6)", "sphinx-argparse (==0.2.5)", "sphinx-rtd-theme (>=0.4,<1)", "types-aiofiles", "types-mock", "types-requests", "vcrpy (==4.4.0)", "websockets (>=10,<12)"] +httpx = ["httpx (>=0.23.1,<1)"] +requests = ["requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)"] +test = ["aiofiles", "aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "botocore (>=1.21,<2)", "httpx (>=0.23.1,<1)", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "vcrpy (==4.4.0)", "websockets (>=10,<12)"] +test-no-transport = ["aiofiles", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "vcrpy (==4.4.0)"] +websockets = ["websockets (>=10,<12)"] [[package]] name = "graphene" @@ -1387,75 +1419,73 @@ graphql-core = ">=3.2,<3.3" [[package]] name = "greenlet" -version = "2.0.2" +version = "3.0.3" description = "Lightweight in-process concurrent programming" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -files = [ - {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, - {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, - {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, - {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, - {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, - {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, - {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, - {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, - {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, - {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, - {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, - {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, - {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, - {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, - {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, - {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, - {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, - {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, - {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, - {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, - {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, - {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, - {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, - {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, - {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, - {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, ] [package.extras] -docs = ["Sphinx", "docutils (<0.18)"] +docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] [[package]] @@ -1490,90 +1520,83 @@ oauth2client = ">=1.4.11" [[package]] name = "grpcio" -version = "1.56.2" +version = "1.60.0" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.7" files = [ - {file = "grpcio-1.56.2-cp310-cp310-linux_armv7l.whl", hash = "sha256:bf0b9959e673505ee5869950642428046edb91f99942607c2ecf635f8a4b31c9"}, - {file = "grpcio-1.56.2-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:5144feb20fe76e73e60c7d73ec3bf54f320247d1ebe737d10672480371878b48"}, - {file = "grpcio-1.56.2-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:a72797549935c9e0b9bc1def1768c8b5a709538fa6ab0678e671aec47ebfd55e"}, - {file = "grpcio-1.56.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3f3237a57e42f79f1e560726576aedb3a7ef931f4e3accb84ebf6acc485d316"}, - {file = "grpcio-1.56.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:900bc0096c2ca2d53f2e5cebf98293a7c32f532c4aeb926345e9747452233950"}, - {file = "grpcio-1.56.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:97e0efaebbfd222bcaac2f1735c010c1d3b167112d9d237daebbeedaaccf3d1d"}, - {file = "grpcio-1.56.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c0c85c5cbe8b30a32fa6d802588d55ffabf720e985abe9590c7c886919d875d4"}, - {file = "grpcio-1.56.2-cp310-cp310-win32.whl", hash = "sha256:06e84ad9ae7668a109e970c7411e7992751a116494cba7c4fb877656527f9a57"}, - {file = "grpcio-1.56.2-cp310-cp310-win_amd64.whl", hash = "sha256:10954662f77dc36c9a1fb5cc4a537f746580d6b5734803be1e587252682cda8d"}, - {file = "grpcio-1.56.2-cp311-cp311-linux_armv7l.whl", hash = "sha256:c435f5ce1705de48e08fcbcfaf8aee660d199c90536e3e06f2016af7d6a938dd"}, - {file = "grpcio-1.56.2-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:6108e5933eb8c22cd3646e72d5b54772c29f57482fd4c41a0640aab99eb5071d"}, - {file = "grpcio-1.56.2-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:8391cea5ce72f4a12368afd17799474015d5d3dc00c936a907eb7c7eaaea98a5"}, - {file = "grpcio-1.56.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:750de923b456ca8c0f1354d6befca45d1f3b3a789e76efc16741bd4132752d95"}, - {file = "grpcio-1.56.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fda2783c12f553cdca11c08e5af6eecbd717280dc8fbe28a110897af1c15a88c"}, - {file = "grpcio-1.56.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9e04d4e4cfafa7c5264e535b5d28e786f0571bea609c3f0aaab13e891e933e9c"}, - {file = "grpcio-1.56.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:89a49cc5ad08a38b6141af17e00d1dd482dc927c7605bc77af457b5a0fca807c"}, - {file = "grpcio-1.56.2-cp311-cp311-win32.whl", hash = "sha256:6a007a541dff984264981fbafeb052bfe361db63578948d857907df9488d8774"}, - {file = "grpcio-1.56.2-cp311-cp311-win_amd64.whl", hash = "sha256:af4063ef2b11b96d949dccbc5a987272f38d55c23c4c01841ea65a517906397f"}, - {file = "grpcio-1.56.2-cp37-cp37m-linux_armv7l.whl", hash = "sha256:a6ff459dac39541e6a2763a4439c4ca6bc9ecb4acc05a99b79246751f9894756"}, - {file = "grpcio-1.56.2-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:f20fd21f7538f8107451156dd1fe203300b79a9ddceba1ee0ac8132521a008ed"}, - {file = "grpcio-1.56.2-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:d1fbad1f9077372b6587ec589c1fc120b417b6c8ad72d3e3cc86bbbd0a3cee93"}, - {file = "grpcio-1.56.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee26e9dfb3996aff7c870f09dc7ad44a5f6732b8bdb5a5f9905737ac6fd4ef1"}, - {file = "grpcio-1.56.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c60abd950d6de3e4f1ddbc318075654d275c29c846ab6a043d6ed2c52e4c8c"}, - {file = "grpcio-1.56.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1c31e52a04e62c8577a7bf772b3e7bed4df9c9e0dd90f92b6ffa07c16cab63c9"}, - {file = "grpcio-1.56.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:345356b307cce5d14355e8e055b4ca5f99bc857c33a3dc1ddbc544fca9cd0475"}, - {file = "grpcio-1.56.2-cp37-cp37m-win_amd64.whl", hash = "sha256:42e63904ee37ae46aa23de50dac8b145b3596f43598fa33fe1098ab2cbda6ff5"}, - {file = "grpcio-1.56.2-cp38-cp38-linux_armv7l.whl", hash = "sha256:7c5ede2e2558f088c49a1ddda19080e4c23fb5d171de80a726b61b567e3766ed"}, - {file = "grpcio-1.56.2-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:33971197c47965cc1d97d78d842163c283e998223b151bab0499b951fd2c0b12"}, - {file = "grpcio-1.56.2-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:d39f5d4af48c138cb146763eda14eb7d8b3ccbbec9fe86fb724cd16e0e914c64"}, - {file = "grpcio-1.56.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ded637176addc1d3eef35331c39acc598bac550d213f0a1bedabfceaa2244c87"}, - {file = "grpcio-1.56.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c90da4b124647547a68cf2f197174ada30c7bb9523cb976665dfd26a9963d328"}, - {file = "grpcio-1.56.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3ccb621749a81dc7755243665a70ce45536ec413ef5818e013fe8dfbf5aa497b"}, - {file = "grpcio-1.56.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4eb37dd8dd1aa40d601212afa27ca5be255ba792e2e0b24d67b8af5e012cdb7d"}, - {file = "grpcio-1.56.2-cp38-cp38-win32.whl", hash = "sha256:ddb4a6061933bd9332b74eac0da25f17f32afa7145a33a0f9711ad74f924b1b8"}, - {file = "grpcio-1.56.2-cp38-cp38-win_amd64.whl", hash = "sha256:8940d6de7068af018dfa9a959a3510e9b7b543f4c405e88463a1cbaa3b2b379a"}, - {file = "grpcio-1.56.2-cp39-cp39-linux_armv7l.whl", hash = "sha256:51173e8fa6d9a2d85c14426bdee5f5c4a0654fd5fddcc21fe9d09ab0f6eb8b35"}, - {file = "grpcio-1.56.2-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:373b48f210f43327a41e397391715cd11cfce9ded2fe76a5068f9bacf91cc226"}, - {file = "grpcio-1.56.2-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:42a3bbb2bc07aef72a7d97e71aabecaf3e4eb616d39e5211e2cfe3689de860ca"}, - {file = "grpcio-1.56.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5344be476ac37eb9c9ad09c22f4ea193c1316bf074f1daf85bddb1b31fda5116"}, - {file = "grpcio-1.56.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3fa3ab0fb200a2c66493828ed06ccd1a94b12eddbfb985e7fd3e5723ff156c6"}, - {file = "grpcio-1.56.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b975b85d1d5efc36cf8b237c5f3849b64d1ba33d6282f5e991f28751317504a1"}, - {file = "grpcio-1.56.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cbdf2c498e077282cd427cfd88bdce4668019791deef0be8155385ab2ba7837f"}, - {file = "grpcio-1.56.2-cp39-cp39-win32.whl", hash = "sha256:139f66656a762572ae718fa0d1f2dce47c05e9fbf7a16acd704c354405b97df9"}, - {file = "grpcio-1.56.2-cp39-cp39-win_amd64.whl", hash = "sha256:830215173ad45d670140ff99aac3b461f9be9a6b11bee1a17265aaaa746a641a"}, - {file = "grpcio-1.56.2.tar.gz", hash = "sha256:0ff789ae7d8ddd76d2ac02e7d13bfef6fc4928ac01e1dcaa182be51b6bcc0aaa"}, + {file = "grpcio-1.60.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:d020cfa595d1f8f5c6b343530cd3ca16ae5aefdd1e832b777f9f0eb105f5b139"}, + {file = "grpcio-1.60.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:b98f43fcdb16172dec5f4b49f2fece4b16a99fd284d81c6bbac1b3b69fcbe0ff"}, + {file = "grpcio-1.60.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:20e7a4f7ded59097c84059d28230907cd97130fa74f4a8bfd1d8e5ba18c81491"}, + {file = "grpcio-1.60.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452ca5b4afed30e7274445dd9b441a35ece656ec1600b77fff8c216fdf07df43"}, + {file = "grpcio-1.60.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43e636dc2ce9ece583b3e2ca41df5c983f4302eabc6d5f9cd04f0562ee8ec1ae"}, + {file = "grpcio-1.60.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e306b97966369b889985a562ede9d99180def39ad42c8014628dd3cc343f508"}, + {file = "grpcio-1.60.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f897c3b127532e6befdcf961c415c97f320d45614daf84deba0a54e64ea2457b"}, + {file = "grpcio-1.60.0-cp310-cp310-win32.whl", hash = "sha256:b87efe4a380887425bb15f220079aa8336276398dc33fce38c64d278164f963d"}, + {file = "grpcio-1.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:a9c7b71211f066908e518a2ef7a5e211670761651039f0d6a80d8d40054047df"}, + {file = "grpcio-1.60.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:fb464479934778d7cc5baf463d959d361954d6533ad34c3a4f1d267e86ee25fd"}, + {file = "grpcio-1.60.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:4b44d7e39964e808b071714666a812049765b26b3ea48c4434a3b317bac82f14"}, + {file = "grpcio-1.60.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:90bdd76b3f04bdb21de5398b8a7c629676c81dfac290f5f19883857e9371d28c"}, + {file = "grpcio-1.60.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91229d7203f1ef0ab420c9b53fe2ca5c1fbeb34f69b3bc1b5089466237a4a134"}, + {file = "grpcio-1.60.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b36a2c6d4920ba88fa98075fdd58ff94ebeb8acc1215ae07d01a418af4c0253"}, + {file = "grpcio-1.60.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:297eef542156d6b15174a1231c2493ea9ea54af8d016b8ca7d5d9cc65cfcc444"}, + {file = "grpcio-1.60.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:87c9224acba0ad8bacddf427a1c2772e17ce50b3042a789547af27099c5f751d"}, + {file = "grpcio-1.60.0-cp311-cp311-win32.whl", hash = "sha256:95ae3e8e2c1b9bf671817f86f155c5da7d49a2289c5cf27a319458c3e025c320"}, + {file = "grpcio-1.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:467a7d31554892eed2aa6c2d47ded1079fc40ea0b9601d9f79204afa8902274b"}, + {file = "grpcio-1.60.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:a7152fa6e597c20cb97923407cf0934e14224af42c2b8d915f48bc3ad2d9ac18"}, + {file = "grpcio-1.60.0-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:7db16dd4ea1b05ada504f08d0dca1cd9b926bed3770f50e715d087c6f00ad748"}, + {file = "grpcio-1.60.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:b0571a5aef36ba9177e262dc88a9240c866d903a62799e44fd4aae3f9a2ec17e"}, + {file = "grpcio-1.60.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fd9584bf1bccdfff1512719316efa77be235469e1e3295dce64538c4773840b"}, + {file = "grpcio-1.60.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6a478581b1a1a8fdf3318ecb5f4d0cda41cacdffe2b527c23707c9c1b8fdb55"}, + {file = "grpcio-1.60.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:77c8a317f0fd5a0a2be8ed5cbe5341537d5c00bb79b3bb27ba7c5378ba77dbca"}, + {file = "grpcio-1.60.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1c30bb23a41df95109db130a6cc1b974844300ae2e5d68dd4947aacba5985aa5"}, + {file = "grpcio-1.60.0-cp312-cp312-win32.whl", hash = "sha256:2aef56e85901c2397bd557c5ba514f84de1f0ae5dd132f5d5fed042858115951"}, + {file = "grpcio-1.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:e381fe0c2aa6c03b056ad8f52f8efca7be29fb4d9ae2f8873520843b6039612a"}, + {file = "grpcio-1.60.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:92f88ca1b956eb8427a11bb8b4a0c0b2b03377235fc5102cb05e533b8693a415"}, + {file = "grpcio-1.60.0-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:e278eafb406f7e1b1b637c2cf51d3ad45883bb5bd1ca56bc05e4fc135dfdaa65"}, + {file = "grpcio-1.60.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:a48edde788b99214613e440fce495bbe2b1e142a7f214cce9e0832146c41e324"}, + {file = "grpcio-1.60.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de2ad69c9a094bf37c1102b5744c9aec6cf74d2b635558b779085d0263166454"}, + {file = "grpcio-1.60.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:073f959c6f570797272f4ee9464a9997eaf1e98c27cb680225b82b53390d61e6"}, + {file = "grpcio-1.60.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c826f93050c73e7769806f92e601e0efdb83ec8d7c76ddf45d514fee54e8e619"}, + {file = "grpcio-1.60.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9e30be89a75ee66aec7f9e60086fadb37ff8c0ba49a022887c28c134341f7179"}, + {file = "grpcio-1.60.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b0fb2d4801546598ac5cd18e3ec79c1a9af8b8f2a86283c55a5337c5aeca4b1b"}, + {file = "grpcio-1.60.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:9073513ec380434eb8d21970e1ab3161041de121f4018bbed3146839451a6d8e"}, + {file = "grpcio-1.60.0-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:74d7d9fa97809c5b892449b28a65ec2bfa458a4735ddad46074f9f7d9550ad13"}, + {file = "grpcio-1.60.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:1434ca77d6fed4ea312901122dc8da6c4389738bf5788f43efb19a838ac03ead"}, + {file = "grpcio-1.60.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e61e76020e0c332a98290323ecfec721c9544f5b739fab925b6e8cbe1944cf19"}, + {file = "grpcio-1.60.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675997222f2e2f22928fbba640824aebd43791116034f62006e19730715166c0"}, + {file = "grpcio-1.60.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5208a57eae445ae84a219dfd8b56e04313445d146873117b5fa75f3245bc1390"}, + {file = "grpcio-1.60.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:428d699c8553c27e98f4d29fdc0f0edc50e9a8a7590bfd294d2edb0da7be3629"}, + {file = "grpcio-1.60.0-cp38-cp38-win32.whl", hash = "sha256:83f2292ae292ed5a47cdcb9821039ca8e88902923198f2193f13959360c01860"}, + {file = "grpcio-1.60.0-cp38-cp38-win_amd64.whl", hash = "sha256:705a68a973c4c76db5d369ed573fec3367d7d196673fa86614b33d8c8e9ebb08"}, + {file = "grpcio-1.60.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:c193109ca4070cdcaa6eff00fdb5a56233dc7610216d58fb81638f89f02e4968"}, + {file = "grpcio-1.60.0-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:676e4a44e740deaba0f4d95ba1d8c5c89a2fcc43d02c39f69450b1fa19d39590"}, + {file = "grpcio-1.60.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:5ff21e000ff2f658430bde5288cb1ac440ff15c0d7d18b5fb222f941b46cb0d2"}, + {file = "grpcio-1.60.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c86343cf9ff7b2514dd229bdd88ebba760bd8973dac192ae687ff75e39ebfab"}, + {file = "grpcio-1.60.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fd3b3968ffe7643144580f260f04d39d869fcc2cddb745deef078b09fd2b328"}, + {file = "grpcio-1.60.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:30943b9530fe3620e3b195c03130396cd0ee3a0d10a66c1bee715d1819001eaf"}, + {file = "grpcio-1.60.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b10241250cb77657ab315270b064a6c7f1add58af94befa20687e7c8d8603ae6"}, + {file = "grpcio-1.60.0-cp39-cp39-win32.whl", hash = "sha256:79a050889eb8d57a93ed21d9585bb63fca881666fc709f5d9f7f9372f5e7fd03"}, + {file = "grpcio-1.60.0-cp39-cp39-win_amd64.whl", hash = "sha256:8a97a681e82bc11a42d4372fe57898d270a2707f36c45c6676e49ce0d5c41353"}, + {file = "grpcio-1.60.0.tar.gz", hash = "sha256:2199165a1affb666aa24adf0c97436686d0a61bc5fc113c037701fb7c7fceb96"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.56.2)"] +protobuf = ["grpcio-tools (>=1.60.0)"] [[package]] name = "grpcio-health-checking" -version = "1.56.2" +version = "1.60.0" description = "Standard Health Checking Service for gRPC" optional = false python-versions = ">=3.6" files = [ - {file = "grpcio-health-checking-1.56.2.tar.gz", hash = "sha256:5cda1d8a1368be2cda04f9284a8b73cee09ff3e277eec8ddd9abcf2fef76b372"}, - {file = "grpcio_health_checking-1.56.2-py3-none-any.whl", hash = "sha256:d0aedbcdbb365c08a5bd860384098502e35045e31fdd9d80e440bb58487e83d7"}, -] - -[package.dependencies] -grpcio = ">=1.56.2" -protobuf = ">=4.21.6" - -[[package]] -name = "grpcio-status" -version = "1.56.2" -description = "Status proto mapping for gRPC" -optional = false -python-versions = ">=3.6" -files = [ - {file = "grpcio-status-1.56.2.tar.gz", hash = "sha256:a046b2c0118df4a5687f4585cca9d3c3bae5c498c4dff055dcb43fb06a1180c8"}, - {file = "grpcio_status-1.56.2-py3-none-any.whl", hash = "sha256:63f3842867735f59f5d70e723abffd2e8501a6bcd915612a1119e52f10614782"}, + {file = "grpcio-health-checking-1.60.0.tar.gz", hash = "sha256:478b5300778120fed9f6d134d72b157a59f9c06689789218cbff47fafca2f119"}, + {file = "grpcio_health_checking-1.60.0-py3-none-any.whl", hash = "sha256:13caf28bc93795bd6bdb580b21832ebdd1aa3f5b648ea47ed17362d85bed96d3"}, ] [package.dependencies] -googleapis-common-protos = ">=1.5.5" -grpcio = ">=1.56.2" +grpcio = ">=1.60.0" protobuf = ">=4.21.6" [[package]] @@ -1587,27 +1610,6 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] -[[package]] -name = "html5lib" -version = "1.1" -description = "HTML parser based on the WHATWG HTML specification" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, - {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, -] - -[package.dependencies] -six = ">=1.9" -webencodings = "*" - -[package.extras] -all = ["chardet (>=2.2)", "genshi", "lxml"] -chardet = ["chardet (>=2.2)"] -genshi = ["genshi"] -lxml = ["lxml"] - [[package]] name = "httplib2" version = "0.22.0" @@ -1624,46 +1626,47 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0 [[package]] name = "httptools" -version = "0.6.0" +version = "0.6.1" description = "A collection of framework independent HTTP protocol utils." optional = false -python-versions = ">=3.5.0" -files = [ - {file = "httptools-0.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:818325afee467d483bfab1647a72054246d29f9053fd17cc4b86cda09cc60339"}, - {file = "httptools-0.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72205730bf1be875003692ca54a4a7c35fac77b4746008966061d9d41a61b0f5"}, - {file = "httptools-0.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33eb1d4e609c835966e969a31b1dedf5ba16b38cab356c2ce4f3e33ffa94cad3"}, - {file = "httptools-0.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdc6675ec6cb79d27e0575750ac6e2b47032742e24eed011b8db73f2da9ed40"}, - {file = "httptools-0.6.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:463c3bc5ef64b9cf091be9ac0e0556199503f6e80456b790a917774a616aff6e"}, - {file = "httptools-0.6.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82f228b88b0e8c6099a9c4757ce9fdbb8b45548074f8d0b1f0fc071e35655d1c"}, - {file = "httptools-0.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:0781fedc610293a2716bc7fa142d4c85e6776bc59d617a807ff91246a95dea35"}, - {file = "httptools-0.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:721e503245d591527cddd0f6fd771d156c509e831caa7a57929b55ac91ee2b51"}, - {file = "httptools-0.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:274bf20eeb41b0956e34f6a81f84d26ed57c84dd9253f13dcb7174b27ccd8aaf"}, - {file = "httptools-0.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:259920bbae18740a40236807915def554132ad70af5067e562f4660b62c59b90"}, - {file = "httptools-0.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03bfd2ae8a2d532952ac54445a2fb2504c804135ed28b53fefaf03d3a93eb1fd"}, - {file = "httptools-0.6.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f959e4770b3fc8ee4dbc3578fd910fab9003e093f20ac8c621452c4d62e517cb"}, - {file = "httptools-0.6.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6e22896b42b95b3237eccc42278cd72c0df6f23247d886b7ded3163452481e38"}, - {file = "httptools-0.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:38f3cafedd6aa20ae05f81f2e616ea6f92116c8a0f8dcb79dc798df3356836e2"}, - {file = "httptools-0.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:47043a6e0ea753f006a9d0dd076a8f8c99bc0ecae86a0888448eb3076c43d717"}, - {file = "httptools-0.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a541579bed0270d1ac10245a3e71e5beeb1903b5fbbc8d8b4d4e728d48ff1d"}, - {file = "httptools-0.6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65d802e7b2538a9756df5acc062300c160907b02e15ed15ba035b02bce43e89c"}, - {file = "httptools-0.6.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:26326e0a8fe56829f3af483200d914a7cd16d8d398d14e36888b56de30bec81a"}, - {file = "httptools-0.6.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e41ccac9e77cd045f3e4ee0fc62cbf3d54d7d4b375431eb855561f26ee7a9ec4"}, - {file = "httptools-0.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4e748fc0d5c4a629988ef50ac1aef99dfb5e8996583a73a717fc2cac4ab89932"}, - {file = "httptools-0.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cf8169e839a0d740f3d3c9c4fa630ac1a5aaf81641a34575ca6773ed7ce041a1"}, - {file = "httptools-0.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5dcc14c090ab57b35908d4a4585ec5c0715439df07be2913405991dbb37e049d"}, - {file = "httptools-0.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0b0571806a5168013b8c3d180d9f9d6997365a4212cb18ea20df18b938aa0b"}, - {file = "httptools-0.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb4a608c631f7dcbdf986f40af7a030521a10ba6bc3d36b28c1dc9e9035a3c0"}, - {file = "httptools-0.6.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:93f89975465133619aea8b1952bc6fa0e6bad22a447c6d982fc338fbb4c89649"}, - {file = "httptools-0.6.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:73e9d66a5a28b2d5d9fbd9e197a31edd02be310186db423b28e6052472dc8201"}, - {file = "httptools-0.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:22c01fcd53648162730a71c42842f73b50f989daae36534c818b3f5050b54589"}, - {file = "httptools-0.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f96d2a351b5625a9fd9133c95744e8ca06f7a4f8f0b8231e4bbaae2c485046a"}, - {file = "httptools-0.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72ec7c70bd9f95ef1083d14a755f321d181f046ca685b6358676737a5fecd26a"}, - {file = "httptools-0.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b703d15dbe082cc23266bf5d9448e764c7cb3fcfe7cb358d79d3fd8248673ef9"}, - {file = "httptools-0.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c723ed5982f8ead00f8e7605c53e55ffe47c47465d878305ebe0082b6a1755"}, - {file = "httptools-0.6.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b0a816bb425c116a160fbc6f34cece097fd22ece15059d68932af686520966bd"}, - {file = "httptools-0.6.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:dea66d94e5a3f68c5e9d86e0894653b87d952e624845e0b0e3ad1c733c6cc75d"}, - {file = "httptools-0.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:23b09537086a5a611fad5696fc8963d67c7e7f98cb329d38ee114d588b0b74cd"}, - {file = "httptools-0.6.0.tar.gz", hash = "sha256:9fc6e409ad38cbd68b177cd5158fc4042c796b82ca88d99ec78f07bed6c6b796"}, +python-versions = ">=3.8.0" +files = [ + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, + {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, + {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, + {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, + {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, + {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, + {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, ] [package.extras] @@ -1685,13 +1688,13 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve [[package]] name = "humanize" -version = "4.7.0" +version = "4.9.0" description = "Python humanize utilities" optional = false python-versions = ">=3.8" files = [ - {file = "humanize-4.7.0-py3-none-any.whl", hash = "sha256:df7c429c2d27372b249d3f26eb53b07b166b661326e0325793e0a988082e3889"}, - {file = "humanize-4.7.0.tar.gz", hash = "sha256:7ca0e43e870981fa684acb5b062deb307218193bca1a01f2b2676479df849b3a"}, + {file = "humanize-4.9.0-py3-none-any.whl", hash = "sha256:ce284a76d5b1377fd8836733b983bfb0b76f1aa1c090de2566fcf008d7f6ab16"}, + {file = "humanize-4.9.0.tar.gz", hash = "sha256:582a265c931c683a7e9b8ed9559089dea7edcf6cc95be39a3cbc2c5d5ac2bcfa"}, ] [package.extras] @@ -1699,31 +1702,31 @@ tests = ["freezegun", "pytest", "pytest-cov"] [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] name = "importlib-metadata" -version = "6.8.0" +version = "7.0.1" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, - {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, + {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, + {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] @@ -1769,13 +1772,13 @@ testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", [[package]] name = "jedi" -version = "0.19.0" +version = "0.19.1" description = "An autocompletion tool for Python that can be used for text editors." optional = false python-versions = ">=3.6" files = [ - {file = "jedi-0.19.0-py2.py3-none-any.whl", hash = "sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e"}, - {file = "jedi-0.19.0.tar.gz", hash = "sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4"}, + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, ] [package.dependencies] @@ -1784,7 +1787,7 @@ parso = ">=0.8.3,<0.9.0" [package.extras] docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "jeepney" @@ -1818,50 +1821,15 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] -[[package]] -name = "jsonschema" -version = "4.18.6" -description = "An implementation of JSON Schema validation for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jsonschema-4.18.6-py3-none-any.whl", hash = "sha256:dc274409c36175aad949c68e5ead0853aaffbe8e88c830ae66bb3c7a1728ad2d"}, - {file = "jsonschema-4.18.6.tar.gz", hash = "sha256:ce71d2f8c7983ef75a756e568317bf54bc531dc3ad7e66a128eae0d51623d8a3"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" -referencing = ">=0.28.4" -rpds-py = ">=0.7.1" - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] - -[[package]] -name = "jsonschema-specifications" -version = "2023.7.1" -description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jsonschema_specifications-2023.7.1-py3-none-any.whl", hash = "sha256:05adf340b659828a004220a9613be00fa3f223f2b82002e273dee62fd50524b1"}, - {file = "jsonschema_specifications-2023.7.1.tar.gz", hash = "sha256:c91a50404e88a1f6ba40636778e2ee08f6e24c5613fe4c53ac24578a5a7f72bb"}, -] - -[package.dependencies] -referencing = ">=0.28.0" - [[package]] name = "keyring" -version = "23.13.1" +version = "24.3.0" description = "Store and access your passwords safely." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "keyring-23.13.1-py3-none-any.whl", hash = "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd"}, - {file = "keyring-23.13.1.tar.gz", hash = "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678"}, + {file = "keyring-24.3.0-py3-none-any.whl", hash = "sha256:4446d35d636e6a10b8bce7caa66913dd9eca5fd222ca03a3d42c38608ac30836"}, + {file = "keyring-24.3.0.tar.gz", hash = "sha256:e730ecffd309658a08ee82535a3b5ec4b4c8669a9be11efb66249d8e0aeb9a25"}, ] [package.dependencies] @@ -1872,30 +1840,19 @@ pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] -completion = ["shtab"] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[[package]] -name = "lockfile" -version = "0.12.2" -description = "Platform-independent file locking module" -optional = false -python-versions = "*" -files = [ - {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"}, - {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, -] +completion = ["shtab (>=1.1.0)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [[package]] name = "mako" -version = "1.2.4" +version = "1.3.0" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, - {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, + {file = "Mako-1.3.0-py3-none-any.whl", hash = "sha256:57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9"}, + {file = "Mako-1.3.0.tar.gz", hash = "sha256:e3a9d388fd00e87043edbe8792f45880ac0114e9c4adc69f6e9bfb2c55e3b11b"}, ] [package.dependencies] @@ -2036,85 +1993,78 @@ url = "../lib" [[package]] name = "more-itertools" -version = "10.1.0" +version = "10.2.0" description = "More routines for operating on iterables, beyond itertools" optional = false python-versions = ">=3.8" files = [ - {file = "more-itertools-10.1.0.tar.gz", hash = "sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a"}, - {file = "more_itertools-10.1.0-py3-none-any.whl", hash = "sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6"}, + {file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"}, + {file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"}, ] [[package]] name = "msgpack" -version = "1.0.5" +version = "1.0.7" description = "MessagePack serializer" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"}, - {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"}, - {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"}, - {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"}, - {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"}, - {file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"}, - {file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"}, - {file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"}, - {file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"}, - {file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"}, - {file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"}, - {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"}, - {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"}, - {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"}, - {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, - {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, + {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862"}, + {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329"}, + {file = "msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681"}, + {file = "msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9"}, + {file = "msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e"}, + {file = "msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1"}, + {file = "msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5"}, + {file = "msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9"}, + {file = "msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c"}, + {file = "msgpack-1.0.7-cp38-cp38-win32.whl", hash = "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2"}, + {file = "msgpack-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f"}, + {file = "msgpack-1.0.7-cp39-cp39-win32.whl", hash = "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad"}, + {file = "msgpack-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3"}, + {file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"}, ] [[package]] @@ -2202,36 +2152,47 @@ files = [ [[package]] name = "numpy" -version = "1.25.2" +version = "1.26.3" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-1.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3"}, - {file = "numpy-1.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f"}, - {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187"}, - {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357"}, - {file = "numpy-1.25.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9"}, - {file = "numpy-1.25.2-cp310-cp310-win32.whl", hash = "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044"}, - {file = "numpy-1.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545"}, - {file = "numpy-1.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418"}, - {file = "numpy-1.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f"}, - {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2"}, - {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf"}, - {file = "numpy-1.25.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364"}, - {file = "numpy-1.25.2-cp311-cp311-win32.whl", hash = "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d"}, - {file = "numpy-1.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4"}, - {file = "numpy-1.25.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3"}, - {file = "numpy-1.25.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926"}, - {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca"}, - {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295"}, - {file = "numpy-1.25.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f"}, - {file = "numpy-1.25.2-cp39-cp39-win32.whl", hash = "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01"}, - {file = "numpy-1.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf"}, - {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, + {file = "numpy-1.26.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:806dd64230dbbfaca8a27faa64e2f414bf1c6622ab78cc4264f7f5f028fee3bf"}, + {file = "numpy-1.26.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f98011ba4ab17f46f80f7f8f1c291ee7d855fcef0a5a98db80767a468c85cd"}, + {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d45b3ec2faed4baca41c76617fcdcfa4f684ff7a151ce6fc78ad3b6e85af0a6"}, + {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdd2b45bf079d9ad90377048e2747a0c82351989a2165821f0c96831b4a2a54b"}, + {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:211ddd1e94817ed2d175b60b6374120244a4dd2287f4ece45d49228b4d529178"}, + {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1240f767f69d7c4c8a29adde2310b871153df9b26b5cb2b54a561ac85146485"}, + {file = "numpy-1.26.3-cp310-cp310-win32.whl", hash = "sha256:21a9484e75ad018974a2fdaa216524d64ed4212e418e0a551a2d83403b0531d3"}, + {file = "numpy-1.26.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e1591f6ae98bcfac2a4bbf9221c0b92ab49762228f38287f6eeb5f3f55905ce"}, + {file = "numpy-1.26.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b831295e5472954104ecb46cd98c08b98b49c69fdb7040483aff799a755a7374"}, + {file = "numpy-1.26.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e87562b91f68dd8b1c39149d0323b42e0082db7ddb8e934ab4c292094d575d6"}, + {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c66d6fec467e8c0f975818c1796d25c53521124b7cfb760114be0abad53a0a2"}, + {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f25e2811a9c932e43943a2615e65fc487a0b6b49218899e62e426e7f0a57eeda"}, + {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af36e0aa45e25c9f57bf684b1175e59ea05d9a7d3e8e87b7ae1a1da246f2767e"}, + {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:51c7f1b344f302067b02e0f5b5d2daa9ed4a721cf49f070280ac202738ea7f00"}, + {file = "numpy-1.26.3-cp311-cp311-win32.whl", hash = "sha256:7ca4f24341df071877849eb2034948459ce3a07915c2734f1abb4018d9c49d7b"}, + {file = "numpy-1.26.3-cp311-cp311-win_amd64.whl", hash = "sha256:39763aee6dfdd4878032361b30b2b12593fb445ddb66bbac802e2113eb8a6ac4"}, + {file = "numpy-1.26.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a7081fd19a6d573e1a05e600c82a1c421011db7935ed0d5c483e9dd96b99cf13"}, + {file = "numpy-1.26.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12c70ac274b32bc00c7f61b515126c9205323703abb99cd41836e8125ea0043e"}, + {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f784e13e598e9594750b2ef6729bcd5a47f6cfe4a12cca13def35e06d8163e3"}, + {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f24750ef94d56ce6e33e4019a8a4d68cfdb1ef661a52cdaee628a56d2437419"}, + {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:77810ef29e0fb1d289d225cabb9ee6cf4d11978a00bb99f7f8ec2132a84e0166"}, + {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8ed07a90f5450d99dad60d3799f9c03c6566709bd53b497eb9ccad9a55867f36"}, + {file = "numpy-1.26.3-cp312-cp312-win32.whl", hash = "sha256:f73497e8c38295aaa4741bdfa4fda1a5aedda5473074369eca10626835445511"}, + {file = "numpy-1.26.3-cp312-cp312-win_amd64.whl", hash = "sha256:da4b0c6c699a0ad73c810736303f7fbae483bcb012e38d7eb06a5e3b432c981b"}, + {file = "numpy-1.26.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1666f634cb3c80ccbd77ec97bc17337718f56d6658acf5d3b906ca03e90ce87f"}, + {file = "numpy-1.26.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18c3319a7d39b2c6a9e3bb75aab2304ab79a811ac0168a671a62e6346c29b03f"}, + {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b7e807d6888da0db6e7e75838444d62495e2b588b99e90dd80c3459594e857b"}, + {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4d362e17bcb0011738c2d83e0a65ea8ce627057b2fdda37678f4374a382a137"}, + {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b8c275f0ae90069496068c714387b4a0eba5d531aace269559ff2b43655edd58"}, + {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc0743f0302b94f397a4a65a660d4cd24267439eb16493fb3caad2e4389bccbb"}, + {file = "numpy-1.26.3-cp39-cp39-win32.whl", hash = "sha256:9bc6d1a7f8cedd519c4b7b1156d98e051b726bf160715b769106661d567b3f03"}, + {file = "numpy-1.26.3-cp39-cp39-win_amd64.whl", hash = "sha256:867e3644e208c8922a3be26fc6bbf112a035f50f0a86497f98f228c50c607bb2"}, + {file = "numpy-1.26.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3c67423b3703f8fbd90f5adaa37f85b5794d3366948efe9a5190a5f3a83fc34e"}, + {file = "numpy-1.26.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46f47ee566d98849323f01b349d58f2557f02167ee301e5e28809a8c0e27a2d0"}, + {file = "numpy-1.26.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a8474703bffc65ca15853d5fd4d06b18138ae90c17c8d12169968e998e448bb5"}, + {file = "numpy-1.26.3.tar.gz", hash = "sha256:697df43e2b6310ecc9d95f05d5ef20eacc09c7c4ecc9da3f235d39e71b7da1e4"}, ] [[package]] @@ -2268,13 +2229,13 @@ dev = ["black", "mypy", "pytest"] [[package]] name = "packaging" -version = "23.1" +version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] [[package]] @@ -2396,29 +2357,27 @@ pytzdata = ">=2020.1" [[package]] name = "pex" -version = "2.0.3" +version = "2.1.156" description = "The PEX packaging toolchain." optional = false -python-versions = "*" +python-versions = ">=2.7,<3.13,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ - {file = "pex-2.0.3-py2.py3-none-any.whl", hash = "sha256:4ca62e27fd30cd1d4acbcdadfc52739c6e73c9292613d98cb35d88065174fa63"}, - {file = "pex-2.0.3.tar.gz", hash = "sha256:a8a35e7eb212616b2964d70d8a134d41d16649c943ab206b90c749c005e60999"}, + {file = "pex-2.1.156-py2.py3-none-any.whl", hash = "sha256:e7c00fe6f12f6b2ed57ab8e55c4d422647b30e25a4a275cfbc3d3b0bc26e774a"}, + {file = "pex-2.1.156.tar.gz", hash = "sha256:542ecb457c21f5ae8fa749894098e1c54e8639628efee70ece7f89da602aa4c2"}, ] [package.extras] -cachecontrol = ["CacheControl (>=0.12.3)"] -requests = ["requests (>=2.8.14)"] subprocess = ["subprocess32 (>=3.2.7)"] [[package]] name = "pexpect" -version = "4.8.0" +version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." optional = false python-versions = "*" files = [ - {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, - {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, ] [package.dependencies] @@ -2440,13 +2399,13 @@ testing = ["pytest", "pytest-cov"] [[package]] name = "platformdirs" -version = "3.10.0" +version = "3.11.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, - {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, ] [package.extras] @@ -2455,13 +2414,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co [[package]] name = "pluggy" -version = "1.2.0" +version = "1.3.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [package.extras] @@ -2480,70 +2439,66 @@ files = [ [[package]] name = "poetry" -version = "1.5.1" +version = "1.7.1" description = "Python dependency management and packaging made easy." optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.8,<4.0" files = [ - {file = "poetry-1.5.1-py3-none-any.whl", hash = "sha256:dfc7ce3a38ae216c0465694e2e674bef6eb1a2ba81aa47a26f9dc03362fe2f5f"}, - {file = "poetry-1.5.1.tar.gz", hash = "sha256:cc7ea4524d1a11558006224bfe8ba8ed071417d4eb5ef6c89decc6a37d437eeb"}, + {file = "poetry-1.7.1-py3-none-any.whl", hash = "sha256:03d3807a0fb3bc1028cc3707dfd646aae629d58e476f7e7f062437680741c561"}, + {file = "poetry-1.7.1.tar.gz", hash = "sha256:b348a70e7d67ad9c0bd3d0ea255bc6df84c24cf4b16f8d104adb30b425d6ff32"}, ] [package.dependencies] -build = ">=0.10.0,<0.11.0" -cachecontrol = {version = ">=0.12.9,<0.13.0", extras = ["filecache"]} -cleo = ">=2.0.0,<3.0.0" +build = ">=1.0.3,<2.0.0" +cachecontrol = {version = ">=0.13.0,<0.14.0", extras = ["filecache"]} +cleo = ">=2.1.0,<3.0.0" crashtest = ">=0.4.1,<0.5.0" dulwich = ">=0.21.2,<0.22.0" -filelock = ">=3.8.0,<4.0.0" -html5lib = ">=1.0,<2.0" +fastjsonschema = ">=2.18.0,<3.0.0" importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} installer = ">=0.7.0,<0.8.0" -jsonschema = ">=4.10.0,<5.0.0" -keyring = ">=23.9.0,<24.0.0" -lockfile = ">=0.12.2,<0.13.0" -packaging = ">=20.4" +keyring = ">=24.0.0,<25.0.0" +packaging = ">=20.5" pexpect = ">=4.7.0,<5.0.0" pkginfo = ">=1.9.4,<2.0.0" platformdirs = ">=3.0.0,<4.0.0" -poetry-core = "1.6.1" -poetry-plugin-export = ">=1.4.0,<2.0.0" +poetry-core = "1.8.1" +poetry-plugin-export = ">=1.6.0,<2.0.0" pyproject-hooks = ">=1.0.0,<2.0.0" -requests = ">=2.18,<3.0" +requests = ">=2.26,<3.0" requests-toolbelt = ">=0.9.1,<2" shellingham = ">=1.5,<2.0" tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} tomlkit = ">=0.11.4,<1.0.0" trove-classifiers = ">=2022.5.19" -urllib3 = ">=1.26.0,<2.0.0" -virtualenv = ">=20.22.0,<21.0.0" +virtualenv = ">=20.23.0,<21.0.0" xattr = {version = ">=0.10.0,<0.11.0", markers = "sys_platform == \"darwin\""} [[package]] name = "poetry-core" -version = "1.6.1" +version = "1.8.1" description = "Poetry PEP 517 Build Backend" optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.8,<4.0" files = [ - {file = "poetry_core-1.6.1-py3-none-any.whl", hash = "sha256:70707340447dee0e7f334f9495ae652481c67b32d8d218f296a376ac2ed73573"}, - {file = "poetry_core-1.6.1.tar.gz", hash = "sha256:0f9b0de39665f36d6594657e7d57b6f463cc10f30c28e6d1c3b9ff54c26c9ac3"}, + {file = "poetry_core-1.8.1-py3-none-any.whl", hash = "sha256:194832b24f3283e01c5402eae71a6aae850ecdfe53f50a979c76bf7aa5010ffa"}, + {file = "poetry_core-1.8.1.tar.gz", hash = "sha256:67a76c671da2a70e55047cddda83566035b701f7e463b32a2abfeac6e2a16376"}, ] [[package]] name = "poetry-plugin-export" -version = "1.4.0" +version = "1.6.0" description = "Poetry plugin to export the dependencies to various formats" optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.8,<4.0" files = [ - {file = "poetry_plugin_export-1.4.0-py3-none-any.whl", hash = "sha256:5d9186d6f77cf2bf35fc96bd11fe650cc7656e515b17d99cb65018d50ba22589"}, - {file = "poetry_plugin_export-1.4.0.tar.gz", hash = "sha256:f16974cd9f222d4ef640fa97a8d661b04d4fb339e51da93973f1bc9d578e183f"}, + {file = "poetry_plugin_export-1.6.0-py3-none-any.whl", hash = "sha256:2dce6204c9318f1f6509a11a03921fb3f461b201840b59f1c237b6ab454dabcf"}, + {file = "poetry_plugin_export-1.6.0.tar.gz", hash = "sha256:091939434984267a91abf2f916a26b00cff4eee8da63ec2a24ba4b17cf969a59"}, ] [package.dependencies] -poetry = ">=1.5.0,<2.0.0" -poetry-core = ">=1.6.0,<2.0.0" +poetry = ">=1.6.0,<2.0.0" +poetry-core = ">=1.7.0,<2.0.0" [[package]] name = "poetry2setup" @@ -2561,78 +2516,61 @@ poetry-core = ">=1.0.0,<2.0.0" [[package]] name = "prompt-toolkit" -version = "3.0.39" +version = "3.0.43" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, - {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, + {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, + {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, ] [package.dependencies] wcwidth = "*" -[[package]] -name = "proto-plus" -version = "1.22.3" -description = "Beautiful, Pythonic protocol buffers." -optional = false -python-versions = ">=3.6" -files = [ - {file = "proto-plus-1.22.3.tar.gz", hash = "sha256:fdcd09713cbd42480740d2fe29c990f7fbd885a67efc328aa8be6ee3e9f76a6b"}, - {file = "proto_plus-1.22.3-py3-none-any.whl", hash = "sha256:a49cd903bc0b6ab41f76bf65510439d56ca76f868adf0274e738bfdd096894df"}, -] - -[package.dependencies] -protobuf = ">=3.19.0,<5.0.0dev" - -[package.extras] -testing = ["google-api-core[grpc] (>=1.31.5)"] - [[package]] name = "protobuf" -version = "4.23.4" +version = "4.25.1" description = "" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "protobuf-4.23.4-cp310-abi3-win32.whl", hash = "sha256:5fea3c64d41ea5ecf5697b83e41d09b9589e6f20b677ab3c48e5f242d9b7897b"}, - {file = "protobuf-4.23.4-cp310-abi3-win_amd64.whl", hash = "sha256:7b19b6266d92ca6a2a87effa88ecc4af73ebc5cfde194dc737cf8ef23a9a3b12"}, - {file = "protobuf-4.23.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8547bf44fe8cec3c69e3042f5c4fb3e36eb2a7a013bb0a44c018fc1e427aafbd"}, - {file = "protobuf-4.23.4-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:fee88269a090ada09ca63551bf2f573eb2424035bcf2cb1b121895b01a46594a"}, - {file = "protobuf-4.23.4-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:effeac51ab79332d44fba74660d40ae79985901ac21bca408f8dc335a81aa597"}, - {file = "protobuf-4.23.4-cp37-cp37m-win32.whl", hash = "sha256:c3e0939433c40796ca4cfc0fac08af50b00eb66a40bbbc5dee711998fb0bbc1e"}, - {file = "protobuf-4.23.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9053df6df8e5a76c84339ee4a9f5a2661ceee4a0dab019e8663c50ba324208b0"}, - {file = "protobuf-4.23.4-cp38-cp38-win32.whl", hash = "sha256:e1c915778d8ced71e26fcf43c0866d7499891bca14c4368448a82edc61fdbc70"}, - {file = "protobuf-4.23.4-cp38-cp38-win_amd64.whl", hash = "sha256:351cc90f7d10839c480aeb9b870a211e322bf05f6ab3f55fcb2f51331f80a7d2"}, - {file = "protobuf-4.23.4-cp39-cp39-win32.whl", hash = "sha256:6dd9b9940e3f17077e820b75851126615ee38643c2c5332aa7a359988820c720"}, - {file = "protobuf-4.23.4-cp39-cp39-win_amd64.whl", hash = "sha256:0a5759f5696895de8cc913f084e27fd4125e8fb0914bb729a17816a33819f474"}, - {file = "protobuf-4.23.4-py3-none-any.whl", hash = "sha256:e9d0be5bf34b275b9f87ba7407796556abeeba635455d036c7351f7c183ef8ff"}, - {file = "protobuf-4.23.4.tar.gz", hash = "sha256:ccd9430c0719dce806b93f89c91de7977304729e55377f872a92465d548329a9"}, + {file = "protobuf-4.25.1-cp310-abi3-win32.whl", hash = "sha256:193f50a6ab78a970c9b4f148e7c750cfde64f59815e86f686c22e26b4fe01ce7"}, + {file = "protobuf-4.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:3497c1af9f2526962f09329fd61a36566305e6c72da2590ae0d7d1322818843b"}, + {file = "protobuf-4.25.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:0bf384e75b92c42830c0a679b0cd4d6e2b36ae0cf3dbb1e1dfdda48a244f4bcd"}, + {file = "protobuf-4.25.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:0f881b589ff449bf0b931a711926e9ddaad3b35089cc039ce1af50b21a4ae8cb"}, + {file = "protobuf-4.25.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:ca37bf6a6d0046272c152eea90d2e4ef34593aaa32e8873fc14c16440f22d4b7"}, + {file = "protobuf-4.25.1-cp38-cp38-win32.whl", hash = "sha256:abc0525ae2689a8000837729eef7883b9391cd6aa7950249dcf5a4ede230d5dd"}, + {file = "protobuf-4.25.1-cp38-cp38-win_amd64.whl", hash = "sha256:1484f9e692091450e7edf418c939e15bfc8fc68856e36ce399aed6889dae8bb0"}, + {file = "protobuf-4.25.1-cp39-cp39-win32.whl", hash = "sha256:8bdbeaddaac52d15c6dce38c71b03038ef7772b977847eb6d374fc86636fa510"}, + {file = "protobuf-4.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:becc576b7e6b553d22cbdf418686ee4daa443d7217999125c045ad56322dda10"}, + {file = "protobuf-4.25.1-py3-none-any.whl", hash = "sha256:a19731d5e83ae4737bb2a089605e636077ac001d18781b3cf489b9546c7c80d6"}, + {file = "protobuf-4.25.1.tar.gz", hash = "sha256:57d65074b4f5baa4ab5da1605c02be90ac20c8b40fb137d6a8df9f416b0d0ce2"}, ] [[package]] name = "psutil" -version = "5.9.5" +version = "5.9.7" description = "Cross-platform lib for process and system monitoring in Python." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "psutil-5.9.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f"}, - {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5"}, - {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4"}, - {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48"}, - {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4"}, - {file = "psutil-5.9.5-cp27-none-win32.whl", hash = "sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f"}, - {file = "psutil-5.9.5-cp27-none-win_amd64.whl", hash = "sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42"}, - {file = "psutil-5.9.5-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217"}, - {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da"}, - {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4"}, - {file = "psutil-5.9.5-cp36-abi3-win32.whl", hash = "sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d"}, - {file = "psutil-5.9.5-cp36-abi3-win_amd64.whl", hash = "sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9"}, - {file = "psutil-5.9.5-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30"}, - {file = "psutil-5.9.5.tar.gz", hash = "sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c"}, + {file = "psutil-5.9.7-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0bd41bf2d1463dfa535942b2a8f0e958acf6607ac0be52265ab31f7923bcd5e6"}, + {file = "psutil-5.9.7-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:5794944462509e49d4d458f4dbfb92c47539e7d8d15c796f141f474010084056"}, + {file = "psutil-5.9.7-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:fe361f743cb3389b8efda21980d93eb55c1f1e3898269bc9a2a1d0bb7b1f6508"}, + {file = "psutil-5.9.7-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:e469990e28f1ad738f65a42dcfc17adaed9d0f325d55047593cb9033a0ab63df"}, + {file = "psutil-5.9.7-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:3c4747a3e2ead1589e647e64aad601981f01b68f9398ddf94d01e3dc0d1e57c7"}, + {file = "psutil-5.9.7-cp27-none-win32.whl", hash = "sha256:1d4bc4a0148fdd7fd8f38e0498639ae128e64538faa507df25a20f8f7fb2341c"}, + {file = "psutil-5.9.7-cp27-none-win_amd64.whl", hash = "sha256:4c03362e280d06bbbfcd52f29acd79c733e0af33d707c54255d21029b8b32ba6"}, + {file = "psutil-5.9.7-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ea36cc62e69a13ec52b2f625c27527f6e4479bca2b340b7a452af55b34fcbe2e"}, + {file = "psutil-5.9.7-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1132704b876e58d277168cd729d64750633d5ff0183acf5b3c986b8466cd0284"}, + {file = "psutil-5.9.7-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8b7f07948f1304497ce4f4684881250cd859b16d06a1dc4d7941eeb6233bfe"}, + {file = "psutil-5.9.7-cp36-cp36m-win32.whl", hash = "sha256:b27f8fdb190c8c03914f908a4555159327d7481dac2f01008d483137ef3311a9"}, + {file = "psutil-5.9.7-cp36-cp36m-win_amd64.whl", hash = "sha256:44969859757f4d8f2a9bd5b76eba8c3099a2c8cf3992ff62144061e39ba8568e"}, + {file = "psutil-5.9.7-cp37-abi3-win32.whl", hash = "sha256:c727ca5a9b2dd5193b8644b9f0c883d54f1248310023b5ad3e92036c5e2ada68"}, + {file = "psutil-5.9.7-cp37-abi3-win_amd64.whl", hash = "sha256:f37f87e4d73b79e6c5e749440c3113b81d1ee7d26f21c19c47371ddea834f414"}, + {file = "psutil-5.9.7-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:032f4f2c909818c86cea4fe2cc407f1c0f0cde8e6c6d702b28b8ce0c0d143340"}, + {file = "psutil-5.9.7.tar.gz", hash = "sha256:3f02134e82cfb5d089fddf20bb2e03fd5cd52395321d1c8458a9e58500ff417c"}, ] [package.extras] @@ -2640,19 +2578,19 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] name = "ptpython" -version = "3.0.23" +version = "3.0.25" description = "Python REPL build on top of prompt_toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "ptpython-3.0.23-py2.py3-none-any.whl", hash = "sha256:51069503684169b21e1980734a9ba2e104643b7e6a50d3ca0e5669ea70d9e21c"}, - {file = "ptpython-3.0.23.tar.gz", hash = "sha256:9fc9bec2cc51bc4000c1224d8c56241ce8a406b3d49ec8dc266f78cd3cd04ba4"}, + {file = "ptpython-3.0.25-py2.py3-none-any.whl", hash = "sha256:16654143dea960dcefb9d6e69af5f92f01c7a783dd28ff99e78bc7449fba805c"}, + {file = "ptpython-3.0.25.tar.gz", hash = "sha256:887f0a91a576bc26585a0dcec41cd03f004ac7c46a2c88576c87fc51d6c06cd7"}, ] [package.dependencies] appdirs = "*" jedi = ">=0.16.0" -prompt-toolkit = ">=3.0.28,<3.1.0" +prompt-toolkit = ">=3.0.34,<3.1.0" pygments = "*" [package.extras] @@ -2672,36 +2610,47 @@ files = [ [[package]] name = "pyarrow" -version = "12.0.1" +version = "14.0.2" description = "Python library for Apache Arrow" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pyarrow-12.0.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:6d288029a94a9bb5407ceebdd7110ba398a00412c5b0155ee9813a40d246c5df"}, - {file = "pyarrow-12.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345e1828efdbd9aa4d4de7d5676778aba384a2c3add896d995b23d368e60e5af"}, - {file = "pyarrow-12.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d6009fdf8986332b2169314da482baed47ac053311c8934ac6651e614deacd6"}, - {file = "pyarrow-12.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d3c4cbbf81e6dd23fe921bc91dc4619ea3b79bc58ef10bce0f49bdafb103daf"}, - {file = "pyarrow-12.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:cdacf515ec276709ac8042c7d9bd5be83b4f5f39c6c037a17a60d7ebfd92c890"}, - {file = "pyarrow-12.0.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:749be7fd2ff260683f9cc739cb862fb11be376de965a2a8ccbf2693b098db6c7"}, - {file = "pyarrow-12.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6895b5fb74289d055c43db3af0de6e16b07586c45763cb5e558d38b86a91e3a7"}, - {file = "pyarrow-12.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1887bdae17ec3b4c046fcf19951e71b6a619f39fa674f9881216173566c8f718"}, - {file = "pyarrow-12.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2c9cb8eeabbadf5fcfc3d1ddea616c7ce893db2ce4dcef0ac13b099ad7ca082"}, - {file = "pyarrow-12.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:ce4aebdf412bd0eeb800d8e47db854f9f9f7e2f5a0220440acf219ddfddd4f63"}, - {file = "pyarrow-12.0.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:e0d8730c7f6e893f6db5d5b86eda42c0a130842d101992b581e2138e4d5663d3"}, - {file = "pyarrow-12.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43364daec02f69fec89d2315f7fbfbeec956e0d991cbbef471681bd77875c40f"}, - {file = "pyarrow-12.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:051f9f5ccf585f12d7de836e50965b3c235542cc896959320d9776ab93f3b33d"}, - {file = "pyarrow-12.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:be2757e9275875d2a9c6e6052ac7957fbbfc7bc7370e4a036a9b893e96fedaba"}, - {file = "pyarrow-12.0.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:cf812306d66f40f69e684300f7af5111c11f6e0d89d6b733e05a3de44961529d"}, - {file = "pyarrow-12.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:459a1c0ed2d68671188b2118c63bac91eaef6fc150c77ddd8a583e3c795737bf"}, - {file = "pyarrow-12.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85e705e33eaf666bbe508a16fd5ba27ca061e177916b7a317ba5a51bee43384c"}, - {file = "pyarrow-12.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9120c3eb2b1f6f516a3b7a9714ed860882d9ef98c4b17edcdc91d95b7528db60"}, - {file = "pyarrow-12.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:c780f4dc40460015d80fcd6a6140de80b615349ed68ef9adb653fe351778c9b3"}, - {file = "pyarrow-12.0.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a3c63124fc26bf5f95f508f5d04e1ece8cc23a8b0af2a1e6ab2b1ec3fdc91b24"}, - {file = "pyarrow-12.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b13329f79fa4472324f8d32dc1b1216616d09bd1e77cfb13104dec5463632c36"}, - {file = "pyarrow-12.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb656150d3d12ec1396f6dde542db1675a95c0cc8366d507347b0beed96e87ca"}, - {file = "pyarrow-12.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6251e38470da97a5b2e00de5c6a049149f7b2bd62f12fa5dbb9ac674119ba71a"}, - {file = "pyarrow-12.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:3de26da901216149ce086920547dfff5cd22818c9eab67ebc41e863a5883bac7"}, - {file = "pyarrow-12.0.1.tar.gz", hash = "sha256:cce317fc96e5b71107bf1f9f184d5e54e2bd14bbf3f9a3d62819961f0af86fec"}, + {file = "pyarrow-14.0.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:ba9fe808596c5dbd08b3aeffe901e5f81095baaa28e7d5118e01354c64f22807"}, + {file = "pyarrow-14.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:22a768987a16bb46220cef490c56c671993fbee8fd0475febac0b3e16b00a10e"}, + {file = "pyarrow-14.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dbba05e98f247f17e64303eb876f4a80fcd32f73c7e9ad975a83834d81f3fda"}, + {file = "pyarrow-14.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a898d134d00b1eca04998e9d286e19653f9d0fcb99587310cd10270907452a6b"}, + {file = "pyarrow-14.0.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:87e879323f256cb04267bb365add7208f302df942eb943c93a9dfeb8f44840b1"}, + {file = "pyarrow-14.0.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:76fc257559404ea5f1306ea9a3ff0541bf996ff3f7b9209fc517b5e83811fa8e"}, + {file = "pyarrow-14.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0c4a18e00f3a32398a7f31da47fefcd7a927545b396e1f15d0c85c2f2c778cd"}, + {file = "pyarrow-14.0.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:87482af32e5a0c0cce2d12eb3c039dd1d853bd905b04f3f953f147c7a196915b"}, + {file = "pyarrow-14.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:059bd8f12a70519e46cd64e1ba40e97eae55e0cbe1695edd95384653d7626b23"}, + {file = "pyarrow-14.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f16111f9ab27e60b391c5f6d197510e3ad6654e73857b4e394861fc79c37200"}, + {file = "pyarrow-14.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06ff1264fe4448e8d02073f5ce45a9f934c0f3db0a04460d0b01ff28befc3696"}, + {file = "pyarrow-14.0.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6dd4f4b472ccf4042f1eab77e6c8bce574543f54d2135c7e396f413046397d5a"}, + {file = "pyarrow-14.0.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:32356bfb58b36059773f49e4e214996888eeea3a08893e7dbde44753799b2a02"}, + {file = "pyarrow-14.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:52809ee69d4dbf2241c0e4366d949ba035cbcf48409bf404f071f624ed313a2b"}, + {file = "pyarrow-14.0.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:c87824a5ac52be210d32906c715f4ed7053d0180c1060ae3ff9b7e560f53f944"}, + {file = "pyarrow-14.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a25eb2421a58e861f6ca91f43339d215476f4fe159eca603c55950c14f378cc5"}, + {file = "pyarrow-14.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c1da70d668af5620b8ba0a23f229030a4cd6c5f24a616a146f30d2386fec422"}, + {file = "pyarrow-14.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cc61593c8e66194c7cdfae594503e91b926a228fba40b5cf25cc593563bcd07"}, + {file = "pyarrow-14.0.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:78ea56f62fb7c0ae8ecb9afdd7893e3a7dbeb0b04106f5c08dbb23f9c0157591"}, + {file = "pyarrow-14.0.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:37c233ddbce0c67a76c0985612fef27c0c92aef9413cf5aa56952f359fcb7379"}, + {file = "pyarrow-14.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:e4b123ad0f6add92de898214d404e488167b87b5dd86e9a434126bc2b7a5578d"}, + {file = "pyarrow-14.0.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e354fba8490de258be7687f341bc04aba181fc8aa1f71e4584f9890d9cb2dec2"}, + {file = "pyarrow-14.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:20e003a23a13da963f43e2b432483fdd8c38dc8882cd145f09f21792e1cf22a1"}, + {file = "pyarrow-14.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc0de7575e841f1595ac07e5bc631084fd06ca8b03c0f2ecece733d23cd5102a"}, + {file = "pyarrow-14.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66e986dc859712acb0bd45601229021f3ffcdfc49044b64c6d071aaf4fa49e98"}, + {file = "pyarrow-14.0.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:f7d029f20ef56673a9730766023459ece397a05001f4e4d13805111d7c2108c0"}, + {file = "pyarrow-14.0.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:209bac546942b0d8edc8debda248364f7f668e4aad4741bae58e67d40e5fcf75"}, + {file = "pyarrow-14.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1e6987c5274fb87d66bb36816afb6f65707546b3c45c44c28e3c4133c010a881"}, + {file = "pyarrow-14.0.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a01d0052d2a294a5f56cc1862933014e696aa08cc7b620e8c0cce5a5d362e976"}, + {file = "pyarrow-14.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a51fee3a7db4d37f8cda3ea96f32530620d43b0489d169b285d774da48ca9785"}, + {file = "pyarrow-14.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64df2bf1ef2ef14cee531e2dfe03dd924017650ffaa6f9513d7a1bb291e59c15"}, + {file = "pyarrow-14.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c0fa3bfdb0305ffe09810f9d3e2e50a2787e3a07063001dcd7adae0cee3601a"}, + {file = "pyarrow-14.0.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c65bf4fd06584f058420238bc47a316e80dda01ec0dfb3044594128a6c2db794"}, + {file = "pyarrow-14.0.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:63ac901baec9369d6aae1cbe6cca11178fb018a8d45068aaf5bb54f94804a866"}, + {file = "pyarrow-14.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:75ee0efe7a87a687ae303d63037d08a48ef9ea0127064df18267252cfe2e9541"}, + {file = "pyarrow-14.0.2.tar.gz", hash = "sha256:36cef6ba12b499d864d1def3e990f97949e0b79400d08b7cf74504ffbd3eb025"}, ] [package.dependencies] @@ -2709,13 +2658,13 @@ numpy = ">=1.16.6" [[package]] name = "pyasn1" -version = "0.5.0" +version = "0.5.1" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"}, - {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, + {file = "pyasn1-0.5.1-py2.py3-none-any.whl", hash = "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58"}, + {file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"}, ] [[package]] @@ -2745,47 +2694,47 @@ files = [ [[package]] name = "pydantic" -version = "1.10.12" +version = "1.10.13" description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, - {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, - {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, - {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, - {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, - {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, - {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, - {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, - {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, - {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, + {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, + {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, + {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, + {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, + {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, + {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, + {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, + {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, + {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, + {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, ] [package.dependencies] @@ -2828,17 +2777,18 @@ requests = ">=2.14.0" [[package]] name = "pygments" -version = "2.15.1" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" files = [ - {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, - {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" @@ -2947,13 +2897,13 @@ files = [ [[package]] name = "pytest" -version = "7.4.0" +version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -2997,13 +2947,13 @@ cli = ["click (>=5.0)"] [[package]] name = "pytz" -version = "2023.3" +version = "2023.3.post1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, - {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, ] [[package]] @@ -3119,123 +3069,106 @@ docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphin [[package]] name = "rapidfuzz" -version = "2.15.1" +version = "3.6.1" description = "rapid fuzzy string matching" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "rapidfuzz-2.15.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc0bc259ebe3b93e7ce9df50b3d00e7345335d35acbd735163b7c4b1957074d3"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d59fb3a410d253f50099d7063855c2b95df1ef20ad93ea3a6b84115590899f25"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c525a3da17b6d79d61613096c8683da86e3573e807dfaecf422eea09e82b5ba6"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4deae6a918ecc260d0c4612257be8ba321d8e913ccb43155403842758c46fbe"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2577463d10811386e704a3ab58b903eb4e2a31b24dfd9886d789b0084d614b01"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f67d5f56aa48c0da9de4ab81bffb310683cf7815f05ea38e5aa64f3ba4368339"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7927722ff43690e52b3145b5bd3089151d841d350c6f8378c3cfac91f67573a"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6534afc787e32c4104f65cdeb55f6abe4d803a2d0553221d00ef9ce12788dcde"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d0ae6ec79a1931929bb9dd57bc173eb5ba4c7197461bf69e3a34b6dd314feed2"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be7ccc45c4d1a7dfb595f260e8022a90c6cb380c2a346ee5aae93f85c96d362b"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:8ba013500a2b68c64b2aecc5fb56a2dad6c2872cf545a0308fd044827b6e5f6a"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4d9f7d10065f657f960b48699e7dddfce14ab91af4bab37a215f0722daf0d716"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7e24a1b802cea04160b3fccd75d2d0905065783ebc9de157d83c14fb9e1c6ce2"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-win32.whl", hash = "sha256:dffdf03499e0a5b3442951bb82b556333b069e0661e80568752786c79c5b32de"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d150d90a7c6caae7962f29f857a4e61d42038cfd82c9df38508daf30c648ae7"}, - {file = "rapidfuzz-2.15.1-cp310-cp310-win_arm64.whl", hash = "sha256:87c30e9184998ff6eb0fa9221f94282ce7c908fd0da96a1ef66ecadfaaa4cdb7"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6986413cb37035eb796e32f049cbc8c13d8630a4ac1e0484e3e268bb3662bd1b"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a72f26e010d4774b676f36e43c0fc8a2c26659efef4b3be3fd7714d3491e9957"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b5cd54c98a387cca111b3b784fc97a4f141244bbc28a92d4bde53f164464112e"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da7fac7c3da39f93e6b2ebe386ed0ffe1cefec91509b91857f6e1204509e931f"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f976e76ac72f650790b3a5402431612175b2ac0363179446285cb3c901136ca9"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:abde47e1595902a490ed14d4338d21c3509156abb2042a99e6da51f928e0c117"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca8f1747007a3ce919739a60fa95c5325f7667cccf6f1c1ef18ae799af119f5e"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c35da09ab9797b020d0d4f07a66871dfc70ea6566363811090353ea971748b5a"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a3a769ca7580686a66046b77df33851b3c2d796dc1eb60c269b68f690f3e1b65"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d50622efefdb03a640a51a6123748cd151d305c1f0431af762e833d6ffef71f0"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b7461b0a7651d68bc23f0896bffceea40f62887e5ab8397bf7caa883592ef5cb"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:074ee9e17912e025c72a5780ee4c7c413ea35cd26449719cc399b852d4e42533"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7025fb105a11f503943f17718cdb8241ea3bb4d812c710c609e69bead40e2ff0"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-win32.whl", hash = "sha256:2084d36b95139413cef25e9487257a1cc892b93bd1481acd2a9656f7a1d9930c"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:5a738fcd24e34bce4b19126b92fdae15482d6d3a90bd687fd3d24ce9d28ce82d"}, - {file = "rapidfuzz-2.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:dc3cafa68cfa54638632bdcadf9aab89a3d182b4a3f04d2cad7585ed58ea8731"}, - {file = "rapidfuzz-2.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3c53d57ba7a88f7bf304d4ea5a14a0ca112db0e0178fff745d9005acf2879f7d"}, - {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6ee758eec4cf2215dc8d8eafafcea0d1f48ad4b0135767db1b0f7c5c40a17dd"}, - {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d93ba3ae59275e7a3a116dac4ffdb05e9598bf3ee0861fecc5b60fb042d539e"}, - {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c3ff75e647908ddbe9aa917fbe39a112d5631171f3fcea5809e2363e525a59d"}, - {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d89c421702474c6361245b6b199e6e9783febacdbfb6b002669e6cb3ef17a09"}, - {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f69e6199fec0f58f9a89afbbaea78d637c7ce77f656a03a1d6ea6abdc1d44f8"}, - {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:41dfea282844d0628279b4db2929da0dacb8ac317ddc5dcccc30093cf16357c1"}, - {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2dd03477feefeccda07b7659dd614f6738cfc4f9b6779dd61b262a73b0a9a178"}, - {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:5efe035aa76ff37d1b5fa661de3c4b4944de9ff227a6c0b2e390a95c101814c0"}, - {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ed2cf7c69102c7a0a06926d747ed855bc836f52e8d59a5d1e3adfd980d1bd165"}, - {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a0e441d4c2025110ec3eba5d54f11f78183269a10152b3a757a739ffd1bb12bf"}, - {file = "rapidfuzz-2.15.1-cp37-cp37m-win32.whl", hash = "sha256:a4a54efe17cc9f53589c748b53f28776dfdfb9bc83619685740cb7c37985ac2f"}, - {file = "rapidfuzz-2.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:bb8318116ecac4dfb84841d8b9b461f9bb0c3be5b616418387d104f72d2a16d1"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e9296c530e544f68858c3416ad1d982a1854f71e9d2d3dcedb5b216e6d54f067"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:49c4bcdb9238f11f8c4eba1b898937f09b92280d6f900023a8216008f299b41a"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb40a279e134bb3fef099a8b58ed5beefb201033d29bdac005bddcdb004ef71"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7381c11cb590bbd4e6f2d8779a0b34fdd2234dfa13d0211f6aee8ca166d9d05"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfdcdedfd12a0077193f2cf3626ff6722c5a184adf0d2d51f1ec984bf21c23c3"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85bece1ec59bda8b982bd719507d468d4df746dfb1988df11d916b5e9fe19e8"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1b393f4a1eaa6867ffac6aef58cfb04bab2b3d7d8e40b9fe2cf40dd1d384601"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53de456ef020a77bf9d7c6c54860a48e2e902584d55d3001766140ac45c54bc7"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2492330bc38b76ed967eab7bdaea63a89b6ceb254489e2c65c3824efcbf72993"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:099e4c6befaa8957a816bdb67ce664871f10aaec9bebf2f61368cf7e0869a7a1"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:46599b2ad4045dd3f794a24a6db1e753d23304699d4984462cf1ead02a51ddf3"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:591f19d16758a3c55c9d7a0b786b40d95599a5b244d6eaef79c7a74fcf5104d8"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed17359061840eb249f8d833cb213942e8299ffc4f67251a6ed61833a9f2ea20"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-win32.whl", hash = "sha256:aa1e5aad325168e29bf8e17006479b97024aa9d2fdbe12062bd2f8f09080acf8"}, - {file = "rapidfuzz-2.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:c2bb68832b140c551dbed691290bef4ee6719d4e8ce1b7226a3736f61a9d1a83"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3fac40972cf7b6c14dded88ae2331eb50dfbc278aa9195473ef6fc6bfe49f686"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0e456cbdc0abf39352800309dab82fd3251179fa0ff6573fa117f51f4e84be8"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:22b9d22022b9d09fd4ece15102270ab9b6a5cfea8b6f6d1965c1df7e3783f5ff"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46754fe404a9a6f5cbf7abe02d74af390038d94c9b8c923b3f362467606bfa28"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91abb8bf7610efe326394adc1d45e1baca8f360e74187f3fa0ef3df80cdd3ba6"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e40a2f60024f9d3c15401e668f732800114a023f3f8d8c40f1521a62081ff054"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a48ee83916401ac73938526d7bd804e01d2a8fe61809df7f1577b0b3b31049a3"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c71580052f9dbac443c02f60484e5a2e5f72ad4351b84b2009fbe345b1f38422"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:82b86d5b8c1b9bcbc65236d75f81023c78d06a721c3e0229889ff4ed5c858169"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fc4528b7736e5c30bc954022c2cf410889abc19504a023abadbc59cdf9f37cae"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e1e0e569108a5760d8f01d0f2148dd08cc9a39ead79fbefefca9e7c7723c7e88"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:94e1c97f0ad45b05003806f8a13efc1fc78983e52fa2ddb00629003acf4676ef"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47e81767a962e41477a85ad7ac937e34d19a7d2a80be65614f008a5ead671c56"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-win32.whl", hash = "sha256:79fc574aaf2d7c27ec1022e29c9c18f83cdaf790c71c05779528901e0caad89b"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:f3dd4bcef2d600e0aa121e19e6e62f6f06f22a89f82ef62755e205ce14727874"}, - {file = "rapidfuzz-2.15.1-cp39-cp39-win_arm64.whl", hash = "sha256:cac095cbdf44bc286339a77214bbca6d4d228c9ebae3da5ff6a80aaeb7c35634"}, - {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b89d1126be65c85763d56e3b47d75f1a9b7c5529857b4d572079b9a636eaa8a7"}, - {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19b7460e91168229768be882ea365ba0ac7da43e57f9416e2cfadc396a7df3c2"}, - {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c33c03e7092642c38f8a15ca2d8fc38da366f2526ec3b46adf19d5c7aa48ba"}, - {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040faca2e26d9dab5541b45ce72b3f6c0e36786234703fc2ac8c6f53bb576743"}, - {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:6e2a3b23e1e9aa13474b3c710bba770d0dcc34d517d3dd6f97435a32873e3f28"}, - {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e597b9dfd6dd180982684840975c458c50d447e46928efe3e0120e4ec6f6686"}, - {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d14752c9dd2036c5f36ebe8db5f027275fa7d6b3ec6484158f83efb674bab84e"}, - {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558224b6fc6124d13fa32d57876f626a7d6188ba2a97cbaea33a6ee38a867e31"}, - {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c89cfa88dc16fd8c9bcc0c7f0b0073f7ef1e27cceb246c9f5a3f7004fa97c4d"}, - {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:509c5b631cd64df69f0f011893983eb15b8be087a55bad72f3d616b6ae6a0f96"}, - {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0f73a04135a03a6e40393ecd5d46a7a1049d353fc5c24b82849830d09817991f"}, - {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c99d53138a2dfe8ada67cb2855719f934af2733d726fbf73247844ce4dd6dd5"}, - {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f01fa757f0fb332a1f045168d29b0d005de6c39ee5ce5d6c51f2563bb53c601b"}, - {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60368e1add6e550faae65614844c43f8a96e37bf99404643b648bf2dba92c0fb"}, - {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:785744f1270828cc632c5a3660409dee9bcaac6931a081bae57542c93e4d46c4"}, - {file = "rapidfuzz-2.15.1.tar.gz", hash = "sha256:d62137c2ca37aea90a11003ad7dc109c8f1739bfbe5a9a217f3cdb07d7ac00f6"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ac434fc71edda30d45db4a92ba5e7a42c7405e1a54cb4ec01d03cc668c6dcd40"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a791168e119cfddf4b5a40470620c872812042f0621e6a293983a2d52372db0"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a2f3e9df346145c2be94e4d9eeffb82fab0cbfee85bd4a06810e834fe7c03fa"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23de71e7f05518b0bbeef55d67b5dbce3bcd3e2c81e7e533051a2e9401354eb0"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d056e342989248d2bdd67f1955bb7c3b0ecfa239d8f67a8dfe6477b30872c607"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01835d02acd5d95c1071e1da1bb27fe213c84a013b899aba96380ca9962364bc"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed0f712e0bb5fea327e92aec8a937afd07ba8de4c529735d82e4c4124c10d5a0"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96cd19934f76a1264e8ecfed9d9f5291fde04ecb667faef5f33bdbfd95fe2d1f"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e06c4242a1354cf9d48ee01f6f4e6e19c511d50bb1e8d7d20bcadbb83a2aea90"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d73dcfe789d37c6c8b108bf1e203e027714a239e50ad55572ced3c004424ed3b"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:06e98ff000e2619e7cfe552d086815671ed09b6899408c2c1b5103658261f6f3"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:08b6fb47dd889c69fbc0b915d782aaed43e025df6979b6b7f92084ba55edd526"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a1788ebb5f5b655a15777e654ea433d198f593230277e74d51a2a1e29a986283"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-win32.whl", hash = "sha256:c65f92881753aa1098c77818e2b04a95048f30edbe9c3094dc3707d67df4598b"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:4243a9c35667a349788461aae6471efde8d8800175b7db5148a6ab929628047f"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-win_arm64.whl", hash = "sha256:f59d19078cc332dbdf3b7b210852ba1f5db8c0a2cd8cc4c0ed84cc00c76e6802"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fbc07e2e4ac696497c5f66ec35c21ddab3fc7a406640bffed64c26ab2f7ce6d6"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40cced1a8852652813f30fb5d4b8f9b237112a0bbaeebb0f4cc3611502556764"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82300e5f8945d601c2daaaac139d5524d7c1fdf719aa799a9439927739917460"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf97c321fd641fea2793abce0e48fa4f91f3c202092672f8b5b4e781960b891"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7420e801b00dee4a344ae2ee10e837d603461eb180e41d063699fb7efe08faf0"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:060bd7277dc794279fa95522af355034a29c90b42adcb7aa1da358fc839cdb11"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7e3375e4f2bfec77f907680328e4cd16cc64e137c84b1886d547ab340ba6928"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a490cd645ef9d8524090551016f05f052e416c8adb2d8b85d35c9baa9d0428ab"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2e03038bfa66d2d7cffa05d81c2f18fd6acbb25e7e3c068d52bb7469e07ff382"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b19795b26b979c845dba407fe79d66975d520947b74a8ab6cee1d22686f7967"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:064c1d66c40b3a0f488db1f319a6e75616b2e5fe5430a59f93a9a5e40a656d15"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3c772d04fb0ebeece3109d91f6122b1503023086a9591a0b63d6ee7326bd73d9"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:841eafba6913c4dfd53045835545ba01a41e9644e60920c65b89c8f7e60c00a9"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-win32.whl", hash = "sha256:266dd630f12696ea7119f31d8b8e4959ef45ee2cbedae54417d71ae6f47b9848"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:d79aec8aeee02ab55d0ddb33cea3ecd7b69813a48e423c966a26d7aab025cdfe"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-win_arm64.whl", hash = "sha256:484759b5dbc5559e76fefaa9170147d1254468f555fd9649aea3bad46162a88b"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b2ef4c0fd3256e357b70591ffb9e8ed1d439fb1f481ba03016e751a55261d7c1"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:588c4b20fa2fae79d60a4e438cf7133d6773915df3cc0a7f1351da19eb90f720"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7142ee354e9c06e29a2636b9bbcb592bb00600a88f02aa5e70e4f230347b373e"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1dfc557c0454ad22382373ec1b7df530b4bbd974335efe97a04caec936f2956a"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:03f73b381bdeccb331a12c3c60f1e41943931461cdb52987f2ecf46bfc22f50d"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b0ccc2ec1781c7e5370d96aef0573dd1f97335343e4982bdb3a44c133e27786"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da3e8c9f7e64bb17faefda085ff6862ecb3ad8b79b0f618a6cf4452028aa2222"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fde9b14302a31af7bdafbf5cfbb100201ba21519be2b9dedcf4f1048e4fbe65d"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1a23eee225dfb21c07f25c9fcf23eb055d0056b48e740fe241cbb4b22284379"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e49b9575d16c56c696bc7b06a06bf0c3d4ef01e89137b3ddd4e2ce709af9fe06"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:0a9fc714b8c290261669f22808913aad49553b686115ad0ee999d1cb3df0cd66"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:a3ee4f8f076aa92184e80308fc1a079ac356b99c39408fa422bbd00145be9854"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f056ba42fd2f32e06b2c2ba2443594873cfccc0c90c8b6327904fc2ddf6d5799"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-win32.whl", hash = "sha256:5d82b9651e3d34b23e4e8e201ecd3477c2baa17b638979deeabbb585bcb8ba74"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:dad55a514868dae4543ca48c4e1fc0fac704ead038dafedf8f1fc0cc263746c1"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-win_arm64.whl", hash = "sha256:3c84294f4470fcabd7830795d754d808133329e0a81d62fcc2e65886164be83b"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e19d519386e9db4a5335a4b29f25b8183a1c3f78cecb4c9c3112e7f86470e37f"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01eb03cd880a294d1bf1a583fdd00b87169b9cc9c9f52587411506658c864d73"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:be368573255f8fbb0125a78330a1a40c65e9ba3c5ad129a426ff4289099bfb41"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3e5af946f419c30f5cb98b69d40997fe8580efe78fc83c2f0f25b60d0e56efb"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f382f7ffe384ce34345e1c0b2065451267d3453cadde78946fbd99a59f0cc23c"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be156f51f3a4f369e758505ed4ae64ea88900dcb2f89d5aabb5752676d3f3d7e"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1936d134b6c513fbe934aeb668b0fee1ffd4729a3c9d8d373f3e404fbb0ce8a0"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ff8eaf4a9399eb2bebd838f16e2d1ded0955230283b07376d68947bbc2d33d"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae598a172e3a95df3383634589660d6b170cc1336fe7578115c584a99e0ba64d"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cd4ba4c18b149da11e7f1b3584813159f189dc20833709de5f3df8b1342a9759"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:0402f1629e91a4b2e4aee68043a30191e5e1b7cd2aa8dacf50b1a1bcf6b7d3ab"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:1e12319c6b304cd4c32d5db00b7a1e36bdc66179c44c5707f6faa5a889a317c0"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0bbfae35ce4de4c574b386c43c78a0be176eeddfdae148cb2136f4605bebab89"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-win32.whl", hash = "sha256:7fec74c234d3097612ea80f2a80c60720eec34947066d33d34dc07a3092e8105"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:a553cc1a80d97459d587529cc43a4c7c5ecf835f572b671107692fe9eddf3e24"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:757dfd7392ec6346bd004f8826afb3bf01d18a723c97cbe9958c733ab1a51791"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2963f4a3f763870a16ee076796be31a4a0958fbae133dbc43fc55c3968564cf5"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d2f0274595cc5b2b929c80d4e71b35041104b577e118cf789b3fe0a77b37a4c5"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f211e366e026de110a4246801d43a907cd1a10948082f47e8a4e6da76fef52"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a59472b43879012b90989603aa5a6937a869a72723b1bf2ff1a0d1edee2cc8e6"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a03863714fa6936f90caa7b4b50ea59ea32bb498cc91f74dc25485b3f8fccfe9"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd95b6b7bfb1584f806db89e1e0c8dbb9d25a30a4683880c195cc7f197eaf0c"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7183157edf0c982c0b8592686535c8b3e107f13904b36d85219c77be5cefd0d8"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ad9d74ef7c619b5b0577e909582a1928d93e07d271af18ba43e428dc3512c2a1"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b53137d81e770c82189e07a8f32722d9e4260f13a0aec9914029206ead38cac3"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:49b9ed2472394d306d5dc967a7de48b0aab599016aa4477127b20c2ed982dbf9"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:dec307b57ec2d5054d77d03ee4f654afcd2c18aee00c48014cb70bfed79597d6"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4381023fa1ff32fd5076f5d8321249a9aa62128eb3f21d7ee6a55373e672b261"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-win32.whl", hash = "sha256:8d7a072f10ee57c8413c8ab9593086d42aaff6ee65df4aa6663eecdb7c398dca"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:ebcfb5bfd0a733514352cfc94224faad8791e576a80ffe2fd40b2177bf0e7198"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-win_arm64.whl", hash = "sha256:1c47d592e447738744905c18dda47ed155620204714e6df20eb1941bb1ba315e"}, + {file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:eef8b346ab331bec12bbc83ac75641249e6167fab3d84d8f5ca37fd8e6c7a08c"}, + {file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53251e256017e2b87f7000aee0353ba42392c442ae0bafd0f6b948593d3f68c6"}, + {file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6dede83a6b903e3ebcd7e8137e7ff46907ce9316e9d7e7f917d7e7cdc570ee05"}, + {file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4da90e4c2b444d0a171d7444ea10152e07e95972bb40b834a13bdd6de1110c"}, + {file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ca3dfcf74f2b6962f411c33dd95b0adf3901266e770da6281bc96bb5a8b20de9"}, + {file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bcc957c0a8bde8007f1a8a413a632a1a409890f31f73fe764ef4eac55f59ca87"}, + {file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:692c9a50bea7a8537442834f9bc6b7d29d8729a5b6379df17c31b6ab4df948c2"}, + {file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c23ceaea27e790ddd35ef88b84cf9d721806ca366199a76fd47cfc0457a81b"}, + {file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b155e67fff215c09f130555002e42f7517d0ea72cbd58050abb83cb7c880cec"}, + {file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3028ee8ecc48250607fa8a0adce37b56275ec3b1acaccd84aee1f68487c8557b"}, + {file = "rapidfuzz-3.6.1.tar.gz", hash = "sha256:35660bee3ce1204872574fa041c7ad7ec5175b3053a4cb6e181463fc07013de7"}, ] [package.extras] full = ["numpy"] -[[package]] -name = "referencing" -version = "0.30.1" -description = "JSON Referencing + Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "referencing-0.30.1-py3-none-any.whl", hash = "sha256:185d4a29f001c6e8ae4dad3861e61282a81cb01b9f0ef70a15450c45c6513a0d"}, - {file = "referencing-0.30.1.tar.gz", hash = "sha256:9370c77ceefd39510d70948bbe7375ce2d0125b9c11fd380671d4de959a8e3ce"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -rpds-py = ">=0.7.0" - [[package]] name = "requests" version = "2.31.0" @@ -3259,13 +3192,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-toolbelt" -version = "0.10.1" +version = "1.0.0" description = "A utility belt for advanced users of python-requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "requests-toolbelt-0.10.1.tar.gz", hash = "sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d"}, - {file = "requests_toolbelt-0.10.1-py2.py3-none-any.whl", hash = "sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7"}, + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, ] [package.dependencies] @@ -3273,13 +3206,13 @@ requests = ">=2.0.1,<3.0.0" [[package]] name = "rich" -version = "13.5.2" +version = "13.7.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.5.2-py3-none-any.whl", hash = "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808"}, - {file = "rich-13.5.2.tar.gz", hash = "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39"}, + {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, + {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, ] [package.dependencies] @@ -3289,112 +3222,6 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] -[[package]] -name = "rpds-py" -version = "0.9.2" -description = "Python bindings to Rust's persistent data structures (rpds)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "rpds_py-0.9.2-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:ab6919a09c055c9b092798ce18c6c4adf49d24d4d9e43a92b257e3f2548231e7"}, - {file = "rpds_py-0.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d55777a80f78dd09410bd84ff8c95ee05519f41113b2df90a69622f5540c4f8b"}, - {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a216b26e5af0a8e265d4efd65d3bcec5fba6b26909014effe20cd302fd1138fa"}, - {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29cd8bfb2d716366a035913ced99188a79b623a3512292963d84d3e06e63b496"}, - {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44659b1f326214950a8204a248ca6199535e73a694be8d3e0e869f820767f12f"}, - {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:745f5a43fdd7d6d25a53ab1a99979e7f8ea419dfefebcab0a5a1e9095490ee5e"}, - {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a987578ac5214f18b99d1f2a3851cba5b09f4a689818a106c23dbad0dfeb760f"}, - {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf4151acb541b6e895354f6ff9ac06995ad9e4175cbc6d30aaed08856558201f"}, - {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:03421628f0dc10a4119d714a17f646e2837126a25ac7a256bdf7c3943400f67f"}, - {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13b602dc3e8dff3063734f02dcf05111e887f301fdda74151a93dbbc249930fe"}, - {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fae5cb554b604b3f9e2c608241b5d8d303e410d7dfb6d397c335f983495ce7f6"}, - {file = "rpds_py-0.9.2-cp310-none-win32.whl", hash = "sha256:47c5f58a8e0c2c920cc7783113df2fc4ff12bf3a411d985012f145e9242a2764"}, - {file = "rpds_py-0.9.2-cp310-none-win_amd64.whl", hash = "sha256:4ea6b73c22d8182dff91155af018b11aac9ff7eca085750455c5990cb1cfae6e"}, - {file = "rpds_py-0.9.2-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:e564d2238512c5ef5e9d79338ab77f1cbbda6c2d541ad41b2af445fb200385e3"}, - {file = "rpds_py-0.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f411330a6376fb50e5b7a3e66894e4a39e60ca2e17dce258d53768fea06a37bd"}, - {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e7521f5af0233e89939ad626b15278c71b69dc1dfccaa7b97bd4cdf96536bb7"}, - {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8d3335c03100a073883857e91db9f2e0ef8a1cf42dc0369cbb9151c149dbbc1b"}, - {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d25b1c1096ef0447355f7293fbe9ad740f7c47ae032c2884113f8e87660d8f6e"}, - {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a5d3fbd02efd9cf6a8ffc2f17b53a33542f6b154e88dd7b42ef4a4c0700fdad"}, - {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5934e2833afeaf36bd1eadb57256239785f5af0220ed8d21c2896ec4d3a765f"}, - {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:095b460e117685867d45548fbd8598a8d9999227e9061ee7f012d9d264e6048d"}, - {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:91378d9f4151adc223d584489591dbb79f78814c0734a7c3bfa9c9e09978121c"}, - {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:24a81c177379300220e907e9b864107614b144f6c2a15ed5c3450e19cf536fae"}, - {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:de0b6eceb46141984671802d412568d22c6bacc9b230174f9e55fc72ef4f57de"}, - {file = "rpds_py-0.9.2-cp311-none-win32.whl", hash = "sha256:700375326ed641f3d9d32060a91513ad668bcb7e2cffb18415c399acb25de2ab"}, - {file = "rpds_py-0.9.2-cp311-none-win_amd64.whl", hash = "sha256:0766babfcf941db8607bdaf82569ec38107dbb03c7f0b72604a0b346b6eb3298"}, - {file = "rpds_py-0.9.2-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1440c291db3f98a914e1afd9d6541e8fc60b4c3aab1a9008d03da4651e67386"}, - {file = "rpds_py-0.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0f2996fbac8e0b77fd67102becb9229986396e051f33dbceada3debaacc7033f"}, - {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f30d205755566a25f2ae0382944fcae2f350500ae4df4e795efa9e850821d82"}, - {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:159fba751a1e6b1c69244e23ba6c28f879a8758a3e992ed056d86d74a194a0f3"}, - {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1f044792e1adcea82468a72310c66a7f08728d72a244730d14880cd1dabe36b"}, - {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9251eb8aa82e6cf88510530b29eef4fac825a2b709baf5b94a6094894f252387"}, - {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01899794b654e616c8625b194ddd1e5b51ef5b60ed61baa7a2d9c2ad7b2a4238"}, - {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0c43f8ae8f6be1d605b0465671124aa8d6a0e40f1fb81dcea28b7e3d87ca1e1"}, - {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:207f57c402d1f8712618f737356e4b6f35253b6d20a324d9a47cb9f38ee43a6b"}, - {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b52e7c5ae35b00566d244ffefba0f46bb6bec749a50412acf42b1c3f402e2c90"}, - {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:978fa96dbb005d599ec4fd9ed301b1cc45f1a8f7982d4793faf20b404b56677d"}, - {file = "rpds_py-0.9.2-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6aa8326a4a608e1c28da191edd7c924dff445251b94653988efb059b16577a4d"}, - {file = "rpds_py-0.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aad51239bee6bff6823bbbdc8ad85136c6125542bbc609e035ab98ca1e32a192"}, - {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd4dc3602370679c2dfb818d9c97b1137d4dd412230cfecd3c66a1bf388a196"}, - {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd9da77c6ec1f258387957b754f0df60766ac23ed698b61941ba9acccd3284d1"}, - {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:190ca6f55042ea4649ed19c9093a9be9d63cd8a97880106747d7147f88a49d18"}, - {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:876bf9ed62323bc7dcfc261dbc5572c996ef26fe6406b0ff985cbcf460fc8a4c"}, - {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa2818759aba55df50592ecbc95ebcdc99917fa7b55cc6796235b04193eb3c55"}, - {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9ea4d00850ef1e917815e59b078ecb338f6a8efda23369677c54a5825dbebb55"}, - {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5855c85eb8b8a968a74dc7fb014c9166a05e7e7a8377fb91d78512900aadd13d"}, - {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:14c408e9d1a80dcb45c05a5149e5961aadb912fff42ca1dd9b68c0044904eb32"}, - {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:65a0583c43d9f22cb2130c7b110e695fff834fd5e832a776a107197e59a1898e"}, - {file = "rpds_py-0.9.2-cp38-none-win32.whl", hash = "sha256:71f2f7715935a61fa3e4ae91d91b67e571aeb5cb5d10331ab681256bda2ad920"}, - {file = "rpds_py-0.9.2-cp38-none-win_amd64.whl", hash = "sha256:674c704605092e3ebbbd13687b09c9f78c362a4bc710343efe37a91457123044"}, - {file = "rpds_py-0.9.2-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:07e2c54bef6838fa44c48dfbc8234e8e2466d851124b551fc4e07a1cfeb37260"}, - {file = "rpds_py-0.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fdf55283ad38c33e35e2855565361f4bf0abd02470b8ab28d499c663bc5d7c"}, - {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:890ba852c16ace6ed9f90e8670f2c1c178d96510a21b06d2fa12d8783a905193"}, - {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50025635ba8b629a86d9d5474e650da304cb46bbb4d18690532dd79341467846"}, - {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:517cbf6e67ae3623c5127206489d69eb2bdb27239a3c3cc559350ef52a3bbf0b"}, - {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0836d71ca19071090d524739420a61580f3f894618d10b666cf3d9a1688355b1"}, - {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c439fd54b2b9053717cca3de9583be6584b384d88d045f97d409f0ca867d80f"}, - {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f68996a3b3dc9335037f82754f9cdbe3a95db42bde571d8c3be26cc6245f2324"}, - {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7d68dc8acded354c972116f59b5eb2e5864432948e098c19fe6994926d8e15c3"}, - {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f963c6b1218b96db85fc37a9f0851eaf8b9040aa46dec112611697a7023da535"}, - {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a46859d7f947061b4010e554ccd1791467d1b1759f2dc2ec9055fa239f1bc26"}, - {file = "rpds_py-0.9.2-cp39-none-win32.whl", hash = "sha256:e07e5dbf8a83c66783a9fe2d4566968ea8c161199680e8ad38d53e075df5f0d0"}, - {file = "rpds_py-0.9.2-cp39-none-win_amd64.whl", hash = "sha256:682726178138ea45a0766907957b60f3a1bf3acdf212436be9733f28b6c5af3c"}, - {file = "rpds_py-0.9.2-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:196cb208825a8b9c8fc360dc0f87993b8b260038615230242bf18ec84447c08d"}, - {file = "rpds_py-0.9.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c7671d45530fcb6d5e22fd40c97e1e1e01965fc298cbda523bb640f3d923b387"}, - {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83b32f0940adec65099f3b1c215ef7f1d025d13ff947975a055989cb7fd019a4"}, - {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f67da97f5b9eac838b6980fc6da268622e91f8960e083a34533ca710bec8611"}, - {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03975db5f103997904c37e804e5f340c8fdabbb5883f26ee50a255d664eed58c"}, - {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:987b06d1cdb28f88a42e4fb8a87f094e43f3c435ed8e486533aea0bf2e53d931"}, - {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c861a7e4aef15ff91233751619ce3a3d2b9e5877e0fcd76f9ea4f6847183aa16"}, - {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02938432352359805b6da099c9c95c8a0547fe4b274ce8f1a91677401bb9a45f"}, - {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ef1f08f2a924837e112cba2953e15aacfccbbfcd773b4b9b4723f8f2ddded08e"}, - {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:35da5cc5cb37c04c4ee03128ad59b8c3941a1e5cd398d78c37f716f32a9b7f67"}, - {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:141acb9d4ccc04e704e5992d35472f78c35af047fa0cfae2923835d153f091be"}, - {file = "rpds_py-0.9.2-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79f594919d2c1a0cc17d1988a6adaf9a2f000d2e1048f71f298b056b1018e872"}, - {file = "rpds_py-0.9.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:a06418fe1155e72e16dddc68bb3780ae44cebb2912fbd8bb6ff9161de56e1798"}, - {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b2eb034c94b0b96d5eddb290b7b5198460e2d5d0c421751713953a9c4e47d10"}, - {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b08605d248b974eb02f40bdcd1a35d3924c83a2a5e8f5d0fa5af852c4d960af"}, - {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0805911caedfe2736935250be5008b261f10a729a303f676d3d5fea6900c96a"}, - {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab2299e3f92aa5417d5e16bb45bb4586171c1327568f638e8453c9f8d9e0f020"}, - {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c8d7594e38cf98d8a7df25b440f684b510cf4627fe038c297a87496d10a174f"}, - {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b9ec12ad5f0a4625db34db7e0005be2632c1013b253a4a60e8302ad4d462afd"}, - {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1fcdee18fea97238ed17ab6478c66b2095e4ae7177e35fb71fbe561a27adf620"}, - {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:933a7d5cd4b84f959aedeb84f2030f0a01d63ae6cf256629af3081cf3e3426e8"}, - {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:686ba516e02db6d6f8c279d1641f7067ebb5dc58b1d0536c4aaebb7bf01cdc5d"}, - {file = "rpds_py-0.9.2-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:0173c0444bec0a3d7d848eaeca2d8bd32a1b43f3d3fde6617aac3731fa4be05f"}, - {file = "rpds_py-0.9.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d576c3ef8c7b2d560e301eb33891d1944d965a4d7a2eacb6332eee8a71827db6"}, - {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed89861ee8c8c47d6beb742a602f912b1bb64f598b1e2f3d758948721d44d468"}, - {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1054a08e818f8e18910f1bee731583fe8f899b0a0a5044c6e680ceea34f93876"}, - {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99e7c4bb27ff1aab90dcc3e9d37ee5af0231ed98d99cb6f5250de28889a3d502"}, - {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c545d9d14d47be716495076b659db179206e3fd997769bc01e2d550eeb685596"}, - {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9039a11bca3c41be5a58282ed81ae422fa680409022b996032a43badef2a3752"}, - {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fb39aca7a64ad0c9490adfa719dbeeb87d13be137ca189d2564e596f8ba32c07"}, - {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2d8b3b3a2ce0eaa00c5bbbb60b6713e94e7e0becab7b3db6c5c77f979e8ed1f1"}, - {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:99b1c16f732b3a9971406fbfe18468592c5a3529585a45a35adbc1389a529a03"}, - {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c27ee01a6c3223025f4badd533bea5e87c988cb0ba2811b690395dfe16088cfe"}, - {file = "rpds_py-0.9.2.tar.gz", hash = "sha256:8d70e8f14900f2657c249ea4def963bed86a29b81f81f5b76b5a9215680de945"}, -] - [[package]] name = "rsa" version = "4.9" @@ -3426,24 +3253,24 @@ jeepney = ">=0.6" [[package]] name = "semver" -version = "3.0.1" +version = "3.0.2" description = "Python helper for Semantic Versioning (https://semver.org)" optional = false python-versions = ">=3.7" files = [ - {file = "semver-3.0.1-py3-none-any.whl", hash = "sha256:2a23844ba1647362c7490fe3995a86e097bb590d16f0f32dfc383008f19e4cdf"}, - {file = "semver-3.0.1.tar.gz", hash = "sha256:9ec78c5447883c67b97f98c3b6212796708191d22e4ad30f4570f840171cbce1"}, + {file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"}, + {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, ] [[package]] name = "sentry-sdk" -version = "1.29.2" +version = "1.39.1" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = "*" files = [ - {file = "sentry-sdk-1.29.2.tar.gz", hash = "sha256:a99ee105384788c3f228726a88baf515fe7b5f1d2d0f215a03d194369f158df7"}, - {file = "sentry_sdk-1.29.2-py2.py3-none-any.whl", hash = "sha256:3e17215d8006612e2df02b0e73115eb8376c37e3f586d8436fa41644e605074d"}, + {file = "sentry-sdk-1.39.1.tar.gz", hash = "sha256:320a55cdf9da9097a0bead239c35b7e61f53660ef9878861824fd6d9b2eaf3b5"}, + {file = "sentry_sdk-1.39.1-py2.py3-none-any.whl", hash = "sha256:81b5b9ffdd1a374e9eb0c053b5d2012155db9cbe76393a8585677b753bd5fdc1"}, ] [package.dependencies] @@ -3453,10 +3280,12 @@ urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""} [package.extras] aiohttp = ["aiohttp (>=3.5)"] arq = ["arq (>=0.23)"] +asyncpg = ["asyncpg (>=0.23)"] beam = ["apache-beam (>=2.12)"] bottle = ["bottle (>=0.12.13)"] celery = ["celery (>=3)"] chalice = ["chalice (>=1.16.0)"] +clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] django = ["django (>=1.8)"] falcon = ["falcon (>=1.4)"] fastapi = ["fastapi (>=0.79.0)"] @@ -3466,6 +3295,7 @@ httpx = ["httpx (>=0.16.0)"] huey = ["huey (>=2)"] loguru = ["loguru (>=0.5)"] opentelemetry = ["opentelemetry-distro (>=0.35b0)"] +opentelemetry-experimental = ["opentelemetry-distro (>=0.40b0,<1.0)", "opentelemetry-instrumentation-aiohttp-client (>=0.40b0,<1.0)", "opentelemetry-instrumentation-django (>=0.40b0,<1.0)", "opentelemetry-instrumentation-fastapi (>=0.40b0,<1.0)", "opentelemetry-instrumentation-flask (>=0.40b0,<1.0)", "opentelemetry-instrumentation-requests (>=0.40b0,<1.0)", "opentelemetry-instrumentation-sqlite3 (>=0.40b0,<1.0)", "opentelemetry-instrumentation-urllib (>=0.40b0,<1.0)"] pure-eval = ["asttokens", "executing", "pure-eval"] pymongo = ["pymongo (>=3.1)"] pyspark = ["pyspark (>=2.4.4)"] @@ -3479,29 +3309,29 @@ tornado = ["tornado (>=5)"] [[package]] name = "setuptools" -version = "68.0.0" +version = "69.0.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, - {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "shellingham" -version = "1.5.0.post1" +version = "1.5.4" description = "Tool to Detect Surrounding Shell" optional = false python-versions = ">=3.7" files = [ - {file = "shellingham-1.5.0.post1-py2.py3-none-any.whl", hash = "sha256:368bf8c00754fd4f55afb7bbb86e272df77e4dc76ac29dbcbb81a59e9fc15744"}, - {file = "shellingham-1.5.0.post1.tar.gz", hash = "sha256:823bc5fb5c34d60f285b624e7264f4dda254bc803a3774a147bf99c0e3004a28"}, + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] [[package]] @@ -3517,19 +3347,15 @@ files = [ [[package]] name = "slack-sdk" -version = "3.21.3" +version = "3.26.2" description = "The Slack API Platform SDK for Python" optional = false -python-versions = ">=3.6.0" +python-versions = ">=3.6" files = [ - {file = "slack_sdk-3.21.3-py2.py3-none-any.whl", hash = "sha256:de3c07b92479940b61cd68c566f49fbc9974c8f38f661d26244078f3903bb9cc"}, - {file = "slack_sdk-3.21.3.tar.gz", hash = "sha256:20829bdc1a423ec93dac903470975ebf3bc76fd3fd91a4dadc0eeffc940ecb0c"}, + {file = "slack_sdk-3.26.2-py2.py3-none-any.whl", hash = "sha256:a10e8ee69ca17d274989d0c2bbecb875f19898da3052d8d57de0898a00b1ab52"}, + {file = "slack_sdk-3.26.2.tar.gz", hash = "sha256:bcdac5e688fa50e9357ecd00b803b6a8bad766aa614d35d8dc0636f40adc48bf"}, ] -[package.extras] -optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=10,<11)"] -testing = ["Flask (>=1,<2)", "Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "Werkzeug (<2)", "black (==22.8.0)", "boto3 (<=2)", "click (==8.0.4)", "databases (>=0.5)", "flake8 (>=5,<6)", "itsdangerous (==1.1.0)", "moto (>=3,<4)", "psutil (>=5,<6)", "pytest (>=6.2.5,<7)", "pytest-asyncio (<1)", "pytest-cov (>=2,<3)"] - [[package]] name = "sniffio" version = "1.3.0" @@ -3543,72 +3369,81 @@ files = [ [[package]] name = "soupsieve" -version = "2.4.1" +version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, - {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, ] [[package]] name = "sqlalchemy" -version = "2.0.19" +version = "2.0.25" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9deaae357edc2091a9ed5d25e9ee8bba98bcfae454b3911adeaf159c2e9ca9e3"}, - {file = "SQLAlchemy-2.0.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bf0fd65b50a330261ec7fe3d091dfc1c577483c96a9fa1e4323e932961aa1b5"}, - {file = "SQLAlchemy-2.0.19-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d90ccc15ba1baa345796a8fb1965223ca7ded2d235ccbef80a47b85cea2d71a"}, - {file = "SQLAlchemy-2.0.19-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4e688f6784427e5f9479d1a13617f573de8f7d4aa713ba82813bcd16e259d1"}, - {file = "SQLAlchemy-2.0.19-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:584f66e5e1979a7a00f4935015840be627e31ca29ad13f49a6e51e97a3fb8cae"}, - {file = "SQLAlchemy-2.0.19-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c69ce70047b801d2aba3e5ff3cba32014558966109fecab0c39d16c18510f15"}, - {file = "SQLAlchemy-2.0.19-cp310-cp310-win32.whl", hash = "sha256:96f0463573469579d32ad0c91929548d78314ef95c210a8115346271beeeaaa2"}, - {file = "SQLAlchemy-2.0.19-cp310-cp310-win_amd64.whl", hash = "sha256:22bafb1da60c24514c141a7ff852b52f9f573fb933b1e6b5263f0daa28ce6db9"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6894708eeb81f6d8193e996257223b6bb4041cb05a17cd5cf373ed836ef87a2"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8f2afd1aafded7362b397581772c670f20ea84d0a780b93a1a1529da7c3d369"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15afbf5aa76f2241184c1d3b61af1a72ba31ce4161013d7cb5c4c2fca04fd6e"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc05b59142445a4efb9c1fd75c334b431d35c304b0e33f4fa0ff1ea4890f92e"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5831138f0cc06b43edf5f99541c64adf0ab0d41f9a4471fd63b54ae18399e4de"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3afa8a21a9046917b3a12ffe016ba7ebe7a55a6fc0c7d950beb303c735c3c3ad"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-win32.whl", hash = "sha256:c896d4e6ab2eba2afa1d56be3d0b936c56d4666e789bfc59d6ae76e9fcf46145"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-win_amd64.whl", hash = "sha256:024d2f67fb3ec697555e48caeb7147cfe2c08065a4f1a52d93c3d44fc8e6ad1c"}, - {file = "SQLAlchemy-2.0.19-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:89bc2b374ebee1a02fd2eae6fd0570b5ad897ee514e0f84c5c137c942772aa0c"}, - {file = "SQLAlchemy-2.0.19-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd4d410a76c3762511ae075d50f379ae09551d92525aa5bb307f8343bf7c2c12"}, - {file = "SQLAlchemy-2.0.19-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f469f15068cd8351826df4080ffe4cc6377c5bf7d29b5a07b0e717dddb4c7ea2"}, - {file = "SQLAlchemy-2.0.19-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cda283700c984e699e8ef0fcc5c61f00c9d14b6f65a4f2767c97242513fcdd84"}, - {file = "SQLAlchemy-2.0.19-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:43699eb3f80920cc39a380c159ae21c8a8924fe071bccb68fc509e099420b148"}, - {file = "SQLAlchemy-2.0.19-cp37-cp37m-win32.whl", hash = "sha256:61ada5831db36d897e28eb95f0f81814525e0d7927fb51145526c4e63174920b"}, - {file = "SQLAlchemy-2.0.19-cp37-cp37m-win_amd64.whl", hash = "sha256:57d100a421d9ab4874f51285c059003292433c648df6abe6c9c904e5bd5b0828"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16a310f5bc75a5b2ce7cb656d0e76eb13440b8354f927ff15cbaddd2523ee2d1"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cf7b5e3856cbf1876da4e9d9715546fa26b6e0ba1a682d5ed2fc3ca4c7c3ec5b"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e7b69d9ced4b53310a87117824b23c509c6fc1f692aa7272d47561347e133b6"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9eb4575bfa5afc4b066528302bf12083da3175f71b64a43a7c0badda2be365"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6b54d1ad7a162857bb7c8ef689049c7cd9eae2f38864fc096d62ae10bc100c7d"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5d6afc41ca0ecf373366fd8e10aee2797128d3ae45eb8467b19da4899bcd1ee0"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-win32.whl", hash = "sha256:430614f18443b58ceb9dedec323ecddc0abb2b34e79d03503b5a7579cd73a531"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-win_amd64.whl", hash = "sha256:eb60699de43ba1a1f77363f563bb2c652f7748127ba3a774f7cf2c7804aa0d3d"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a752b7a9aceb0ba173955d4f780c64ee15a1a991f1c52d307d6215c6c73b3a4c"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7351c05db355da112e056a7b731253cbeffab9dfdb3be1e895368513c7d70106"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa51ce4aea583b0c6b426f4b0563d3535c1c75986c4373a0987d84d22376585b"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae7473a67cd82a41decfea58c0eac581209a0aa30f8bc9190926fbf628bb17f7"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:851a37898a8a39783aab603c7348eb5b20d83c76a14766a43f56e6ad422d1ec8"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539010665c90e60c4a1650afe4ab49ca100c74e6aef882466f1de6471d414be7"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-win32.whl", hash = "sha256:f82c310ddf97b04e1392c33cf9a70909e0ae10a7e2ddc1d64495e3abdc5d19fb"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-win_amd64.whl", hash = "sha256:8e712cfd2e07b801bc6b60fdf64853bc2bd0af33ca8fa46166a23fe11ce0dbb0"}, - {file = "SQLAlchemy-2.0.19-py3-none-any.whl", hash = "sha256:314145c1389b021a9ad5aa3a18bac6f5d939f9087d7fc5443be28cba19d2c972"}, - {file = "SQLAlchemy-2.0.19.tar.gz", hash = "sha256:77a14fa20264af73ddcdb1e2b9c5a829b8cc6b8304d0f093271980e36c200a3f"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84daa0a2055df9ca0f148a64fdde12ac635e30edbca80e87df9b3aaf419e144a"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5693145220517b5f42393e07a6898acdfe820e136c98663b971906120549da5"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-win32.whl", hash = "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-win_amd64.whl", hash = "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa9373708763ef46782d10e950b49d0235bfe58facebd76917d3f5cbf5971aed"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75432b5b14dc2fff43c50435e248b45c7cdadef73388e5610852b95280ffd0e9"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-win32.whl", hash = "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a21853f5daeb50412d459cfb13cb82c089ad4c04ec208cd14dddd99fc23b39"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b64b183d610b424a160b0d4d880995e935208fc043d0302dd29fee32d1ee3f95"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-win32.whl", hash = "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-win_amd64.whl", hash = "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bb209a73b8307f8fe4fe46f6ad5979649be01607f11af1eb94aa9e8a3aaf77f0"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798f717ae7c806d67145f6ae94dc7c342d3222d3b9a311a784f371a4333212c7"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd402169aa00df3142149940b3bf9ce7dde075928c1886d9a1df63d4b8de62"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0d3cab3076af2e4aa5693f89622bef7fa770c6fec967143e4da7508b3dceb9b9"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:74b080c897563f81062b74e44f5a72fa44c2b373741a9ade701d5f789a10ba23"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-win32.whl", hash = "sha256:87d91043ea0dc65ee583026cb18e1b458d8ec5fc0a93637126b5fc0bc3ea68c4"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-win_amd64.whl", hash = "sha256:75f99202324383d613ddd1f7455ac908dca9c2dd729ec8584c9541dd41822a2c"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3be4987e3ee9d9a380b66393b77a4cd6d742480c951a1c56a23c335caca4ce3"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8b8cb63d3ea63b29074dcd29da4dc6a97ad1349151f2d2949495418fd6e48db9"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-win32.whl", hash = "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-win_amd64.whl", hash = "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecf6d4cda1f9f6cb0b45803a01ea7f034e2f1aed9475e883410812d9f9e3cfcf"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:790f533fa5c8901a62b6fef5811d48980adeb2f51f1290ade8b5e7ba990ba3de"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-win32.whl", hash = "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-win_amd64.whl", hash = "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7"}, + {file = "SQLAlchemy-2.0.25-py3-none-any.whl", hash = "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3"}, + {file = "SQLAlchemy-2.0.25.tar.gz", hash = "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08"}, ] [package.dependencies] greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} -typing-extensions = ">=4.2.0" +typing-extensions = ">=4.6.0" [package.extras] -aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] @@ -3618,7 +3453,7 @@ mssql-pyodbc = ["pyodbc"] mypy = ["mypy (>=0.910)"] mysql = ["mysqlclient (>=1.4.0)"] mysql-connector = ["mysql-connector-python"] -oracle = ["cx-oracle (>=7)"] +oracle = ["cx_oracle (>=8)"] oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] @@ -3628,17 +3463,17 @@ postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2cffi = ["psycopg2cffi"] postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3-binary"] +sqlcipher = ["sqlcipher3_binary"] [[package]] name = "starlette" -version = "0.31.0" +version = "0.34.0" description = "The little ASGI library that shines." optional = false python-versions = ">=3.8" files = [ - {file = "starlette-0.31.0-py3-none-any.whl", hash = "sha256:1aab7e04bcbafbb1867c1ce62f6b21c60a6e3cecb5a08dcee8abac7457fbcfbf"}, - {file = "starlette-0.31.0.tar.gz", hash = "sha256:7df0a3d8fa2c027d641506204ef69239d19bf9406ad2e77b319926e476ac3042"}, + {file = "starlette-0.34.0-py3-none-any.whl", hash = "sha256:2e14ee943f2df59eb8c141326240ce601643f1a97b577db44634f6d05d368c37"}, + {file = "starlette-0.34.0.tar.gz", hash = "sha256:ed050aaf3896945bfaae93bdf337e53ef3f29115a9d9c153e402985115cd9c8e"}, ] [package.dependencies] @@ -3675,13 +3510,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.1" +version = "0.12.3" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"}, - {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, + {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, + {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, ] [[package]] @@ -3697,33 +3532,33 @@ files = [ [[package]] name = "tqdm" -version = "4.65.0" +version = "4.66.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"}, - {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"}, + {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, + {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["py-make (>=0.1.0)", "twine", "wheel"] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] [[package]] name = "trove-classifiers" -version = "2023.7.6" +version = "2024.1.8" description = "Canonical source for classifiers on PyPI (pypi.org)." optional = false python-versions = "*" files = [ - {file = "trove-classifiers-2023.7.6.tar.gz", hash = "sha256:8a8e168b51d20fed607043831d37632bb50919d1c80a64e0f1393744691a8b22"}, - {file = "trove_classifiers-2023.7.6-py3-none-any.whl", hash = "sha256:b420d5aa048ee7c456233a49203f7d58d1736af4a6cde637657d78c13ab7969b"}, + {file = "trove-classifiers-2024.1.8.tar.gz", hash = "sha256:6e36caf430ff6485c4b57a4c6b364a13f6a898d16b9417c6c37467e59c14b05a"}, + {file = "trove_classifiers-2024.1.8-py3-none-any.whl", hash = "sha256:3c1ff4deb10149c7e39ede6e5bbc107def64362ef1ee7590ec98d71fb92f1b6a"}, ] [[package]] @@ -3752,32 +3587,32 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6. [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] [[package]] name = "universal-pathlib" -version = "0.0.24" +version = "0.1.4" description = "pathlib api extended to use fsspec backends" optional = false python-versions = ">=3.8" files = [ - {file = "universal_pathlib-0.0.24-py3-none-any.whl", hash = "sha256:a2e907b11b1b3f6e982275e5ac0c58a4d34dba2b9e703ecbe2040afa572c741b"}, - {file = "universal_pathlib-0.0.24.tar.gz", hash = "sha256:fcbffb95e4bc69f704af5dde4f9a624b2269f251a38c81ab8bec19dfeaad830f"}, + {file = "universal_pathlib-0.1.4-py3-none-any.whl", hash = "sha256:f99186cf950bde1262de9a590bb019613ef84f9fabd9f276e8b019722201943a"}, + {file = "universal_pathlib-0.1.4.tar.gz", hash = "sha256:82e5d86d16a27e0ea1adc7d88acbcba9d02d5a45488163174f96d9ac289db2e4"}, ] [package.dependencies] -fsspec = "*" +fsspec = ">=2022.1.0" [package.extras] -dev = ["adlfs", "aiohttp", "cheroot", "gcsfs", "hadoop-test-cluster", "moto[s3,server]", "mypy (==1.3.0)", "pyarrow", "pylint (==2.17.4)", "pytest (==7.3.2)", "pytest-cov (==4.1.0)", "pytest-mock (==3.11.1)", "pytest-sugar (==0.9.6)", "requests", "s3fs", "webdav4[fsspec]", "wsgidav"] -tests = ["mypy (==1.3.0)", "pylint (==2.17.4)", "pytest (==7.3.2)", "pytest-cov (==4.1.0)", "pytest-mock (==3.11.1)", "pytest-sugar (==0.9.6)"] +dev = ["adlfs", "aiohttp", "cheroot", "gcsfs", "hadoop-test-cluster", "moto[s3,server]", "mypy (==1.3.0)", "packaging", "pyarrow", "pydantic", "pydantic-settings", "pylint (==2.17.4)", "pytest (==7.3.2)", "pytest-cov (==4.1.0)", "pytest-mock (==3.11.1)", "pytest-sugar (==0.9.6)", "requests", "s3fs", "webdav4[fsspec]", "wsgidav"] +tests = ["mypy (==1.3.0)", "packaging", "pylint (==2.17.4)", "pytest (==7.3.2)", "pytest-cov (==4.1.0)", "pytest-mock (==3.11.1)", "pytest-sugar (==0.9.6)"] [[package]] name = "uritemplate" @@ -3792,29 +3627,29 @@ files = [ [[package]] name = "urllib3" -version = "1.26.16" +version = "2.1.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.8" files = [ - {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"}, - {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"}, + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.23.2" +version = "0.25.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"}, - {file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"}, + {file = "uvicorn-0.25.0-py3-none-any.whl", hash = "sha256:ce107f5d9bd02b4636001a77a4e74aab5e1e2b146868ebbad565237145af444c"}, + {file = "uvicorn-0.25.0.tar.gz", hash = "sha256:6dddbad1d7ee0f5140aba5ec138ddc9612c5109399903828b4874c9937f009c2"}, ] [package.dependencies] @@ -3834,66 +3669,66 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", [[package]] name = "uvloop" -version = "0.17.0" +version = "0.19.0" description = "Fast implementation of asyncio event loop on top of libuv" optional = false -python-versions = ">=3.7" -files = [ - {file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce9f61938d7155f79d3cb2ffa663147d4a76d16e08f65e2c66b77bd41b356718"}, - {file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:68532f4349fd3900b839f588972b3392ee56042e440dd5873dfbbcd2cc67617c"}, - {file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0949caf774b9fcefc7c5756bacbbbd3fc4c05a6b7eebc7c7ad6f825b23998d6d"}, - {file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff3d00b70ce95adce264462c930fbaecb29718ba6563db354608f37e49e09024"}, - {file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a5abddb3558d3f0a78949c750644a67be31e47936042d4f6c888dd6f3c95f4aa"}, - {file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8efcadc5a0003d3a6e887ccc1fb44dec25594f117a94e3127954c05cf144d811"}, - {file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3378eb62c63bf336ae2070599e49089005771cc651c8769aaad72d1bd9385a7c"}, - {file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6aafa5a78b9e62493539456f8b646f85abc7093dd997f4976bb105537cf2635e"}, - {file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c686a47d57ca910a2572fddfe9912819880b8765e2f01dc0dd12a9bf8573e539"}, - {file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:864e1197139d651a76c81757db5eb199db8866e13acb0dfe96e6fc5d1cf45fc4"}, - {file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2a6149e1defac0faf505406259561bc14b034cdf1d4711a3ddcdfbaa8d825a05"}, - {file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6708f30db9117f115eadc4f125c2a10c1a50d711461699a0cbfaa45b9a78e376"}, - {file = "uvloop-0.17.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:23609ca361a7fc587031429fa25ad2ed7242941adec948f9d10c045bfecab06b"}, - {file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2deae0b0fb00a6af41fe60a675cec079615b01d68beb4cc7b722424406b126a8"}, - {file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45cea33b208971e87a31c17622e4b440cac231766ec11e5d22c76fab3bf9df62"}, - {file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9b09e0f0ac29eee0451d71798878eae5a4e6a91aa275e114037b27f7db72702d"}, - {file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dbbaf9da2ee98ee2531e0c780455f2841e4675ff580ecf93fe5c48fe733b5667"}, - {file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a4aee22ece20958888eedbad20e4dbb03c37533e010fb824161b4f05e641f738"}, - {file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:307958f9fc5c8bb01fad752d1345168c0abc5d62c1b72a4a8c6c06f042b45b20"}, - {file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ebeeec6a6641d0adb2ea71dcfb76017602ee2bfd8213e3fcc18d8f699c5104f"}, - {file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1436c8673c1563422213ac6907789ecb2b070f5939b9cbff9ef7113f2b531595"}, - {file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8887d675a64cfc59f4ecd34382e5b4f0ef4ae1da37ed665adba0c2badf0d6578"}, - {file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3db8de10ed684995a7f34a001f15b374c230f7655ae840964d51496e2f8a8474"}, - {file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d37dccc7ae63e61f7b96ee2e19c40f153ba6ce730d8ba4d3b4e9738c1dccc1b"}, - {file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbbe908fda687e39afd6ea2a2f14c2c3e43f2ca88e3a11964b297822358d0e6c"}, - {file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d97672dc709fa4447ab83276f344a165075fd9f366a97b712bdd3fee05efae8"}, - {file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1e507c9ee39c61bfddd79714e4f85900656db1aec4d40c6de55648e85c2799c"}, - {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c092a2c1e736086d59ac8e41f9c98f26bbf9b9222a76f21af9dfe949b99b2eb9"}, - {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:30babd84706115626ea78ea5dbc7dd8d0d01a2e9f9b306d24ca4ed5796c66ded"}, - {file = "uvloop-0.17.0.tar.gz", hash = "sha256:0ddf6baf9cf11a1a22c71487f39f15b2cf78eb5bde7e5b45fbb99e8a9d91b9e1"}, +python-versions = ">=3.8.0" +files = [ + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, + {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, ] [package.extras] -dev = ["Cython (>=0.29.32,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=22.0.0,<22.1.0)", "pycodestyle (>=2.7.0,<2.8.0)", "pytest (>=3.6.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=22.0.0,<22.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"] +test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] [[package]] name = "virtualenv" -version = "20.24.2" +version = "20.25.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.2-py3-none-any.whl", hash = "sha256:43a3052be36080548bdee0b42919c88072037d50d56c28bd3f853cbe92b953ff"}, - {file = "virtualenv-20.24.2.tar.gz", hash = "sha256:fd8a78f46f6b99a67b7ec5cf73f92357891a7b3a40fd97637c27f854aae3b9e0"}, + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<4" +platformdirs = ">=3.9.1,<5" [package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] @@ -3937,33 +3772,86 @@ watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "watchfiles" -version = "0.19.0" +version = "0.21.0" description = "Simple, modern and high performance file watching and code reload in python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "watchfiles-0.19.0-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:91633e64712df3051ca454ca7d1b976baf842d7a3640b87622b323c55f3345e7"}, - {file = "watchfiles-0.19.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b6577b8c6c8701ba8642ea9335a129836347894b666dd1ec2226830e263909d3"}, - {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:18b28f6ad871b82df9542ff958d0c86bb0d8310bb09eb8e87d97318a3b5273af"}, - {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fac19dc9cbc34052394dbe81e149411a62e71999c0a19e1e09ce537867f95ae0"}, - {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09ea3397aecbc81c19ed7f025e051a7387feefdb789cf768ff994c1228182fda"}, - {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c0376deac92377817e4fb8f347bf559b7d44ff556d9bc6f6208dd3f79f104aaf"}, - {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c75eff897786ee262c9f17a48886f4e98e6cfd335e011c591c305e5d083c056"}, - {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb5d45c4143c1dd60f98a16187fd123eda7248f84ef22244818c18d531a249d1"}, - {file = "watchfiles-0.19.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:79c533ff593db861ae23436541f481ec896ee3da4e5db8962429b441bbaae16e"}, - {file = "watchfiles-0.19.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3d7d267d27aceeeaa3de0dd161a0d64f0a282264d592e335fff7958cc0cbae7c"}, - {file = "watchfiles-0.19.0-cp37-abi3-win32.whl", hash = "sha256:176a9a7641ec2c97b24455135d58012a5be5c6217fc4d5fef0b2b9f75dbf5154"}, - {file = "watchfiles-0.19.0-cp37-abi3-win_amd64.whl", hash = "sha256:945be0baa3e2440151eb3718fd8846751e8b51d8de7b884c90b17d271d34cae8"}, - {file = "watchfiles-0.19.0-cp37-abi3-win_arm64.whl", hash = "sha256:0089c6dc24d436b373c3c57657bf4f9a453b13767150d17284fc6162b2791911"}, - {file = "watchfiles-0.19.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cae3dde0b4b2078f31527acff6f486e23abed307ba4d3932466ba7cdd5ecec79"}, - {file = "watchfiles-0.19.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f3920b1285a7d3ce898e303d84791b7bf40d57b7695ad549dc04e6a44c9f120"}, - {file = "watchfiles-0.19.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9afd0d69429172c796164fd7fe8e821ade9be983f51c659a38da3faaaaac44dc"}, - {file = "watchfiles-0.19.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68dce92b29575dda0f8d30c11742a8e2b9b8ec768ae414b54f7453f27bdf9545"}, - {file = "watchfiles-0.19.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:5569fc7f967429d4bc87e355cdfdcee6aabe4b620801e2cf5805ea245c06097c"}, - {file = "watchfiles-0.19.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5471582658ea56fca122c0f0d0116a36807c63fefd6fdc92c71ca9a4491b6b48"}, - {file = "watchfiles-0.19.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b538014a87f94d92f98f34d3e6d2635478e6be6423a9ea53e4dd96210065e193"}, - {file = "watchfiles-0.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20b44221764955b1e703f012c74015306fb7e79a00c15370785f309b1ed9aa8d"}, - {file = "watchfiles-0.19.0.tar.gz", hash = "sha256:d9b073073e048081e502b6c6b0b88714c026a1a4c890569238d04aca5f9ca74b"}, + {file = "watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa"}, + {file = "watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c"}, + {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9"}, + {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9"}, + {file = "watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293"}, + {file = "watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235"}, + {file = "watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7"}, + {file = "watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7"}, + {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0"}, + {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365"}, + {file = "watchfiles-0.21.0-cp311-none-win32.whl", hash = "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400"}, + {file = "watchfiles-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe"}, + {file = "watchfiles-0.21.0-cp311-none-win_arm64.whl", hash = "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078"}, + {file = "watchfiles-0.21.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a"}, + {file = "watchfiles-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c"}, + {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235"}, + {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7"}, + {file = "watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3"}, + {file = "watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094"}, + {file = "watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6"}, + {file = "watchfiles-0.21.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99"}, + {file = "watchfiles-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562"}, + {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19"}, + {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0"}, + {file = "watchfiles-0.21.0-cp38-none-win32.whl", hash = "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214"}, + {file = "watchfiles-0.21.0-cp38-none-win_amd64.whl", hash = "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca"}, + {file = "watchfiles-0.21.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e"}, + {file = "watchfiles-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28"}, + {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6"}, + {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49"}, + {file = "watchfiles-0.21.0-cp39-none-win32.whl", hash = "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94"}, + {file = "watchfiles-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097"}, + {file = "watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3"}, ] [package.dependencies] @@ -3971,197 +3859,190 @@ anyio = ">=3.0.0" [[package]] name = "wcwidth" -version = "0.2.6" +version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" files = [ - {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, - {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, -] - -[[package]] -name = "webencodings" -version = "0.5.1" -description = "Character encoding aliases for legacy web content" -optional = false -python-versions = "*" -files = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] [[package]] name = "websockets" -version = "11.0.3" +version = "12.0" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac"}, - {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d"}, - {file = "websockets-11.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f"}, - {file = "websockets-11.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564"}, - {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11"}, - {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca"}, - {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54"}, - {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4"}, - {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526"}, - {file = "websockets-11.0.3-cp310-cp310-win32.whl", hash = "sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69"}, - {file = "websockets-11.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f"}, - {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb"}, - {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288"}, - {file = "websockets-11.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d"}, - {file = "websockets-11.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3"}, - {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b"}, - {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6"}, - {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97"}, - {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf"}, - {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd"}, - {file = "websockets-11.0.3-cp311-cp311-win32.whl", hash = "sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c"}, - {file = "websockets-11.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8"}, - {file = "websockets-11.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152"}, - {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f"}, - {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b"}, - {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb"}, - {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007"}, - {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0"}, - {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af"}, - {file = "websockets-11.0.3-cp37-cp37m-win32.whl", hash = "sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f"}, - {file = "websockets-11.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de"}, - {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0"}, - {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae"}, - {file = "websockets-11.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99"}, - {file = "websockets-11.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa"}, - {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86"}, - {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c"}, - {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0"}, - {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e"}, - {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788"}, - {file = "websockets-11.0.3-cp38-cp38-win32.whl", hash = "sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74"}, - {file = "websockets-11.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f"}, - {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8"}, - {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd"}, - {file = "websockets-11.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016"}, - {file = "websockets-11.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61"}, - {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b"}, - {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd"}, - {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7"}, - {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1"}, - {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311"}, - {file = "websockets-11.0.3-cp39-cp39-win32.whl", hash = "sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128"}, - {file = "websockets-11.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602"}, - {file = "websockets-11.0.3-py3-none-any.whl", hash = "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6"}, - {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"}, + {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, + {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, + {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, + {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, + {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, + {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, + {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, + {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, + {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, + {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, + {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, + {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, + {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, + {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, + {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, + {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, + {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, + {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, + {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, + {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, + {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, + {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, + {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, + {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, + {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, ] [[package]] name = "wmctrl" -version = "0.4" +version = "0.5" description = "A tool to programmatically control windows inside X" optional = false -python-versions = "*" +python-versions = ">=2.7" files = [ - {file = "wmctrl-0.4.tar.gz", hash = "sha256:66cbff72b0ca06a22ec3883ac3a4d7c41078bdae4fb7310f52951769b10e14e0"}, + {file = "wmctrl-0.5-py2.py3-none-any.whl", hash = "sha256:ae695c1863a314c899e7cf113f07c0da02a394b968c4772e1936219d9234ddd7"}, + {file = "wmctrl-0.5.tar.gz", hash = "sha256:7839a36b6fe9e2d6fd22304e5dc372dbced2116ba41283ea938b2da57f53e962"}, ] +[package.dependencies] +attrs = "*" + +[package.extras] +test = ["pytest"] + [[package]] name = "wrapt" -version = "1.15.0" +version = "1.16.0" description = "Module for decorators, wrappers and monkey patching." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, - {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, - {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, - {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, - {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, - {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, - {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, - {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, - {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, - {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, - {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, - {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, - {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, - {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, - {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, - {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, - {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, - {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, - {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] [[package]] @@ -4250,85 +4131,101 @@ cffi = ">=1.0" [[package]] name = "yarl" -version = "1.9.2" +version = "1.9.4" description = "Yet another URL library" optional = false python-versions = ">=3.7" files = [ - {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, - {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, - {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"}, - {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"}, - {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"}, - {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"}, - {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"}, - {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"}, - {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"}, - {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"}, - {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"}, - {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"}, - {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"}, - {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, - {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, + {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, + {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, + {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, + {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, + {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, + {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, ] [package.dependencies] @@ -4337,20 +4234,20 @@ multidict = ">=4.0" [[package]] name = "zipp" -version = "3.16.2" +version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, - {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [metadata] lock-version = "2.0" -python-versions = "^3.9" -content-hash = "8740e04661a4f3926650d1e905870688781b697a13851ab923817b705b7812fc" +python-versions = "^3.9, <3.13" +content-hash = "bb5bbfdca5cf2dd2c8040275e5ae8ff9ec78719f2aad3bdddb0f652b9f2bd893" diff --git a/airbyte-ci/connectors/metadata_service/orchestrator/pyproject.toml b/airbyte-ci/connectors/metadata_service/orchestrator/pyproject.toml index 1b764780b4b17..755dffa05469c 100644 --- a/airbyte-ci/connectors/metadata_service/orchestrator/pyproject.toml +++ b/airbyte-ci/connectors/metadata_service/orchestrator/pyproject.toml @@ -7,11 +7,11 @@ readme = "README.md" packages = [{include = "orchestrator"}] [tool.poetry.dependencies] -python = "^3.9" # This is set to 3.9 as currently there is an issue when deploying via dagster-cloud where a dependency does not have a prebuild wheel file for 3.10 -dagit = "^1.4.1" -dagster = "^1.4.1" +python = "^3.9, <3.13" # This is set to 3.9 as currently there is an issue when deploying via dagster-cloud where a dependency does not have a prebuild wheel file for 3.10 +dagit = "^1.5.14" +dagster = "^1.5.14" pandas = "^1.5.3" -dagster-gcp = "^0.20.2" +dagster-gcp = "^0.21.14" google = "^3.0.0" jinja2 = "^3.1.2" pygithub = "^1.58.0" @@ -20,12 +20,12 @@ deepdiff = "^6.3.0" mergedeep = "^1.3.4" pydash = "^6.0.2" dpath = "^2.1.5" -dagster-cloud = "^1.2.6" +dagster-cloud = "^1.5.14" grpcio = "^1.47.0" poetry2setup = "^1.1.0" poetry = "^1.5.1" -pydantic = "^1.10.6" -dagster-slack = "^0.20.2" +pydantic = "^1.10.8" +dagster-slack = "^0.21.14" sentry-sdk = "^1.28.1" semver = "^3.0.1" python-dateutil = "^2.8.2" diff --git a/airbyte-ci/connectors/pipelines/README.md b/airbyte-ci/connectors/pipelines/README.md index cb91ce4b03d0d..ede98ed3dd51a 100644 --- a/airbyte-ci/connectors/pipelines/README.md +++ b/airbyte-ci/connectors/pipelines/README.md @@ -543,6 +543,7 @@ E.G.: running `pytest` on a specific test folder: | Version | PR | Description | | ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| 3.4.1 | [#34067](https://github.com/airbytehq/airbyte/pull/34067) | Use dagster-cloud 1.5.7 for deploy | | 3.4.0 | [#34276](https://github.com/airbytehq/airbyte/pull/34276) | Introduce `--only-step` option for connector tests. | | 3.3.0 | [#34218](https://github.com/airbytehq/airbyte/pull/34218) | Introduce `--ci-requirements` option for client defined CI runners. | | 3.2.0 | [#34050](https://github.com/airbytehq/airbyte/pull/34050) | Connector test steps can take extra parameters | diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/pipeline.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/pipeline.py index 4860decdaf739..2bd32b1fcaaeb 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/pipeline.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/metadata/pipeline.py @@ -123,9 +123,7 @@ async def _run(self) -> StepResult: # mount metadata_service/lib and metadata_service/orchestrator parent_dir = self.context.get_repo_dir("airbyte-ci/connectors/metadata_service") python_base = with_python_base(self.context, "3.9") - python_with_dependencies = with_pip_packages( - python_base, ["dagster-cloud==1.2.6", "pydantic==1.10.6", "poetry2setup==1.1.0", "pendulum==2.1.2"] - ) + python_with_dependencies = with_pip_packages(python_base, ["dagster-cloud==1.5.14", "poetry2setup==1.1.0"]) dagster_cloud_api_token_secret: dagger.Secret = get_secret_host_variable( self.context.dagger_client, "DAGSTER_CLOUD_METADATA_API_TOKEN" ) diff --git a/airbyte-ci/connectors/pipelines/pyproject.toml b/airbyte-ci/connectors/pipelines/pyproject.toml index 3315d923151aa..ab036fe10fa4e 100644 --- a/airbyte-ci/connectors/pipelines/pyproject.toml +++ b/airbyte-ci/connectors/pipelines/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pipelines" -version = "3.4.0" +version = "3.4.1" description = "Packaged maintained by the connector operations team to perform CI for connectors' pipelines" authors = ["Airbyte "] From 07579bdaec221dfdd36c493254207b1f4a626bf6 Mon Sep 17 00:00:00 2001 From: Gireesh Sreepathi Date: Tue, 16 Jan 2024 13:04:01 -0800 Subject: [PATCH 122/574] Destination Postgres: Unpin cloud from 0.4.0 (#34303) Signed-off-by: Gireesh Sreepathi --- .../connectors/destination-postgres/metadata.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/airbyte-integrations/connectors/destination-postgres/metadata.yaml b/airbyte-integrations/connectors/destination-postgres/metadata.yaml index 54f3288be39fd..601a751d1ceef 100644 --- a/airbyte-integrations/connectors/destination-postgres/metadata.yaml +++ b/airbyte-integrations/connectors/destination-postgres/metadata.yaml @@ -18,11 +18,9 @@ data: normalizationTag: 0.4.3 registries: cloud: - dockerImageTag: 0.4.0 dockerRepository: airbyte/destination-postgres-strict-encrypt enabled: true oss: - dockerImageTag: 0.4.0 enabled: true releaseStage: alpha supportLevel: community From db83e149dda33a72fe2c91654e6bc36f5ea3501c Mon Sep 17 00:00:00 2001 From: Joe Bell Date: Tue, 16 Jan 2024 13:23:39 -0800 Subject: [PATCH 123/574] Destination Redshift - additional check method check, fix s3 file deletion (#34186) Signed-off-by: Gireesh Sreepathi Co-authored-by: Sitaram Shelke Co-authored-by: Marcos Marx Co-authored-by: Cynthia Yin Co-authored-by: Baz Co-authored-by: bazarnov Co-authored-by: kekiss Co-authored-by: Anatolii Yatsuk <35109939+tolik0@users.noreply.github.com> Co-authored-by: Edward Gao Co-authored-by: Augustin Co-authored-by: Joe Reuter Co-authored-by: Alexandre Cuoci Co-authored-by: perangel Co-authored-by: Aaron ("AJ") Steers Co-authored-by: Ben Church Co-authored-by: Gireesh Sreepathi --- airbyte-cdk/java/airbyte-cdk/README.md | 1 + .../src/main/resources/version.properties | 2 +- .../jdbc/AbstractJdbcDestination.java | 10 ++++++ .../staging/GeneralStagingFunctions.java | 30 +++++++++++------ .../destination/staging/SerialFlush.java | 5 ++- .../staging/StagingOperations.java | 25 ++------------- .../destination/s3/BlobStorageOperations.java | 2 +- .../destination/s3/S3ConsumerFactory.java | 1 - .../destination/s3/S3StorageOperations.java | 1 - .../destination/staging/AsyncFlush.java | 6 ++-- .../staging/StagingConsumerFactory.java | 10 ------ .../destination-redshift/build.gradle | 3 +- .../destination-redshift/metadata.yaml | 2 +- .../redshift/RedshiftInsertDestination.java | 6 ++++ .../RedshiftStagingS3Destination.java | 4 ++- .../RedshiftS3StagingSqlOperations.java | 32 ++----------------- .../redshift/util/RedshiftUtil.java | 8 +++++ docs/integrations/destinations/redshift.md | 25 ++++++++++++--- 18 files changed, 82 insertions(+), 91 deletions(-) diff --git a/airbyte-cdk/java/airbyte-cdk/README.md b/airbyte-cdk/java/airbyte-cdk/README.md index 5a45cbb2c3021..d572437df71c0 100644 --- a/airbyte-cdk/java/airbyte-cdk/README.md +++ b/airbyte-cdk/java/airbyte-cdk/README.md @@ -166,6 +166,7 @@ MavenLocal debugging steps: | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0.12.1 | 2024-01-11 | [\#34186](https://github.com/airbytehq/airbyte/pull/34186) | Add hook for additional destination specific checks to JDBC destination check method | | 0.12.0 | 2024-01-10 | [\#33875](https://github.com/airbytehq/airbyte/pull/33875) | Upgrade sshd-mina to 2.11.1 | | 0.11.5 | 2024-01-10 | [\#34119](https://github.com/airbytehq/airbyte/pull/34119) | Remove wal2json support for postgres+debezium. | | 0.11.4 | 2024-01-09 | [\#33305](https://github.com/airbytehq/airbyte/pull/33305) | Source stats in incremental syncs | diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties b/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties index 753eb4c8def57..db02062e29913 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties @@ -1 +1 @@ -version=0.12.0 +version=0.12.1 diff --git a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/AbstractJdbcDestination.java b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/AbstractJdbcDestination.java index 49572d746bbe1..ff93320b19bd2 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/AbstractJdbcDestination.java +++ b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/AbstractJdbcDestination.java @@ -92,6 +92,7 @@ public AirbyteConnectionStatus check(final JsonNode config) { final var v2RawSchema = namingResolver.getIdentifier(TypingAndDedupingFlag.getRawNamespaceOverride(RAW_SCHEMA_OVERRIDE) .orElse(JavaBaseConstants.DEFAULT_AIRBYTE_INTERNAL_NAMESPACE)); attemptTableOperations(v2RawSchema, database, namingResolver, sqlOperations, false); + destinationSpecificTableOperations(database); } return new AirbyteConnectionStatus().withStatus(Status.SUCCEEDED); } catch (final ConnectionErrorException ex) { @@ -114,6 +115,15 @@ public AirbyteConnectionStatus check(final JsonNode config) { } } + /** + * Specific Databases may have additional checks unique to them which they need to perform, override + * this method to add additional checks. + * + * @param database the database to run checks against + * @throws Exception + */ + protected void destinationSpecificTableOperations(final JdbcDatabase database) throws Exception {} + /** * This method is deprecated. It verifies table creation, but not insert right to a newly created * table. Use attemptTableOperations with the attemptInsert argument instead. diff --git a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/GeneralStagingFunctions.java b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/GeneralStagingFunctions.java index e28e9ec1d8f30..b01962a17bc75 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/GeneralStagingFunctions.java +++ b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/GeneralStagingFunctions.java @@ -13,6 +13,7 @@ import io.airbyte.protocol.models.v0.AirbyteStreamNameNamespacePair; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import java.util.concurrent.locks.Lock; import lombok.extern.slf4j.Slf4j; @@ -22,6 +23,16 @@ @Slf4j public class GeneralStagingFunctions { + // using a random string here as a placeholder for the moment. + // This would avoid mixing data in the staging area between different syncs (especially if they + // manipulate streams with similar names) + // if we replaced the random connection id by the actual connection_id, we'd gain the opportunity to + // leverage data that was uploaded to stage + // in a previous attempt but failed to load to the warehouse for some reason (interrupted?) instead. + // This would also allow other programs/scripts + // to load (or reload backups?) in the connection's staging area to be loaded at the next sync. + public static final UUID RANDOM_CONNECTION_ID = UUID.randomUUID(); + public static OnStartFunction onStartFunction(final JdbcDatabase database, final StagingOperations stagingOperations, final List writeConfigs, @@ -34,7 +45,6 @@ public static OnStartFunction onStartFunction(final JdbcDatabase database, final String schema = writeConfig.getOutputSchemaName(); final String stream = writeConfig.getStreamName(); final String dstTableName = writeConfig.getOutputTableName(); - final String stageName = stagingOperations.getStageName(schema, dstTableName); final String stagingPath = stagingOperations.getStagingPath(SerialStagingConsumerFactory.RANDOM_CONNECTION_ID, schema, stream, writeConfig.getOutputTableName(), writeConfig.getWriteDatetime()); @@ -44,7 +54,7 @@ public static OnStartFunction onStartFunction(final JdbcDatabase database, stagingOperations.createSchemaIfNotExists(database, schema); stagingOperations.createTableIfNotExists(database, schema, dstTableName); - stagingOperations.createStageIfNotExists(database, stageName); + stagingOperations.createStageIfNotExists(); /* * When we're in OVERWRITE, clear out the table at the start of a sync, this is an expected side @@ -68,7 +78,6 @@ public static OnStartFunction onStartFunction(final JdbcDatabase database, * upload was unsuccessful */ public static void copyIntoTableFromStage(final JdbcDatabase database, - final String stageName, final String stagingPath, final List stagedFiles, final String tableName, @@ -83,7 +92,7 @@ public static void copyIntoTableFromStage(final JdbcDatabase database, final Lock rawTableInsertLock = typerDeduper.getRawTableInsertLock(streamNamespace, streamName); rawTableInsertLock.lock(); try { - stagingOperations.copyIntoTableFromStage(database, stageName, stagingPath, stagedFiles, + stagingOperations.copyIntoTableFromStage(database, stagingPath, stagedFiles, tableName, schemaName); } finally { rawTableInsertLock.unlock(); @@ -96,8 +105,6 @@ public static void copyIntoTableFromStage(final JdbcDatabase database, typerDeduperValve.updateTimeAndIncreaseInterval(streamId); } } catch (final Exception e) { - stagingOperations.cleanUpStage(database, stageName, stagedFiles); - log.info("Cleaning stage path {}", stagingPath); throw new RuntimeException("Failed to upload data from stage " + stagingPath, e); } } @@ -124,10 +131,15 @@ public static OnCloseFunction onCloseFunction(final JdbcDatabase database, for (final WriteConfig writeConfig : writeConfigs) { final String schemaName = writeConfig.getOutputSchemaName(); if (purgeStagingData) { - final String stageName = stagingOperations.getStageName(schemaName, writeConfig.getOutputTableName()); + final String stagePath = stagingOperations.getStagingPath( + RANDOM_CONNECTION_ID, + schemaName, + writeConfig.getStreamName(), + writeConfig.getOutputTableName(), + writeConfig.getWriteDatetime()); log.info("Cleaning stage in destination started for stream {}. schema {}, stage: {}", writeConfig.getStreamName(), schemaName, - stageName); - stagingOperations.dropStageIfExists(database, stageName); + stagePath); + stagingOperations.dropStageIfExists(database, stagePath); } } typerDeduper.commitFinalTables(); diff --git a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/SerialFlush.java b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/SerialFlush.java index 767eea2333649..a4cb0c5fdaf38 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/SerialFlush.java +++ b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/SerialFlush.java @@ -81,15 +81,14 @@ public static FlushBufferFunction function( final WriteConfig writeConfig = pairToWriteConfig.get(pair); final String schemaName = writeConfig.getOutputSchemaName(); - final String stageName = stagingOperations.getStageName(schemaName, writeConfig.getOutputTableName()); final String stagingPath = stagingOperations.getStagingPath( SerialStagingConsumerFactory.RANDOM_CONNECTION_ID, schemaName, writeConfig.getStreamName(), writeConfig.getOutputTableName(), writeConfig.getWriteDatetime()); try (writer) { writer.flush(); - final String stagedFile = stagingOperations.uploadRecordsToStage(database, writer, schemaName, stageName, stagingPath); - GeneralStagingFunctions.copyIntoTableFromStage(database, stageName, stagingPath, List.of(stagedFile), writeConfig.getOutputTableName(), + final String stagedFile = stagingOperations.uploadRecordsToStage(database, writer, schemaName, stagingPath); + GeneralStagingFunctions.copyIntoTableFromStage(database, stagingPath, List.of(stagedFile), writeConfig.getOutputTableName(), schemaName, stagingOperations, writeConfig.getNamespace(), diff --git a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/StagingOperations.java b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/StagingOperations.java index fc04e995fb476..aac9351b4b7d8 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/StagingOperations.java +++ b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/StagingOperations.java @@ -18,15 +18,6 @@ */ public interface StagingOperations extends SqlOperations { - /** - * Returns the staging environment's name - * - * @param namespace Name of schema - * @param streamName Name of the stream - * @return Fully qualified name of the staging environment - */ - String getStageName(String namespace, String streamName); - /** * @param outputTableName The name of the table this staging file will be loaded into (typically a * raw table). Not all destinations use the table name in the staging path (e.g. Snowflake @@ -37,7 +28,7 @@ public interface StagingOperations extends SqlOperations { /** * Create a staging folder where to upload temporary files before loading into the final destination */ - void createStageIfNotExists(JdbcDatabase database, String stageName) throws Exception; + void createStageIfNotExists() throws Exception; /** * Upload the data file into the stage area. @@ -45,40 +36,28 @@ public interface StagingOperations extends SqlOperations { * @param database database used for syncing * @param recordsData records stored in in-memory buffer * @param schemaName name of schema - * @param stageName name of the staging area folder * @param stagingPath path of staging folder to data files * @return the name of the file that was uploaded. */ - String uploadRecordsToStage(JdbcDatabase database, SerializableBuffer recordsData, String schemaName, String stageName, String stagingPath) + String uploadRecordsToStage(JdbcDatabase database, SerializableBuffer recordsData, String schemaName, String stagingPath) throws Exception; /** * Load the data stored in the stage area into a temporary table in the destination * * @param database database interface - * @param stageName name of staging area folder * @param stagingPath path to staging files * @param stagedFiles collection of staged files * @param tableName name of table to write staging files to * @param schemaName name of schema */ void copyIntoTableFromStage(JdbcDatabase database, - String stageName, String stagingPath, List stagedFiles, String tableName, String schemaName) throws Exception; - /** - * Remove files that were just staged - * - * @param database database used for syncing - * @param stageName name of staging area folder - * @param stagedFiles collection of the staging files to remove - */ - void cleanUpStage(JdbcDatabase database, String stageName, List stagedFiles) throws Exception; - /** * Delete the stage area and all staged files that was in it * diff --git a/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/s3/BlobStorageOperations.java b/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/s3/BlobStorageOperations.java index dfb0d0a50822a..9df281e9e19b2 100644 --- a/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/s3/BlobStorageOperations.java +++ b/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/s3/BlobStorageOperations.java @@ -31,7 +31,7 @@ protected BlobStorageOperations() { * * @return the name of the file that was uploaded. */ - public abstract String uploadRecordsToBucket(SerializableBuffer recordsData, String namespace, String streamName, String objectPath) + public abstract String uploadRecordsToBucket(SerializableBuffer recordsData, String namespace, String objectPath) throws Exception; /** diff --git a/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/s3/S3ConsumerFactory.java b/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/s3/S3ConsumerFactory.java index ff0207cabdb8b..38068dbf38c1c 100644 --- a/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/s3/S3ConsumerFactory.java +++ b/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/s3/S3ConsumerFactory.java @@ -128,7 +128,6 @@ private FlushBufferFunction flushBufferFunction(final BlobStorageOperations stor writeConfig.addStoredFile(storageOperations.uploadRecordsToBucket( writer, writeConfig.getNamespace(), - writeConfig.getStreamName(), writeConfig.getFullOutputPath())); } catch (final Exception e) { LOGGER.error("Failed to flush and upload buffer to storage:", e); diff --git a/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/s3/S3StorageOperations.java b/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/s3/S3StorageOperations.java index 038a06bff9872..9db0d0d4994af 100644 --- a/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/s3/S3StorageOperations.java +++ b/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/s3/S3StorageOperations.java @@ -120,7 +120,6 @@ protected boolean doesBucketExist(final String bucket) { @Override public String uploadRecordsToBucket(final SerializableBuffer recordsData, final String namespace, - final String streamName, final String objectPath) { final List exceptionsThrown = new ArrayList<>(); while (exceptionsThrown.size() < UPLOAD_RETRY_LIMIT) { diff --git a/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/AsyncFlush.java b/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/AsyncFlush.java index d3adf4ff43cea..564e3d3ade85e 100644 --- a/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/AsyncFlush.java +++ b/airbyte-cdk/java/airbyte-cdk/s3-destinations/src/main/java/io/airbyte/cdk/integrations/destination/staging/AsyncFlush.java @@ -103,19 +103,17 @@ public void flush(final StreamDescriptor decs, final Stream outputRecordCollector, final JdbcDatabase database, diff --git a/airbyte-integrations/connectors/destination-redshift/build.gradle b/airbyte-integrations/connectors/destination-redshift/build.gradle index 773a75a9ba127..33f09966a63dd 100644 --- a/airbyte-integrations/connectors/destination-redshift/build.gradle +++ b/airbyte-integrations/connectors/destination-redshift/build.gradle @@ -4,12 +4,11 @@ plugins { } airbyteJavaConnector { - cdkVersionRequired = '0.12.0' + cdkVersionRequired = '0.12.1' features = ['db-destinations', 's3-destinations', 'typing-deduping'] useLocalCdk = false } -//remove once upgrading the CDK version to 0.4.x or later java { compileJava { options.compilerArgs.remove("-Werror") diff --git a/airbyte-integrations/connectors/destination-redshift/metadata.yaml b/airbyte-integrations/connectors/destination-redshift/metadata.yaml index 9531fc7f51b15..93f27e28ecd86 100644 --- a/airbyte-integrations/connectors/destination-redshift/metadata.yaml +++ b/airbyte-integrations/connectors/destination-redshift/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: database connectorType: destination definitionId: f7a7d195-377f-cf5b-70a5-be6b819019dc - dockerImageTag: 0.7.14 + dockerImageTag: 0.7.15 dockerRepository: airbyte/destination-redshift documentationUrl: https://docs.airbyte.com/integrations/destinations/redshift githubIssueLabel: destination-redshift diff --git a/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftInsertDestination.java b/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftInsertDestination.java index 66e5e544093fd..a4ba7a6695570 100644 --- a/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftInsertDestination.java +++ b/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftInsertDestination.java @@ -22,6 +22,7 @@ import io.airbyte.integrations.destination.redshift.operations.RedshiftSqlOperations; import io.airbyte.integrations.destination.redshift.typing_deduping.RedshiftDestinationHandler; import io.airbyte.integrations.destination.redshift.typing_deduping.RedshiftSqlGenerator; +import io.airbyte.integrations.destination.redshift.util.RedshiftUtil; import java.time.Duration; import java.util.HashMap; import java.util.Map; @@ -60,6 +61,11 @@ public DataSource getDataSource(final JsonNode config) { Duration.ofMinutes(2)); } + @Override + protected void destinationSpecificTableOperations(final JdbcDatabase database) throws Exception { + RedshiftUtil.checkSvvTableAccess(database); + } + @Override public JdbcDatabase getDatabase(final DataSource dataSource) { return new DefaultJdbcDatabase(dataSource); diff --git a/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftStagingS3Destination.java b/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftStagingS3Destination.java index d82a22fe2eaa9..e94118e279eed 100644 --- a/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftStagingS3Destination.java +++ b/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/RedshiftStagingS3Destination.java @@ -51,6 +51,7 @@ import io.airbyte.integrations.destination.redshift.operations.RedshiftSqlOperations; import io.airbyte.integrations.destination.redshift.typing_deduping.RedshiftDestinationHandler; import io.airbyte.integrations.destination.redshift.typing_deduping.RedshiftSqlGenerator; +import io.airbyte.integrations.destination.redshift.util.RedshiftUtil; import io.airbyte.protocol.models.v0.AirbyteConnectionStatus; import io.airbyte.protocol.models.v0.AirbyteConnectionStatus.Status; import io.airbyte.protocol.models.v0.AirbyteMessage; @@ -103,7 +104,8 @@ public AirbyteConnectionStatus check(final JsonNode config) { try { final JdbcDatabase database = new DefaultJdbcDatabase(dataSource); final String outputSchema = super.getNamingResolver().getIdentifier(config.get(JdbcUtils.SCHEMA_KEY).asText()); - attemptSQLCreateAndDropTableOperations(outputSchema, database, nameTransformer, redshiftS3StagingSqlOperations); + attemptTableOperations(outputSchema, database, nameTransformer, redshiftS3StagingSqlOperations, false); + RedshiftUtil.checkSvvTableAccess(database); return new AirbyteConnectionStatus().withStatus(AirbyteConnectionStatus.Status.SUCCEEDED); } catch (final ConnectionErrorException e) { final String message = getErrorMessage(e.getStateCode(), e.getErrorCode(), e.getExceptionMessage(), e); diff --git a/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/operations/RedshiftS3StagingSqlOperations.java b/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/operations/RedshiftS3StagingSqlOperations.java index b3adb95f8b352..9cf38f7ce4af1 100644 --- a/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/operations/RedshiftS3StagingSqlOperations.java +++ b/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/operations/RedshiftS3StagingSqlOperations.java @@ -52,19 +52,6 @@ public RedshiftS3StagingSqlOperations(final NamingConventionTransformer nameTran } } - /** - * I suspect this value is ignored. The stage name is eventually passed into - * {@link io.airbyte.cdk.integrations.destination.s3.S3StorageOperations#uploadRecordsToBucket(SerializableBuffer, String, String, String)} - * as the streamName parameter... which is completely ignored. - * - */ - @Override - public String getStageName(final String namespace, final String streamName) { - return nameTransformer.applyDefaultCase(String.join("_", - nameTransformer.convertStreamName(namespace), - nameTransformer.convertStreamName(streamName))); - } - @Override public String getStagingPath(final UUID connectionId, final String namespace, @@ -84,9 +71,7 @@ public String getStagingPath(final UUID connectionId, } @Override - public void createStageIfNotExists(final JdbcDatabase database, final String stageName) throws Exception { - final String bucketPath = s3Config.getBucketPath(); - final String prefix = bucketPath.isEmpty() ? "" : bucketPath + (bucketPath.endsWith("/") ? "" : "/"); + public void createStageIfNotExists() throws Exception { s3StorageOperations.createBucketIfNotExists(); } @@ -94,10 +79,9 @@ public void createStageIfNotExists(final JdbcDatabase database, final String sta public String uploadRecordsToStage(final JdbcDatabase database, final SerializableBuffer recordsData, final String schemaName, - final String stageName, final String stagingPath) throws Exception { - return s3StorageOperations.uploadRecordsToBucket(recordsData, schemaName, stageName, stagingPath); + return s3StorageOperations.uploadRecordsToBucket(recordsData, schemaName, stagingPath); } private String putManifest(final String manifestContents, final String stagingPath) { @@ -108,7 +92,6 @@ private String putManifest(final String manifestContents, final String stagingPa @Override public void copyIntoTableFromStage(final JdbcDatabase database, - final String stageName, final String stagingPath, final List stagedFiles, final String tableName, @@ -176,18 +159,9 @@ private static String getManifestPath(final String s3BucketName, final String s3 return "s3://" + s3BucketName + "/" + stagingPath + s3StagingFile; } - @Override - public void cleanUpStage(final JdbcDatabase database, final String stageName, final List stagedFiles) throws Exception { - final String bucketPath = s3Config.getBucketPath(); - final String prefix = bucketPath.isEmpty() ? "" : bucketPath + (bucketPath.endsWith("/") ? "" : "/"); - s3StorageOperations.cleanUpBucketObject(prefix + stageName, stagedFiles); - } - @Override public void dropStageIfExists(final JdbcDatabase database, final String stageName) throws Exception { - final String bucketPath = s3Config.getBucketPath(); - final String prefix = bucketPath.isEmpty() ? "" : bucketPath + (bucketPath.endsWith("/") ? "" : "/"); - s3StorageOperations.dropBucketObject(prefix + stageName); + s3StorageOperations.dropBucketObject(stageName); } } diff --git a/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/util/RedshiftUtil.java b/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/util/RedshiftUtil.java index c1433c4aa226f..6551820a48316 100644 --- a/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/util/RedshiftUtil.java +++ b/airbyte-integrations/connectors/destination-redshift/src/main/java/io/airbyte/integrations/destination/redshift/util/RedshiftUtil.java @@ -7,10 +7,13 @@ import static io.airbyte.integrations.destination.redshift.constants.RedshiftDestinationConstants.UPLOADING_METHOD; import com.fasterxml.jackson.databind.JsonNode; +import io.airbyte.cdk.db.jdbc.JdbcDatabase; +import lombok.extern.log4j.Log4j2; /** * Helper class for Destination Redshift connector. */ +@Log4j2 public class RedshiftUtil { private RedshiftUtil() {} @@ -36,4 +39,9 @@ private static boolean isNullOrEmpty(final JsonNode jsonNode) { return null == jsonNode || "".equals(jsonNode.asText()); } + public static void checkSvvTableAccess(final JdbcDatabase database) throws Exception { + log.info("checking SVV_TABLE_INFO permissions"); + database.queryJsons("SELECT 1 FROM SVV_TABLE_INFO LIMIT 1;"); + } + } diff --git a/docs/integrations/destinations/redshift.md b/docs/integrations/destinations/redshift.md index 63553b1d06d82..18f338d838618 100644 --- a/docs/integrations/destinations/redshift.md +++ b/docs/integrations/destinations/redshift.md @@ -94,12 +94,26 @@ connection only. S3 is secured through public HTTPS access only. 5. (Optional) [Create](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html) a staging S3 bucket \(for the COPY strategy\). -6. Create a user with at least create table permissions for the schema. If the schema does not exist - you need to add permissions for that, too. Something like this: -``` -GRANT CREATE ON DATABASE database_name TO airflow_user; -- add create schema permission -GRANT usage, create on schema my_schema TO airflow_user; -- add create table permission +### Permissions in Redshift +Airbyte writes data into two schemas, whichever schema you want your data to land in, e.g. `my_schema` +and a "Raw Data" schema that Airbyte uses to improve ELT reliability. By default, this raw data schema +is `airbyte_internal` but this can be overridden in the Redshift Destination's advanced settings. +Airbyte also needs to query Redshift's +[SVV_TABLE_INFO](https://docs.aws.amazon.com/redshift/latest/dg/r_SVV_TABLE_INFO.html) table for +metadata about the tables airbyte manages. + +To ensure the `airbyte_user` has the correction permissions to: +- create schemas in your database +- grant usage to any existing schemas you want Airbyte to use +- grant select to the `svv_table_info` table + +You can execute the following SQL statements + +```sql +GRANT CREATE ON DATABASE database_name TO airbyte_user; -- add create schema permission +GRANT usage, create on schema my_schema TO airbyte_user; -- add create table permission +GRANT SELECT ON TABLE SVV_TABLE_INFO TO airbyte_user; -- add select permission for svv_table_info ``` ### Optional Use of SSH Bastion Host @@ -215,6 +229,7 @@ Each stream will be output into its own raw table in Redshift. Each table will c | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0.7.15 | 2024-01-11 | [\#34186](https://github.com/airbytehq/airbyte/pull/34186) | Update check method with svv_table_info permission check, fix bug where s3 staging files were not being deleted. | | 0.7.14 | 2024-01-08 | [\#34014](https://github.com/airbytehq/airbyte/pull/34014) | Update order of options in spec | | 0.7.13 | 2024-01-05 | [\#33948](https://github.com/airbytehq/airbyte/pull/33948) | Fix NPE when prepare tables fail; Add case sensitive session for super; Bastion heartbeats added | | 0.7.12 | 2024-01-03 | [\#33924](https://github.com/airbytehq/airbyte/pull/33924) | Add new ap-southeast-3 AWS region | From b03d785c41a197c276e034767117ac79f68a8079 Mon Sep 17 00:00:00 2001 From: Augustin Date: Tue, 16 Jan 2024 23:15:49 +0100 Subject: [PATCH 124/574] airbyte-ci: pass extra options after gradle tasks (#34301) --- airbyte-ci/connectors/pipelines/README.md | 1 + .../pipelines/airbyte_ci/steps/gradle.py | 22 ++++++++----------- .../connectors/pipelines/pyproject.toml | 2 +- .../connectors/pipelines/tests/test_gradle.py | 12 +--------- 4 files changed, 12 insertions(+), 25 deletions(-) diff --git a/airbyte-ci/connectors/pipelines/README.md b/airbyte-ci/connectors/pipelines/README.md index ede98ed3dd51a..4c8a85289eaa4 100644 --- a/airbyte-ci/connectors/pipelines/README.md +++ b/airbyte-ci/connectors/pipelines/README.md @@ -543,6 +543,7 @@ E.G.: running `pytest` on a specific test folder: | Version | PR | Description | | ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| 3.4.2 | [#34301](https://github.com/airbytehq/airbyte/pull/34301) | Pass extra params after Gradle tasks. | | 3.4.1 | [#34067](https://github.com/airbytehq/airbyte/pull/34067) | Use dagster-cloud 1.5.7 for deploy | | 3.4.0 | [#34276](https://github.com/airbytehq/airbyte/pull/34276) | Introduce `--only-step` option for connector tests. | | 3.3.0 | [#34218](https://github.com/airbytehq/airbyte/pull/34218) | Introduce `--ci-requirements` option for client defined CI runners. | diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/steps/gradle.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/steps/gradle.py index ae44de953449c..08b110dabc0ad 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/steps/gradle.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/steps/gradle.py @@ -3,7 +3,7 @@ # from abc import ABC -from typing import Any, ClassVar, List +from typing import Any, ClassVar, List, Optional, Tuple import pipelines.dagger.actions.system.docker from dagger import CacheSharingMode, CacheVolume @@ -11,7 +11,7 @@ from pipelines.consts import AMAZONCORRETTO_IMAGE from pipelines.dagger.actions import secrets from pipelines.helpers.utils import sh_dash_c -from pipelines.models.steps import STEP_PARAMS, Step, StepResult +from pipelines.models.steps import Step, StepResult class GradleTask(Step, ABC): @@ -30,20 +30,15 @@ class GradleTask(Step, ABC): LOCAL_MAVEN_REPOSITORY_PATH = "/root/.m2" GRADLE_DEP_CACHE_PATH = "/root/gradle-cache" GRADLE_HOME_PATH = "/root/.gradle" - STATIC_GRADLE_TASK_OPTIONS = ("--no-daemon", "--no-watch-fs") + STATIC_GRADLE_OPTIONS = ("--no-daemon", "--no-watch-fs", "--build-cache", "--scan", "--console=plain") gradle_task_name: ClassVar[str] bind_to_docker_host: ClassVar[bool] = False mount_connector_secrets: ClassVar[bool] = False accept_extra_params = True @property - def default_params(self) -> STEP_PARAMS: - return super().default_params | { - "-Ds3BuildCachePrefix": [self.context.connector.technical_name], # Set the S3 build cache prefix. - "--build-cache": [], # Enable the gradle build cache. - "--scan": [], # Enable the gradle build scan. - "--console": ["plain"], # Disable the gradle rich console. - } + def gradle_task_options(self) -> Tuple[str, ...]: + return self.STATIC_GRADLE_OPTIONS + (f"-Ds3BuildCachePrefix={self.context.connector.technical_name}",) @property def dependency_cache_volume(self) -> CacheVolume: @@ -64,8 +59,9 @@ def build_include(self) -> List[str]: for dependency_directory in self.context.connector.get_local_dependency_paths(with_test_dependencies=True) ] - def _get_gradle_command(self, task: str, *args: Any) -> str: - return f"./gradlew {' '.join(self.STATIC_GRADLE_TASK_OPTIONS + args)} {task}" + def _get_gradle_command(self, task: str, *args: Any, task_options: Optional[List[str]] = None) -> str: + task_options = task_options or [] + return f"./gradlew {' '.join(self.gradle_task_options + args)} {task} {' '.join(task_options)}" async def _run(self, *args: Any, **kwargs: Any) -> StepResult: include = [ @@ -200,7 +196,7 @@ async def _run(self, *args: Any, **kwargs: Any) -> StepResult: # Warm the gradle cache. f"(rsync -a --stats --mkpath {self.GRADLE_DEP_CACHE_PATH}/ {self.GRADLE_HOME_PATH} || true)", # Run the gradle task. - self._get_gradle_command(connector_task, *self.params_as_cli_options), + self._get_gradle_command(connector_task, task_options=self.params_as_cli_options), ] ) ) diff --git a/airbyte-ci/connectors/pipelines/pyproject.toml b/airbyte-ci/connectors/pipelines/pyproject.toml index ab036fe10fa4e..79f37a9efd606 100644 --- a/airbyte-ci/connectors/pipelines/pyproject.toml +++ b/airbyte-ci/connectors/pipelines/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pipelines" -version = "3.4.1" +version = "3.4.2" description = "Packaged maintained by the connector operations team to perform CI for connectors' pipelines" authors = ["Airbyte "] diff --git a/airbyte-ci/connectors/pipelines/tests/test_gradle.py b/airbyte-ci/connectors/pipelines/tests/test_gradle.py index 5e867c3582ba7..34312ec1d0d34 100644 --- a/airbyte-ci/connectors/pipelines/tests/test_gradle.py +++ b/airbyte-ci/connectors/pipelines/tests/test_gradle.py @@ -39,18 +39,8 @@ async def test_build_include(self, test_context): def test_params(self, test_context): step = self.DummyStep(test_context) + step.extra_params = {"-x": ["dummyTask", "dummyTask2"]} assert set(step.params_as_cli_options) == { - f"-Ds3BuildCachePrefix={test_context.connector.technical_name}", - "--build-cache", - "--scan", - "--console=plain", - } - step.extra_params = {"-x": ["dummyTask", "dummyTask2"], "--console": ["rich"]} - assert set(step.params_as_cli_options) == { - f"-Ds3BuildCachePrefix={test_context.connector.technical_name}", - "--build-cache", - "--scan", - "--console=rich", "-x=dummyTask", "-x=dummyTask2", } From cbbbeb92b83d3ef2836a46cfd10bf7e4109a7c90 Mon Sep 17 00:00:00 2001 From: "Aaron (\"AJ\") Steers" Date: Tue, 16 Jan 2024 16:46:51 -0800 Subject: [PATCH 125/574] AirbyteLib: Python lint cleanup (#34223) --- airbyte-lib/airbyte_lib/__init__.py | 2 + airbyte-lib/airbyte_lib/_executor.py | 53 +++-- .../airbyte_lib/_factories/cache_factories.py | 5 +- .../_factories/connector_factories.py | 27 ++- airbyte-lib/airbyte_lib/_file_writers/base.py | 3 +- .../airbyte_lib/_file_writers/parquet.py | 8 +- airbyte-lib/airbyte_lib/_processors.py | 8 +- .../airbyte_lib/_util/protocol_util.py | 8 +- airbyte-lib/airbyte_lib/caches/base.py | 32 +-- airbyte-lib/airbyte_lib/caches/duckdb.py | 7 +- airbyte-lib/airbyte_lib/caches/postgres.py | 3 +- airbyte-lib/airbyte_lib/datasets/_lazy.py | 8 +- airbyte-lib/airbyte_lib/registry.py | 9 +- airbyte-lib/airbyte_lib/source.py | 136 ++++++------ airbyte-lib/airbyte_lib/telemetry.py | 12 +- airbyte-lib/airbyte_lib/types.py | 1 + airbyte-lib/airbyte_lib/validate.py | 22 +- airbyte-lib/docs.py | 12 +- airbyte-lib/docs/generated/airbyte_lib.html | 36 ++-- .../docs/generated/airbyte_lib/factories.html | 7 - airbyte-lib/poetry.lock | 194 +++++++++++++++--- airbyte-lib/pyproject.toml | 103 ++++++++-- airbyte-lib/tests/lint_tests/test_ruff.py | 9 - 23 files changed, 468 insertions(+), 237 deletions(-) delete mode 100644 airbyte-lib/docs/generated/airbyte_lib/factories.html diff --git a/airbyte-lib/airbyte_lib/__init__.py b/airbyte-lib/airbyte_lib/__init__.py index 8ba1300c69730..895849f19771a 100644 --- a/airbyte-lib/airbyte_lib/__init__.py +++ b/airbyte-lib/airbyte_lib/__init__.py @@ -1,3 +1,5 @@ +"""AirbyteLib brings Airbyte ELT to every Python developer.""" + from airbyte_lib._factories.cache_factories import get_default_cache, new_local_cache from airbyte_lib._factories.connector_factories import get_connector from airbyte_lib.datasets import CachedDataset diff --git a/airbyte-lib/airbyte_lib/_executor.py b/airbyte-lib/airbyte_lib/_executor.py index 6d0c6625bb186..1a816cc46848f 100644 --- a/airbyte-lib/airbyte_lib/_executor.py +++ b/airbyte-lib/airbyte_lib/_executor.py @@ -1,18 +1,22 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. +from __future__ import annotations -import os import subprocess import sys from abc import ABC, abstractmethod -from collections.abc import Generator, Iterable, Iterator from contextlib import contextmanager from pathlib import Path -from typing import IO, Any, NoReturn +from typing import IO, TYPE_CHECKING, Any, NoReturn -from airbyte_lib.registry import ConnectorMetadata from airbyte_lib.telemetry import SourceTelemetryInfo, SourceType +if TYPE_CHECKING: + from collections.abc import Generator, Iterable, Iterator + + from airbyte_lib.registry import ConnectorMetadata + + _LATEST_VERSION = "latest" @@ -89,7 +93,7 @@ def _stream_from_file(file: IO[str]) -> Generator[str, Any, None]: exit_code = process.wait() # If the exit code is not 0 or -15 (SIGTERM), raise an exception - if exit_code != 0 and exit_code != -15: + if exit_code not in (0, -15): raise Exception(f"Process exited with code {exit_code}") @@ -98,8 +102,9 @@ def __init__( self, metadata: ConnectorMetadata, target_version: str | None = None, - install_if_missing: bool = False, pip_url: str | None = None, + *, + install_if_missing: bool = False, ) -> None: super().__init__(metadata, target_version) self.install_if_missing = install_if_missing @@ -122,26 +127,28 @@ def _run_subprocess_and_raise_on_failure(self, args: list[str]) -> None: def uninstall(self) -> None: venv_name = self._get_venv_name() - if os.path.exists(venv_name): + if Path(venv_name).exists(): self._run_subprocess_and_raise_on_failure(["rm", "-rf", venv_name]) def install(self) -> None: venv_name = self._get_venv_name() self._run_subprocess_and_raise_on_failure([sys.executable, "-m", "venv", venv_name]) - pip_path = os.path.join(venv_name, "bin", "pip") + pip_path = str(Path(venv_name) / "bin" / "pip") self._run_subprocess_and_raise_on_failure([pip_path, "install", "-e", self.pip_url]) def _get_installed_version(self) -> str: - """ - In the venv, run the following: python -c "from importlib.metadata import version; print(version(''))" + """Detect the version of the connector installed. + + In the venv, we run the following: + > python -c "from importlib.metadata import version; print(version(''))" """ venv_name = self._get_venv_name() connector_name = self.metadata.name return subprocess.check_output( [ - os.path.join(venv_name, "bin", "python"), + Path(venv_name) / "bin" / "python", "-c", f"from importlib.metadata import version; print(version('{connector_name}'))", ], @@ -151,8 +158,8 @@ def _get_installed_version(self) -> str: def ensure_installation( self, ) -> None: - """ - Ensure that the connector is installed in a virtual environment. + """Ensure that the connector is installed in a virtual environment. + If not yet installed and if install_if_missing is True, then install. Optionally, verify that the installed version matches the target version. @@ -165,14 +172,16 @@ def ensure_installation( if not venv_path.exists(): if not self.install_if_missing: raise Exception( - f"Connector {self.metadata.name} is not available - venv {venv_name} does not exist" + f"Connector {self.metadata.name} is not available - " + f"venv {venv_name} does not exist" ) self.install() connector_path = self._get_connector_path() if not connector_path.exists(): raise FileNotFoundError( - f"Could not find connector '{self.metadata.name}' in venv '{venv_name}' with connector path '{connector_path}'.", + f"Could not find connector '{self.metadata.name}' in venv '{venv_name}' with " + f"connector path '{connector_path}'.", ) if self.enforce_version: @@ -185,13 +194,14 @@ def ensure_installation( version_after_install = self._get_installed_version() if version_after_install != self.target_version: raise Exception( - f"Failed to install connector {self.metadata.name} version {self.target_version}. Installed version is {version_after_install}", + f"Failed to install connector {self.metadata.name} version " + f"{self.target_version}. Installed version is {version_after_install}", ) def execute(self, args: list[str]) -> Iterator[str]: connector_path = self._get_connector_path() - with _stream_from_subprocess([str(connector_path)] + args) as stream: + with _stream_from_subprocess([str(connector_path), *args]) as stream: yield from stream def get_telemetry_info(self) -> SourceTelemetryInfo: @@ -204,19 +214,20 @@ def ensure_installation(self) -> None: self.execute(["spec"]) except Exception as e: raise Exception( - f"Connector {self.metadata.name} is not available - executing it failed: {e}" - ) + f"Connector {self.metadata.name} is not available - executing it failed" + ) from e def install(self) -> NoReturn: raise Exception(f"Connector {self.metadata.name} is not available - cannot install it") def uninstall(self) -> NoReturn: raise Exception( - f"Connector {self.metadata.name} is installed manually and not managed by airbyte-lib - please remove it manually" + f"Connector {self.metadata.name} is installed manually and not managed by airbyte-lib -" + " please remove it manually" ) def execute(self, args: list[str]) -> Iterator[str]: - with _stream_from_subprocess([self.metadata.name] + args) as stream: + with _stream_from_subprocess([self.metadata.name, *args]) as stream: yield from stream def get_telemetry_info(self) -> SourceTelemetryInfo: diff --git a/airbyte-lib/airbyte_lib/_factories/cache_factories.py b/airbyte-lib/airbyte_lib/_factories/cache_factories.py index ea863b7bdb00b..5a95dce2db7b4 100644 --- a/airbyte-lib/airbyte_lib/_factories/cache_factories.py +++ b/airbyte-lib/airbyte_lib/_factories/cache_factories.py @@ -1,5 +1,5 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. - +from __future__ import annotations from pathlib import Path @@ -23,13 +23,14 @@ def get_default_cache() -> DuckDBCache: def new_local_cache( cache_name: str | None = None, cache_dir: str | Path | None = None, + *, cleanup: bool = True, ) -> DuckDBCache: """Get a local cache for storing data, using a name string to seed the path. Args: cache_name: Name to use for the cache. Defaults to None. - root_dir: Root directory to store the cache in. Defaults to None. + cache_dir: Root directory to store the cache in. Defaults to None. cleanup: Whether to clean up temporary files. Defaults to True. Cache files are stored in the `.cache` directory, relative to the current diff --git a/airbyte-lib/airbyte_lib/_factories/connector_factories.py b/airbyte-lib/airbyte_lib/_factories/connector_factories.py index 06482d67aa1c7..347710f20824a 100644 --- a/airbyte-lib/airbyte_lib/_factories/connector_factories.py +++ b/airbyte-lib/airbyte_lib/_factories/connector_factories.py @@ -1,5 +1,5 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. - +from __future__ import annotations from typing import Any @@ -13,17 +13,26 @@ def get_connector( version: str | None = None, pip_url: str | None = None, config: dict[str, Any] | None = None, + *, use_local_install: bool = False, install_if_missing: bool = True, ) -> Source: - """ - Get a connector by name and version. - :param name: connector name - :param version: connector version - if not provided, the currently installed version will be used. If no version is installed, the latest available version will be used. The version can also be set to "latest" to force the use of the latest available version. - :param pip_url: connector pip URL - if not provided, the pip url will be inferred from the connector name. - :param config: connector config - if not provided, you need to set it later via the set_config method. - :param use_local_install: whether to use a virtual environment to run the connector. If True, the connector is expected to be available on the path (e.g. installed via pip). If False, the connector will be installed automatically in a virtual environment. - :param install_if_missing: whether to install the connector if it is not available locally. This parameter is ignored if use_local_install is True. + """Get a connector by name and version. + + Args: + name: connector name + version: connector version - if not provided, the currently installed version will be used. + If no version is installed, the latest available version will be used. The version can + also be set to "latest" to force the use of the latest available version. + pip_url: connector pip URL - if not provided, the pip url will be inferred from the + connector name. + config: connector config - if not provided, you need to set it later via the set_config + method. + use_local_install: whether to use a virtual environment to run the connector. If True, the + connector is expected to be available on the path (e.g. installed via pip). If False, + the connector will be installed automatically in a virtual environment. + install_if_missing: whether to install the connector if it is not available locally. This + parameter is ignored if use_local_install is True. """ metadata = get_connector_metadata(name) if use_local_install: diff --git a/airbyte-lib/airbyte_lib/_file_writers/base.py b/airbyte-lib/airbyte_lib/_file_writers/base.py index a4913f0f7bb30..3f16953f12f54 100644 --- a/airbyte-lib/airbyte_lib/_file_writers/base.py +++ b/airbyte-lib/airbyte_lib/_file_writers/base.py @@ -53,8 +53,7 @@ def _write_batch( batch_id: str, record_batch: pa.Table | pa.RecordBatch, ) -> FileWriterBatchHandle: - """ - Process a record batch. + """Process a record batch. Return a list of paths to one or more cache files. """ diff --git a/airbyte-lib/airbyte_lib/_file_writers/parquet.py b/airbyte-lib/airbyte_lib/_file_writers/parquet.py index 201fb4952eefa..aeb2113f2a285 100644 --- a/airbyte-lib/airbyte_lib/_file_writers/parquet.py +++ b/airbyte-lib/airbyte_lib/_file_writers/parquet.py @@ -1,6 +1,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. """A Parquet cache implementation.""" +from __future__ import annotations from pathlib import Path from typing import cast @@ -16,8 +17,7 @@ class ParquetWriterConfig(FileWriterConfigBase): """Configuration for the Snowflake cache.""" - # Inherits from base class: - # cache_dir: str | Path + # Inherits `cache_dir` from base class class ParquetWriter(FileWriterBase): @@ -44,11 +44,11 @@ def _write_batch( batch_id: str, record_batch: pa.Table | pa.RecordBatch, ) -> FileWriterBatchHandle: - """ - Process a record batch. + """Process a record batch. Return the path to the cache file. """ + _ = batch_id # unused output_file_path = self.get_new_cache_file_path(stream_name) with parquet.ParquetWriter(output_file_path, record_batch.schema) as writer: diff --git a/airbyte-lib/airbyte_lib/_processors.py b/airbyte-lib/airbyte_lib/_processors.py index f105bc2488645..f0f94c30c512d 100644 --- a/airbyte-lib/airbyte_lib/_processors.py +++ b/airbyte-lib/airbyte_lib/_processors.py @@ -104,8 +104,7 @@ def process_stdin( self, max_batch_size: int = DEFAULT_BATCH_SIZE, ) -> None: - """ - Process the input stream from stdin. + """Process the input stream from stdin. Return a list of summaries for testing. """ @@ -126,8 +125,7 @@ def process_input_stream( input_stream: io.TextIOBase, max_batch_size: int = DEFAULT_BATCH_SIZE, ) -> None: - """ - Parse the input stream and process data in batches. + """Parse the input stream and process data in batches. Return a list of summaries for testing. """ @@ -229,7 +227,7 @@ def _cleanup_batch( # noqa: B027 # Intentionally empty, not abstract For instance, file writers can override this method to delete the files created. Caches, similarly, can override this method to delete any other temporary artifacts. """ - pass # noqa: PIE790 # Intentional no-op + pass def _new_batch_id(self) -> str: """Return a new batch handle.""" diff --git a/airbyte-lib/airbyte_lib/_util/protocol_util.py b/airbyte-lib/airbyte_lib/_util/protocol_util.py index 56b28b2c628a1..58ada9f5435b2 100644 --- a/airbyte-lib/airbyte_lib/_util/protocol_util.py +++ b/airbyte-lib/airbyte_lib/_util/protocol_util.py @@ -1,9 +1,9 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. """Internal utility functions, especially for dealing with Airbyte Protocol.""" +from __future__ import annotations -from collections.abc import Iterable, Iterator -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from airbyte_protocol.models import ( AirbyteMessage, @@ -13,6 +13,10 @@ ) +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator + + def airbyte_messages_to_record_dicts( messages: Iterable[AirbyteMessage], ) -> Iterator[dict[str, Any]]: diff --git a/airbyte-lib/airbyte_lib/caches/base.py b/airbyte-lib/airbyte_lib/caches/base.py index bc92ea17a7430..0718220a4a183 100644 --- a/airbyte-lib/airbyte_lib/caches/base.py +++ b/airbyte-lib/airbyte_lib/caches/base.py @@ -1,13 +1,13 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. """A SQL Cache implementation.""" +from __future__ import annotations import abc import enum from collections.abc import Generator, Iterator, Mapping from contextlib import contextmanager -from functools import cached_property, lru_cache -from pathlib import Path +from functools import cached_property from typing import TYPE_CHECKING, Any, cast, final import pandas as pd @@ -16,23 +16,24 @@ import ulid from overrides import overrides from sqlalchemy import CursorResult, Executable, TextClause, create_engine, text -from sqlalchemy.engine import Engine from sqlalchemy.pool import StaticPool -from airbyte_protocol.models import ConfiguredAirbyteStream - from airbyte_lib._file_writers.base import FileWriterBase, FileWriterBatchHandle from airbyte_lib._processors import BatchHandle, RecordProcessor from airbyte_lib.config import CacheConfigBase -from airbyte_lib.telemetry import CacheTelemetryInfo from airbyte_lib.types import SQLTypeConverter if TYPE_CHECKING: - from sqlalchemy.engine import Connection + from pathlib import Path + + from sqlalchemy.engine import Connection, Engine from sqlalchemy.engine.reflection import Inspector + from airbyte_protocol.models import ConfiguredAirbyteStream + from airbyte_lib.datasets._base import DatasetBase + from airbyte_lib.telemetry import CacheTelemetryInfo DEBUG_MODE = False # Set to True to enable additional debug logging. @@ -207,7 +208,7 @@ def get_sql_table( @property def streams( self, - ) -> dict[str, "DatasetBase"]: + ) -> dict[str, DatasetBase]: """Return a temporary table name.""" # TODO: Add support for streams map, based on the cached catalog. raise NotImplementedError("Streams map is not yet supported.") @@ -253,7 +254,7 @@ def _ensure_schema_exists( try: self._execute_sql(sql) - except Exception as ex: # noqa: BLE001 # Too-wide catch because we don't know what the DB will throw. + except Exception as ex: # Ignore schema exists errors. if "already exists" not in str(ex): raise @@ -279,7 +280,6 @@ def _fully_qualified( table_name: str, ) -> str: """Return the fully qualified name of the given table.""" - # return f"{self.database_name}.{self.config.schema_name}.{table_name}" return f"{self.config.schema_name}.{table_name}" @final @@ -325,10 +325,10 @@ def _get_schemas_list( def _ensure_final_table_exists( self, stream_name: str, + *, create_if_missing: bool = True, ) -> str: - """ - Create the final table if it doesn't already exist. + """Create the final table if it doesn't already exist. Return the table name. """ @@ -349,6 +349,7 @@ def _ensure_compatible_table_schema( self, stream_name: str, table_name: str, + *, raise_on_error: bool = False, ) -> bool: """Return true if the given table is compatible with the stream's schema. @@ -460,8 +461,7 @@ def _write_batch( batch_id: str, record_batch: pa.Table | pa.RecordBatch, ) -> FileWriterBatchHandle: - """ - Process a record batch. + """Process a record batch. Return the path to the cache file. """ @@ -559,6 +559,7 @@ def _execute_sql(self, sql: str | TextClause | Executable) -> CursorResult: def _drop_temp_table( self, table_name: str, + *, if_exists: bool = True, ) -> None: """Drop the given table.""" @@ -592,7 +593,7 @@ def _write_files_to_new_table( schema=self.config.schema_name, if_exists="append", index=False, - dtype=self._get_sql_column_definitions(stream_name), # type: ignore + dtype=self._get_sql_column_definitions(stream_name), # type: ignore[arg-type] ) return temp_table_name @@ -649,7 +650,6 @@ def _append_temp_table_to_final_table( """, ) - @lru_cache def _get_primary_keys( self, stream_name: str, diff --git a/airbyte-lib/airbyte_lib/caches/duckdb.py b/airbyte-lib/airbyte_lib/caches/duckdb.py index 0672756584662..0d6ba6efe38a9 100644 --- a/airbyte-lib/airbyte_lib/caches/duckdb.py +++ b/airbyte-lib/airbyte_lib/caches/duckdb.py @@ -115,6 +115,7 @@ def _ensure_compatible_table_schema( self, stream_name: str, table_name: str, + *, raise_on_error: bool = True, ) -> bool: """Return true if the given table is compatible with the stream's schema. @@ -122,7 +123,11 @@ def _ensure_compatible_table_schema( In addition to the base implementation, this also checks primary keys. """ # call super - if not super()._ensure_compatible_table_schema(stream_name, table_name, raise_on_error): + if not super()._ensure_compatible_table_schema( + stream_name=stream_name, + table_name=table_name, + raise_on_error=raise_on_error, + ): return False pk_cols = self._get_primary_keys(stream_name) diff --git a/airbyte-lib/airbyte_lib/caches/postgres.py b/airbyte-lib/airbyte_lib/caches/postgres.py index c833190e00ab7..2ff36d2dbfea1 100644 --- a/airbyte-lib/airbyte_lib/caches/postgres.py +++ b/airbyte-lib/airbyte_lib/caches/postgres.py @@ -23,8 +23,7 @@ class PostgresCacheConfig(SQLCacheConfigBase, ParquetWriterConfig): password: str database: str - # Already defined in base class: - # schema_name: str + # Already defined in base class: `schema_name` @overrides def get_sql_alchemy_url(self) -> str: diff --git a/airbyte-lib/airbyte_lib/datasets/_lazy.py b/airbyte-lib/airbyte_lib/datasets/_lazy.py index e2c22dd0a0580..e08f844c40781 100644 --- a/airbyte-lib/airbyte_lib/datasets/_lazy.py +++ b/airbyte-lib/airbyte_lib/datasets/_lazy.py @@ -1,7 +1,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. +from __future__ import annotations -from collections.abc import Callable, Iterator -from typing import Any +from typing import TYPE_CHECKING, Any from overrides import overrides from typing_extensions import Self @@ -9,6 +9,10 @@ from airbyte_lib.datasets import DatasetBase +if TYPE_CHECKING: + from collections.abc import Callable, Iterator + + class LazyDataset(DatasetBase): """A dataset that is loaded incrementally from a source or a SQL query. diff --git a/airbyte-lib/airbyte_lib/registry.py b/airbyte-lib/airbyte_lib/registry.py index baa4917959bb8..e0afdbaf2c3ac 100644 --- a/airbyte-lib/airbyte_lib/registry.py +++ b/airbyte-lib/airbyte_lib/registry.py @@ -1,8 +1,10 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. +from __future__ import annotations import json import os from dataclasses import dataclass +from pathlib import Path import requests @@ -23,7 +25,7 @@ class ConnectorMetadata: def _update_cache() -> None: global _cache if os.environ.get("AIRBYTE_LOCAL_REGISTRY"): - with open(str(os.environ.get("AIRBYTE_LOCAL_REGISTRY"))) as f: + with Path(str(os.environ.get("AIRBYTE_LOCAL_REGISTRY"))).open() as f: data = json.load(f) else: response = requests.get( @@ -38,8 +40,9 @@ def _update_cache() -> None: def get_connector_metadata(name: str) -> ConnectorMetadata: - """ - check the cache for the connector. If the cache is empty, populate by calling update_cache + """Check the cache for the connector. + + If the cache is empty, populate by calling update_cache. """ if not _cache: _update_cache() diff --git a/airbyte-lib/airbyte_lib/source.py b/airbyte-lib/airbyte_lib/source.py index 0fcf6a535c35e..415284994510e 100644 --- a/airbyte-lib/airbyte_lib/source.py +++ b/airbyte-lib/airbyte_lib/source.py @@ -1,11 +1,10 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. +from __future__ import annotations import json import tempfile -from collections.abc import Generator, Iterable, Iterator -from contextlib import contextmanager -from functools import lru_cache -from typing import Any, Optional +from contextlib import contextmanager, suppress +from typing import TYPE_CHECKING, Any import jsonschema @@ -22,10 +21,8 @@ Type, ) -from airbyte_lib._executor import Executor from airbyte_lib._factories.cache_factories import get_default_cache from airbyte_lib._util import protocol_util # Internal utility functions -from airbyte_lib.caches import SQLCacheBase from airbyte_lib.results import ReadResult from airbyte_lib.telemetry import ( CacheTelemetryInfo, @@ -35,6 +32,13 @@ ) +if TYPE_CHECKING: + from collections.abc import Generator, Iterable, Iterator + + from airbyte_lib._executor import Executor + from airbyte_lib.caches import SQLCacheBase + + @contextmanager def as_temp_files(files: list[Any]) -> Generator[list[Any], Any, None]: temp_files: list[Any] = [] @@ -49,29 +53,29 @@ def as_temp_files(files: list[Any]) -> Generator[list[Any], Any, None]: yield [file.name for file in temp_files] finally: for temp_file in temp_files: - try: + with suppress(Exception): temp_file.close() - except Exception: - pass class Source: - """This class is representing a source that can be called""" + """A class representing a source that can be called.""" def __init__( self, executor: Executor, name: str, - config: Optional[dict[str, Any]] = None, - streams: Optional[list[str]] = None, - ): + config: dict[str, Any] | None = None, + streams: list[str] | None = None, + ) -> None: self._processed_records = 0 self.executor = executor self.name = name - self.streams: Optional[list[str]] = None + self.streams: list[str] | None = None self._processed_records = 0 - self._config_dict: Optional[dict[str, Any]] = None + self._config_dict: dict[str, Any] | None = None self._last_log_messages: list[str] = [] + self._discovered_catalog: AirbyteCatalog | None = None + self._spec: ConnectorSpecification | None = None if config is not None: self.set_config(config) if streams is not None: @@ -82,7 +86,8 @@ def set_streams(self, streams: list[str]) -> None: for stream in streams: if stream not in available_streams: raise Exception( - f"Stream {stream} is not available for connector {self.name}, choose from {available_streams}", + f"Stream {stream} is not available for connector {self.name}. " + f"Choose from: {available_streams}", ) self.streams = streams @@ -99,8 +104,7 @@ def _config(self) -> dict[str, Any]: return self._config_dict def _discover(self) -> AirbyteCatalog: - """ - Call discover on the connector. + """Call discover on the connector. This involves the following steps: * Write the config to a temporary file @@ -117,51 +121,46 @@ def _discover(self) -> AirbyteCatalog: ) def _validate_config(self, config: dict[str, Any]) -> None: - """ - Validate the config against the spec. - """ - spec = self._spec() + """Validate the config against the spec.""" + spec = self._get_spec(force_refresh=False) jsonschema.validate(config, spec.connectionSpecification) def get_available_streams(self) -> list[str]: - """ - Get the available streams from the spec. - """ + """Get the available streams from the spec.""" return [s.name for s in self._discover().streams] - @lru_cache(maxsize=1) - def _spec(self) -> ConnectorSpecification: - """ - Call spec on the connector. + def _get_spec(self, *, force_refresh: bool = False) -> ConnectorSpecification: + """Call spec on the connector. This involves the following steps: * execute the connector with spec * Listen to the messages and return the first AirbyteCatalog that comes along. * Make sure the subprocess is killed when the function returns. """ - for msg in self._execute(["spec"]): - if msg.type == Type.SPEC and msg.spec: - return msg.spec + if force_refresh or self._spec is None: + for msg in self._execute(["spec"]): + if msg.type == Type.SPEC and msg.spec: + self._spec = msg.spec + break + + if self._spec: + return self._spec + raise Exception( f"Connector did not return a spec. Last logs: {self._last_log_messages}", ) @property - @lru_cache(maxsize=1) def raw_catalog(self) -> AirbyteCatalog: - """ - Get the raw catalog for the given streams. - """ - catalog = self._discover() - return catalog + """Get the raw catalog for the given streams.""" + return self._discover() @property - @lru_cache(maxsize=1) def configured_catalog(self) -> ConfiguredAirbyteCatalog: - """ - Get the configured catalog for the given streams. - """ - catalog = self._discover() + """Get the configured catalog for the given streams.""" + if self._discovered_catalog is None: + self._discovered_catalog = self._discover() + return ConfiguredAirbyteCatalog( streams=[ ConfiguredAirbyteStream( @@ -170,14 +169,13 @@ def configured_catalog(self) -> ConfiguredAirbyteCatalog: destination_sync_mode=DestinationSyncMode.overwrite, primary_key=None, ) - for s in catalog.streams + for s in self._discovered_catalog.streams if self.streams is None or s.name in self.streams ], ) def get_records(self, stream: str) -> Iterator[dict[str, Any]]: - """ - Read a stream from the connector. + """Read a stream from the connector. This involves the following steps: * Call discover to get the catalog @@ -211,8 +209,7 @@ def get_records(self, stream: str) -> Iterator[dict[str, Any]]: yield from iterator # TODO: Refactor to use LazyDataset here def check(self) -> None: - """ - Call check on the connector. + """Call check on the connector. This involves the following steps: * Write the config to a temporary file @@ -223,25 +220,22 @@ def check(self) -> None: with as_temp_files([self._config]) as [config_file]: for msg in self._execute(["check", "--config", config_file]): if msg.type == Type.CONNECTION_STATUS and msg.connectionStatus: - if msg.connectionStatus.status == Status.FAILED: - raise Exception( - f"Connector returned failed status: {msg.connectionStatus.message}", - ) - else: - return + if msg.connectionStatus.status != Status.FAILED: + return # Success! + + raise Exception( + f"Connector returned failed status: {msg.connectionStatus.message}", + ) raise Exception( f"Connector did not return check status. Last logs: {self._last_log_messages}", ) def install(self) -> None: - """ - Install the connector if it is not yet installed. - """ + """Install the connector if it is not yet installed.""" self.executor.install() def uninstall(self) -> None: - """ - Uninstall the connector if it is installed. + """Uninstall the connector if it is installed. This only works if the use_local_install flag wasn't used and installation is managed by airbyte-lib. @@ -278,14 +272,14 @@ def _read_with_catalog( cache_info: CacheTelemetryInfo, catalog: ConfiguredAirbyteCatalog, ) -> Iterator[AirbyteMessage]: - """ - Call read on the connector. + """Call read on the connector. This involves the following steps: * Write the config to a temporary file * execute the connector with read --config --catalog * Listen to the messages and return the AirbyteRecordMessages that come along. - * Send out telemetry on the performed sync (with information about which source was used and the type of the cache) + * Send out telemetry on the performed sync (with information about which source was used and + the type of the cache) """ source_tracking_information = self.executor.get_telemetry_info() send_telemetry(source_tracking_information, cache_info, SyncState.STARTED) @@ -294,15 +288,14 @@ def _read_with_catalog( config_file, catalog_file, ]: - for msg in self._execute( + yield from self._execute( ["read", "--config", config_file, "--catalog", catalog_file], - ): - yield msg - except Exception as e: + ) + except Exception: send_telemetry( source_tracking_information, cache_info, SyncState.FAILED, self._processed_records ) - raise e + raise finally: send_telemetry( source_tracking_information, @@ -316,15 +309,14 @@ def _add_to_logs(self, message: str) -> None: self._last_log_messages = self._last_log_messages[-10:] def _execute(self, args: list[str]) -> Iterator[AirbyteMessage]: - """ - Execute the connector with the given arguments. + """Execute the connector with the given arguments. This involves the following steps: * Locate the right venv. It is called ".venv-" * Spawn a subprocess with .venv-/bin/ - * Read the output line by line of the subprocess and serialize them AirbyteMessage objects. Drop if not valid. + * Read the output line by line of the subprocess and serialize them AirbyteMessage objects. + Drop if not valid. """ - self.executor.ensure_installation() try: @@ -338,7 +330,7 @@ def _execute(self, args: list[str]) -> Iterator[AirbyteMessage]: except Exception: self._add_to_logs(line) except Exception as e: - raise Exception(f"{e!s}. Last logs: {self._last_log_messages}") + raise Exception(f"Execution failed. Last logs: {self._last_log_messages}") from e def _tally_records( self, diff --git a/airbyte-lib/airbyte_lib/telemetry.py b/airbyte-lib/airbyte_lib/telemetry.py index fe797c9d82613..5ab816208383e 100644 --- a/airbyte-lib/airbyte_lib/telemetry.py +++ b/airbyte-lib/airbyte_lib/telemetry.py @@ -1,11 +1,12 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. +from __future__ import annotations import datetime import os from contextlib import suppress from dataclasses import asdict, dataclass from enum import Enum -from typing import Any, Optional +from typing import Any import requests @@ -13,7 +14,8 @@ # TODO: Use production tracking key -TRACKING_KEY = "jxT1qP9WEKwR3vtKMwP9qKhfQEGFtIM1" or str(os.environ.get("AIRBYTE_TRACKING_KEY")) +# TODO: This 'or' is a no-op. Intentional? Should we switch order to prefer env var if available? +TRACKING_KEY = "jxT1qP9WEKwR3vtKMwP9qKhfQEGFtIM1" or str(os.environ.get("AIRBYTE_TRACKING_KEY")) # noqa: SIM222 class SourceType(str, Enum): @@ -39,20 +41,20 @@ class SyncState(str, Enum): class SourceTelemetryInfo: name: str type: SourceType - version: Optional[str] + version: str | None def send_telemetry( source_info: SourceTelemetryInfo, cache_info: CacheTelemetryInfo, state: SyncState, - number_of_records: Optional[int] = None, + number_of_records: int | None = None, ) -> None: # If DO_NOT_TRACK is set, we don't send any telemetry if os.environ.get("DO_NOT_TRACK"): return - current_time = datetime.datetime.utcnow().isoformat() + current_time: str = datetime.datetime.utcnow().isoformat() # noqa: DTZ003 # prefer now() over utcnow() payload: dict[str, Any] = { "anonymousId": "airbyte-lib-user", "event": "sync", diff --git a/airbyte-lib/airbyte_lib/types.py b/airbyte-lib/airbyte_lib/types.py index eb87740a75cf3..ca34a5801e0bd 100644 --- a/airbyte-lib/airbyte_lib/types.py +++ b/airbyte-lib/airbyte_lib/types.py @@ -1,6 +1,7 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. """Type conversion methods for SQL Caches.""" +from __future__ import annotations from typing import cast diff --git a/airbyte-lib/airbyte_lib/validate.py b/airbyte-lib/airbyte_lib/validate.py index f8aa646d86fc9..8eac20e1692b4 100644 --- a/airbyte-lib/airbyte_lib/validate.py +++ b/airbyte-lib/airbyte_lib/validate.py @@ -1,5 +1,8 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. -"""Defines the `airbyte-lib-validate-source` CLI, which checks if connectors are compatible with airbyte-lib.""" +"""Defines the `airbyte-lib-validate-source` CLI. + +This tool checks if connectors are compatible with airbyte-lib. +""" import argparse import json @@ -40,7 +43,7 @@ def _run_subprocess_and_raise_on_failure(args: list[str]) -> None: def tests(connector_name: str, sample_config: str) -> None: print("Creating source and validating spec and version...") source = ab.get_connector( - # FIXME: noqa: SIM115, PTH123 + # TODO: FIXME: noqa: SIM115, PTH123 connector_name, config=json.load(open(sample_config)), # noqa: SIM115, PTH123 ) @@ -65,15 +68,16 @@ def tests(connector_name: str, sample_config: str) -> None: def run() -> None: - """ - This is a CLI entrypoint for the `airbyte-lib-validate-source` command. - It's called like this: airbyte-lib-validate-source —connector-dir . -—sample-config secrets/config.json + """Handle CLI entrypoint for the `airbyte-lib-validate-source` command. + + It's called like this: + > airbyte-lib-validate-source —connector-dir . -—sample-config secrets/config.json + It performs a basic smoke test to make sure the connector in question is airbyte-lib compliant: * Can be installed into a venv * Can be called via cli entrypoint - * Answers according to the Airbyte protocol when called with spec, check, discover and read + * Answers according to the Airbyte protocol when called with spec, check, discover and read. """ - # parse args args = _parse_args() connector_dir = args.connector_dir @@ -84,7 +88,7 @@ def run() -> None: def validate(connector_dir: str, sample_config: str) -> None: # read metadata.yaml metadata_path = Path(connector_dir) / "metadata.yaml" - with open(metadata_path) as stream: + with Path(metadata_path).open() as stream: metadata = yaml.safe_load(stream)["data"] # TODO: Use remoteRegistries.pypi.packageName once set for connectors @@ -96,7 +100,7 @@ def validate(connector_dir: str, sample_config: str) -> None: if not venv_path.exists(): _run_subprocess_and_raise_on_failure([sys.executable, "-m", "venv", venv_name]) - pip_path = os.path.join(venv_name, "bin", "pip") + pip_path = str(venv_path / "bin" / "pip") _run_subprocess_and_raise_on_failure([pip_path, "install", "-e", connector_dir]) diff --git a/airbyte-lib/docs.py b/airbyte-lib/docs.py index a80ad6e4a8a12..bfd30c05e554f 100644 --- a/airbyte-lib/docs.py +++ b/airbyte-lib/docs.py @@ -7,20 +7,20 @@ import pdoc -def run(): - """ - Generate docs for all public modules in airbyte_lib and save them to docs/generated. +def run() -> None: + """Generate docs for all public modules in airbyte_lib and save them to docs/generated. + Public modules are: * The main airbyte_lib module - * All directory modules in airbyte_lib that don't start with an underscore + * All directory modules in airbyte_lib that don't start with an underscore. """ public_modules = ["airbyte_lib"] # recursively delete the docs/generated folder if it exists - if os.path.exists("docs/generated"): + if pathlib.Path("docs/generated").exists(): shutil.rmtree("docs/generated") - # determine all folders in airbyte_lib that don't start with an underscore and add them to public_modules + # All folders in `airbyte_lib` that don't start with "_" are treated as public modules. for d in os.listdir("airbyte_lib"): dir_path = pathlib.Path(f"airbyte_lib/{d}") if dir_path.is_dir() and not d.startswith("_"): diff --git a/airbyte-lib/docs/generated/airbyte_lib.html b/airbyte-lib/docs/generated/airbyte_lib.html index 15ce24f714eef..240492002ff83 100644 --- a/airbyte-lib/docs/generated/airbyte_lib.html +++ b/airbyte-lib/docs/generated/airbyte_lib.html @@ -4,7 +4,7 @@
def - get_connector( name: str, version: str | None = None, pip_url: str | None = None, config: dict[str, typing.Any] | None = None, use_local_install: bool = False, install_if_missing: bool = True) -> Source: + get_connector( name: str, version: str | None = None, pip_url: str | None = None, config: dict[str, typing.Any] | None = None, *, use_local_install: bool = False, install_if_missing: bool = True) -> Source:
@@ -12,16 +12,20 @@

Get a connector by name and version.

-
Parameters
- -
    -
  • name: connector name
  • -
  • version: connector version - if not provided, the currently installed version will be used. If no version is installed, the latest available version will be used. The version can also be set to "latest" to force the use of the latest available version.
  • -
  • pip_url: connector pip URL - if not provided, the pip url will be inferred from the connector name.
  • -
  • config: connector config - if not provided, you need to set it later via the set_config method.
  • -
  • use_local_install: whether to use a virtual environment to run the connector. If True, the connector is expected to be available on the path (e.g. installed via pip). If False, the connector will be installed automatically in a virtual environment.
  • -
  • install_if_missing: whether to install the connector if it is not available locally. This parameter is ignored if use_local_install is True.
  • -
+

Args: + name: connector name + version: connector version - if not provided, the currently installed version will be used. + If no version is installed, the latest available version will be used. The version can + also be set to "latest" to force the use of the latest available version. + pip_url: connector pip URL - if not provided, the pip url will be inferred from the + connector name. + config: connector config - if not provided, you need to set it later via the set_config + method. + use_local_install: whether to use a virtual environment to run the connector. If True, the + connector is expected to be available on the path (e.g. installed via pip). If False, + the connector will be installed automatically in a virtual environment. + install_if_missing: whether to install the connector if it is not available locally. This + parameter is ignored if use_local_install is True.

@@ -48,7 +52,7 @@
Parameters
def - new_local_cache( cache_name: str | None = None, cache_dir: str | pathlib.Path | None = None, cleanup: bool = True) -> airbyte_lib.caches.duckdb.DuckDBCache: + new_local_cache( cache_name: str | None = None, cache_dir: str | pathlib.Path | None = None, *, cleanup: bool = True) -> airbyte_lib.caches.duckdb.DuckDBCache:
@@ -58,7 +62,7 @@
Parameters

Args: cache_name: Name to use for the cache. Defaults to None. - root_dir: Root directory to store the cache in. Defaults to None. + cache_dir: Root directory to store the cache in. Defaults to None. cleanup: Whether to clean up temporary files. Defaults to True.

Cache files are stored in the .cache directory, relative to the current @@ -192,14 +196,14 @@

Parameters
-

This class is representing a source that can be called

+

A class representing a source that can be called.

- Source( executor: airbyte_lib._executor.Executor, name: str, config: Optional[dict[str, Any]] = None, streams: Optional[list[str]] = None) + Source( executor: airbyte_lib._executor.Executor, name: str, config: dict[str, typing.Any] | None = None, streams: list[str] | None = None)
@@ -232,7 +236,7 @@
Parameters
- streams: Optional[list[str]] + streams: list[str] | None
diff --git a/airbyte-lib/docs/generated/airbyte_lib/factories.html b/airbyte-lib/docs/generated/airbyte_lib/factories.html deleted file mode 100644 index c0d27ca14eaa0..0000000000000 --- a/airbyte-lib/docs/generated/airbyte_lib/factories.html +++ /dev/null @@ -1,7 +0,0 @@ - -
-
- - - - \ No newline at end of file diff --git a/airbyte-lib/poetry.lock b/airbyte-lib/poetry.lock index 58fb0cbf88c71..a201b487b774a 100644 --- a/airbyte-lib/poetry.lock +++ b/airbyte-lib/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "airbyte-cdk" -version = "0.58.3" +version = "0.58.7" description = "A framework for writing Airbyte Connectors." optional = false python-versions = ">=3.8" files = [ - {file = "airbyte-cdk-0.58.3.tar.gz", hash = "sha256:da75898d1503d8bd06840cb0c10a06f6a0ebcc77858deca146de34e392b01ede"}, - {file = "airbyte_cdk-0.58.3-py3-none-any.whl", hash = "sha256:00f81ebe7d8c7be724ea2c8f364f31803de2345d1ccbf9cdcad808562e512b7b"}, + {file = "airbyte-cdk-0.58.7.tar.gz", hash = "sha256:00e379e2379b38683992027114a2190f49befec8cbac67d0a2c907786111e77b"}, + {file = "airbyte_cdk-0.58.7-py3-none-any.whl", hash = "sha256:09b31d32899cc6dc91e39716e8d1601503a7884d837752e683d1e3ef7dfe73be"}, ] [package.dependencies] @@ -524,13 +524,13 @@ six = "*" [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.3" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [package.dependencies] @@ -1386,7 +1386,147 @@ mypy = [ {version = ">=0.900", markers = "python_version >= \"3.11\""}, {version = ">=0.780", markers = "python_version >= \"3.9\" and python_version < \"3.11\""}, ] -pytest = {version = ">=6.2", markers = "python_version >= \"3.10\""} +pytest = [ + {version = ">=6.2", markers = "python_version >= \"3.10\""}, + {version = ">=4.6", markers = "python_version >= \"3.6\" and python_version < \"3.10\""}, +] + +[[package]] +name = "pytest-postgresql" +version = "5.0.0" +description = "Postgresql fixtures and fixture factories for Pytest." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-postgresql-5.0.0.tar.gz", hash = "sha256:22edcbafab8995ee85b8d948ddfaad4f70c2c7462303d7477ecd2f77fc9d15bd"}, + {file = "pytest_postgresql-5.0.0-py3-none-any.whl", hash = "sha256:6e8f0773b57c9b8975b6392c241b7b81b7018f32079a533f368f2fbda732ecd3"}, +] + +[package.dependencies] +mirakuru = "*" +port-for = ">=0.6.0" +psycopg = ">=3.0.0" +pytest = ">=6.2" +setuptools = "*" + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-ulid" +version = "2.2.0" +description = "Universally unique lexicographically sortable identifier" +optional = false +python-versions = ">=3.9" +files = [ + {file = "python_ulid-2.2.0-py3-none-any.whl", hash = "sha256:ec2e69292c0b7c338a07df5e15b05270be6823675c103383e74d1d531945eab5"}, + {file = "python_ulid-2.2.0.tar.gz", hash = "sha256:9ec777177d396880d94be49ac7eb4ae2cd4a7474448bfdbfe911537add970aeb"}, +] + +[[package]] +name = "pytz" +version = "2023.3.post1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, +] + +[[package]] +name = "pytzdata" +version = "2020.1" +description = "The Olson timezone database for Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"}, + {file = "pytzdata-2020.1.tar.gz", hash = "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540"}, +] + +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] [[package]] name = "pytest-postgresql" @@ -1701,28 +1841,28 @@ files = [ [[package]] name = "ruff" -version = "0.1.11" +version = "0.1.12" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a7f772696b4cdc0a3b2e527fc3c7ccc41cdcb98f5c80fdd4f2b8c50eb1458196"}, - {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:934832f6ed9b34a7d5feea58972635c2039c7a3b434fe5ba2ce015064cb6e955"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea0d3e950e394c4b332bcdd112aa566010a9f9c95814844a7468325290aabfd9"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bd4025b9c5b429a48280785a2b71d479798a69f5c2919e7d274c5f4b32c3607"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ad00662305dcb1e987f5ec214d31f7d6a062cae3e74c1cbccef15afd96611d"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4b077ce83f47dd6bea1991af08b140e8b8339f0ba8cb9b7a484c30ebab18a23f"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a88efecec23c37b11076fe676e15c6cdb1271a38f2b415e381e87fe4517f18"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b25093dad3b055667730a9b491129c42d45e11cdb7043b702e97125bcec48a1"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231d8fb11b2cc7c0366a326a66dafc6ad449d7fcdbc268497ee47e1334f66f77"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:09c415716884950080921dd6237767e52e227e397e2008e2bed410117679975b"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f58948c6d212a6b8d41cd59e349751018797ce1727f961c2fa755ad6208ba45"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:190a566c8f766c37074d99640cd9ca3da11d8deae2deae7c9505e68a4a30f740"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6464289bd67b2344d2a5d9158d5eb81025258f169e69a46b741b396ffb0cda95"}, - {file = "ruff-0.1.11-py3-none-win32.whl", hash = "sha256:9b8f397902f92bc2e70fb6bebfa2139008dc72ae5177e66c383fa5426cb0bf2c"}, - {file = "ruff-0.1.11-py3-none-win_amd64.whl", hash = "sha256:eb85ee287b11f901037a6683b2374bb0ec82928c5cbc984f575d0437979c521a"}, - {file = "ruff-0.1.11-py3-none-win_arm64.whl", hash = "sha256:97ce4d752f964ba559c7023a86e5f8e97f026d511e48013987623915431c7ea9"}, - {file = "ruff-0.1.11.tar.gz", hash = "sha256:f9d4d88cb6eeb4dfe20f9f0519bd2eaba8119bde87c3d5065c541dbae2b5a2cb"}, + {file = "ruff-0.1.12-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:544038693543c11edc56bb94a9875df2dc249e3616f90c15964c720dcccf0745"}, + {file = "ruff-0.1.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8a0e3ef6299c4eab75a7740730e4b4bd4a36e0bd8102ded01553403cad088fd4"}, + {file = "ruff-0.1.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47f6d939461e3273f10f4cd059fd0b83c249d73f1736032fffbac83a62939395"}, + {file = "ruff-0.1.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25be18abc1fc3f3d3fb55855c41ed5d52063316defde202f413493bb3888218c"}, + {file = "ruff-0.1.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d41e9f100b50526d80b076fc9c103c729387ff3f10f63606ed1038c30a372a40"}, + {file = "ruff-0.1.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:472a0548738d4711549c7874b43fab61aacafb1fede29c5232d4cfb8e2d13f69"}, + {file = "ruff-0.1.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46685ef2f106b827705df876d38617741ed4f858bbdbc0817f94476c45ab6669"}, + {file = "ruff-0.1.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf6073749c70b616d7929897b14824ec6713a6c3a8195dfd2ffdcc66594d880c"}, + {file = "ruff-0.1.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bdf26e5a2efab4c3aaf6b61648ea47a525dc12775810a85c285dc9ca03e5ac0"}, + {file = "ruff-0.1.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b631c6a95e4b6d5c4299e599067b5a89f5b18e2f2d9a6c22b879b3c4b077c96e"}, + {file = "ruff-0.1.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f193f460e231e63af5fc7516897cf5ab257cbda72ae83cf9a654f1c80c3b758a"}, + {file = "ruff-0.1.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:718523c3a0b787590511f212d30cc9b194228ef369c8bdd72acd1282cc27c468"}, + {file = "ruff-0.1.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1c49e826de55d81a6ef93808b760925e492bad7cc470aaa114a3be158b2c7f99"}, + {file = "ruff-0.1.12-py3-none-win32.whl", hash = "sha256:fbb1c002eeacb60161e51d77b2274c968656599477a1c8c65066953276e8ee2b"}, + {file = "ruff-0.1.12-py3-none-win_amd64.whl", hash = "sha256:7fe06ba77e5b7b78db1d058478c47176810f69bb5be7c1b0d06876af59198203"}, + {file = "ruff-0.1.12-py3-none-win_arm64.whl", hash = "sha256:bb29f8e3e6c95024902eaec5a9ce1fd5ac4e77f4594f4554e67fbb0f6d9a2f37"}, + {file = "ruff-0.1.12.tar.gz", hash = "sha256:97189f38c655e573f6bea0d12e9f18aad5539fd08ab50651449450999f45383a"}, ] [[package]] @@ -2045,5 +2185,5 @@ files = [ [metadata] lock-version = "2.0" -python-versions = "^3.10" -content-hash = "a349c7b28e4ed7cb240906e522e373f6b00a5f2ee4ed4138567a0ed094491a30" +python-versions = "^3.9" +content-hash = "65721067d8262d014d4b36deced214a27a8c2bdef7e5cf25eed709619cfa3716" diff --git a/airbyte-lib/pyproject.toml b/airbyte-lib/pyproject.toml index 84f492f0bba31..ac1042b33d722 100644 --- a/airbyte-lib/pyproject.toml +++ b/airbyte-lib/pyproject.toml @@ -7,7 +7,7 @@ readme = "README.md" packages = [{include = "airbyte_lib"}] [tool.poetry.dependencies] -python = "^3.10" +python = "^3.9" airbyte-cdk = "^0.58.3" # airbyte-protocol-models = "^1.0.1" # Conflicts with airbyte-cdk # TODO: delete or resolve @@ -50,28 +50,97 @@ build-backend = "poetry.core.masonry.api" max-args = 8 # Relaxed from default of 5 [tool.ruff] -target-version = "py310" -select = ["F", "E"] -extend-select = [ - "W", "C90", "I", "N", "UP", "YTT", "ANN", "ASYNC", "BLE", "B", "A", "COM", "C4", "EXE", "FA", "ISC", "ICN", "INP", "PIE", "PYI", "PT", "Q", "RSE", "RET", "SLF", "SLOT", "SIM", "TID", "TCH", "INT", "ARG", "PTH", "TD", "FIX", "PD", "PL", "TRY", "FLY", "NPY", "PERF", "RUF" +target-version = "py39" +select = [ + # For rules reference, see https://docs.astral.sh/ruff/rules/ + "A", # flake8-builtins + "ANN", # flake8-annotations + "ARG", # flake8-unused-arguments + "ASYNC", # flake8-async + "B", # flake8-bugbear + "FBT", # flake8-boolean-trap + "BLE", # Blind except + "C4", # flake8-comprehensions + "C90", # mccabe (complexity) + "COM", # flake8-commas + "CPY", # missing copyright notice + # "D", # pydocstyle # TODO: Re-enable when adding docstrings + "DTZ", # flake8-datetimez + "E", # pycodestyle (errors) + "ERA", # flake8-eradicate (commented out code) + "EXE", # flake8-executable + "F", # Pyflakes + "FA", # flake8-future-annotations + "FIX", # flake8-fixme + "FLY", # flynt + "FURB", # Refurb + "I", # isort + "ICN", # flake8-import-conventions + "INP", # flake8-no-pep420 + "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "LOG", # flake8-logging + "N", # pep8-naming + "PD", # pandas-vet + "PERF", # Perflint + "PIE", # flake8-pie + "PGH", # pygrep-hooks + "PL", # Pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "PYI", # flake8-pyi + "Q", # flake8-quotes + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "T10", # debugger calls + # "T20", # flake8-print # TODO: Re-enable once we have logging + "TCH", # flake8-type-checking + "TD", # flake8-todos + "TID", # flake8-tidy-imports + "TRY", # tryceratops + "UP", # pyupgrade + "W", # pycodestyle (warnings) + "YTT" # flake8-2020 ] ignore = [ # For rules reference, see https://docs.astral.sh/ruff/rules/ - # "I001", # Sorted imports - # "ANN401", # Any-typed expressions + + # These we don't agree with or don't want to prioritize to enforce: "ANN003", # kwargs missing type annotations - # "SIM300", # 'yoda' conditions - "PERF203", # exception handling in loop "ANN101", # Type annotations for 'self' args - "TD002", # Require author for TODOs - "TD003", # Require links for TODOs - "B019", # lru_cache on class methods keep instance from getting garbage collected - "COM812", # Conflicts with ruff auto-format + "COM812", # Because it conflicts with ruff auto-format + "EM", # flake8-errmsgs (may reconsider later) + "DJ", # Django linting + "G", # flake8-logging-format "ISC001", # Conflicts with ruff auto-format - "TRY003" # Raising exceptions with too-long string descriptions # TODO: re-evaluate once we have our own exception classes + "NPY", # NumPy-specific rules + "PIE790", # Allow unnecssary 'pass' (sometimes useful for readability) + "PERF203", # exception handling in loop + "S", # flake8-bandit (noisy, security related) + "TD002", # Require author for TODOs + "TRIO", # flake8-trio (opinionated, noisy) + "TRY003", # Exceptions with too-long string descriptions # TODO: re-evaluate once we have our own exception classes + "INP001", # Dir 'examples' is part of an implicit namespace package. Add an __init__.py. + + # TODO: Consider re-enabling these before release: + "A003", # Class attribute 'type' is shadowing a Python builtin + "BLE001", # Do not catch blind exception: Exception + "ERA001", # Remove commented-out code + "FIX002", # Allow "TODO:" until release (then switch to requiring links via TDO003) + "PLW0603", # Using the global statement to update _cache is discouraged + "TD003", # Require links for TODOs # TODO: Re-enable when we disable FIX002 + "TRY002", # TODO: When we have time to tackle exception management ("Create your own exception") ] fixable = ["ALL"] -unfixable = [] +unfixable = [ + "ERA001", # Commented-out code (avoid silent loss of code) + "T201" # print() calls (avoid silent loss of code / log messages) +] line-length = 100 extend-exclude = ["docs", "test", "tests"] dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" @@ -97,7 +166,7 @@ max-complexity = 24 ignore-overlong-task-comments = true [tool.ruff.pydocstyle] -convention = "numpy" +convention = "google" [tool.ruff.flake8-annotations] allow-star-arg-any = false @@ -113,7 +182,7 @@ docstring-code-format = true [tool.mypy] # Platform configuration -python_version = "3.10" +python_version = "3.9" # imports related ignore_missing_imports = true follow_imports = "silent" diff --git a/airbyte-lib/tests/lint_tests/test_ruff.py b/airbyte-lib/tests/lint_tests/test_ruff.py index 5f654d7b11e4d..57262a8f608c4 100644 --- a/airbyte-lib/tests/lint_tests/test_ruff.py +++ b/airbyte-lib/tests/lint_tests/test_ruff.py @@ -4,16 +4,7 @@ import pytest -XFAIL = True # Toggle to set if the test is expected to fail or not - -@pytest.mark.xfail( - condition=XFAIL, - reason=( - "This is expected to fail until Ruff cleanup is completed.\n" - "In the meanwhile, use `poetry run ruff check --fix .` to find and fix issues." - ), -) def test_ruff_linting(): # Run the check command check_result = subprocess.run( From be09dfe919c5dde188b38e5ab87e02fbf55fe893 Mon Sep 17 00:00:00 2001 From: Raghav Gupta <43565099+Marcus0086@users.noreply.github.com> Date: Wed, 17 Jan 2024 16:41:25 +0530 Subject: [PATCH 126/574] =?UTF-8?q?=F0=9F=90=9B=20Destination=20Weaviate:?= =?UTF-8?q?=20Multi=20Tenancy=20Support=20(#34229)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joe Reuter --- .../destination_weaviate/config.py | 1 + .../destination_weaviate/indexer.py | 75 +++- .../integration_tests/spec.json | 385 ++++++++++-------- .../destination-weaviate/metadata.yaml | 2 +- .../unit_tests/indexer_test.py | 77 +++- docs/integrations/destinations/weaviate.md | 3 + 6 files changed, 328 insertions(+), 215 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/config.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/config.py index 6c580102e7c36..c4708d59ffc9a 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/config.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/config.py @@ -65,6 +65,7 @@ class WeaviateIndexingConfigModel(BaseModel): ) batch_size: int = Field(title="Batch Size", description="The number of records to send to Weaviate in each batch", default=128) text_field: str = Field(title="Text Field", description="The field in the object that contains the embedded text", default="text") + tenant_id: str = Field(title="Tenant ID", description="The tenant ID to use for multi tenancy", airbyte_secret=True, default="") default_vectorizer: str = Field( title="Default Vectorizer", description="The vectorizer to use if new classes need to be created", diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/indexer.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/indexer.py index 45c54d54bfed6..93adb9d825a4f 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/indexer.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/indexer.py @@ -52,6 +52,14 @@ def _create_client(self): batch_size=None, dynamic=False, weaviate_error_retries=weaviate.WeaviateErrorRetryConf(number_retries=5) ) + def _add_tenant_to_class_if_missing(self, class_name: str): + class_tenants = self.client.schema.get_class_tenants(class_name=class_name) + if class_tenants is not None and self.config.tenant_id not in [tenant.name for tenant in class_tenants]: + self.client.schema.add_class_tenants(class_name=class_name, tenants=[weaviate.Tenant(name=self.config.tenant_id)]) + logging.info(f"Added tenant {self.config.tenant_id} to class {class_name}") + else: + logging.info(f"Tenant {self.config.tenant_id} already exists in class {class_name}") + def check(self) -> Optional[str]: deployment_mode = os.environ.get("DEPLOYMENT_MODE", "") if deployment_mode.casefold() == CLOUD_DEPLOYMENT_MODE and not self._uses_safe_config(): @@ -69,6 +77,11 @@ def pre_sync(self, catalog: ConfiguredAirbyteCatalog) -> None: self._create_client() classes = {c["class"]: c for c in self.client.schema.get().get("classes", [])} self.has_record_id_metadata = defaultdict(lambda: False) + + if self.config.tenant_id.strip(): + for class_name in classes.keys(): + self._add_tenant_to_class_if_missing(class_name) + for stream in catalog.streams: class_name = self._stream_to_class_name(stream.stream.name) schema = classes[class_name] if class_name in classes else None @@ -78,24 +91,29 @@ def pre_sync(self, catalog: ConfiguredAirbyteCatalog) -> None: self.client.schema.create_class(schema) logging.info(f"Recreated class {class_name}") elif class_name not in classes: - self.client.schema.create_class( - { - "class": class_name, - "vectorizer": self.config.default_vectorizer, - "properties": [ - { - # Record ID is used for bookkeeping, not for searching - "name": METADATA_RECORD_ID_FIELD, - "dataType": ["text"], - "description": "Record ID, used for bookkeeping.", - "indexFilterable": True, - "indexSearchable": False, - "tokenization": "field", - } - ], - } - ) + config = { + "class": class_name, + "vectorizer": self.config.default_vectorizer, + "properties": [ + { + # Record ID is used for bookkeeping, not for searching + "name": METADATA_RECORD_ID_FIELD, + "dataType": ["text"], + "description": "Record ID, used for bookkeeping.", + "indexFilterable": True, + "indexSearchable": False, + "tokenization": "field", + } + ], + } + if self.config.tenant_id.strip(): + config["multiTenancyConfig"] = {"enabled": True} + + self.client.schema.create_class(config) logging.info(f"Created class {class_name}") + + if self.config.tenant_id.strip(): + self._add_tenant_to_class_if_missing(class_name) else: self.has_record_id_metadata[class_name] = schema is not None and any( prop.get("name") == METADATA_RECORD_ID_FIELD for prop in schema.get("properties", {}) @@ -105,10 +123,18 @@ def delete(self, delete_ids, namespace, stream): if len(delete_ids) > 0: class_name = self._stream_to_class_name(stream) if self.has_record_id_metadata[class_name]: - self.client.batch.delete_objects( - class_name=class_name, - where={"path": [METADATA_RECORD_ID_FIELD], "operator": "ContainsAny", "valueStringArray": delete_ids}, - ) + where_filter = {"path": [METADATA_RECORD_ID_FIELD], "operator": "ContainsAny", "valueStringArray": delete_ids} + if self.config.tenant_id.strip(): + self.client.batch.delete_objects( + class_name=class_name, + tenant=self.config.tenant_id, + where=where_filter, + ) + else: + self.client.batch.delete_objects( + class_name=class_name, + where=where_filter, + ) def index(self, document_chunks, namespace, stream): if len(document_chunks) == 0: @@ -124,7 +150,12 @@ def index(self, document_chunks, namespace, stream): weaviate_object[self.config.text_field] = chunk.page_content object_id = str(uuid.uuid4()) class_name = self._stream_to_class_name(chunk.record.stream) - self.client.batch.add_data_object(weaviate_object, class_name, object_id, vector=chunk.embedding) + if self.config.tenant_id.strip(): + self.client.batch.add_data_object( + weaviate_object, class_name, object_id, vector=chunk.embedding, tenant=self.config.tenant_id + ) + else: + self.client.batch.add_data_object(weaviate_object, class_name, object_id, vector=chunk.embedding) self._flush() def _stream_to_class_name(self, stream_name: str) -> str: diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/spec.json b/airbyte-integrations/connectors/destination-weaviate/integration_tests/spec.json index 3923a8851c4db..a5db30c7213df 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/spec.json +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/spec.json @@ -5,164 +5,6 @@ "description": "The configuration model for the Vector DB based destinations. This model is used to generate the UI for the destination configuration,\nas well as to provide type safety for the configuration passed to the destination.\n\nThe configuration model is composed of four parts:\n* Processing configuration\n* Embedding configuration\n* Indexing configuration\n* Advanced configuration\n\nProcessing, embedding and advanced configuration are provided by this base class, while the indexing configuration is provided by the destination connector in the sub class.", "type": "object", "properties": { - "processing": { - "title": "ProcessingConfigModel", - "type": "object", - "properties": { - "chunk_size": { - "title": "Chunk size", - "description": "Size of chunks in tokens to store in vector store (make sure it is not too big for the context if your LLM)", - "minimum": 1, - "maximum": 8191, - "type": "integer" - }, - "chunk_overlap": { - "title": "Chunk overlap", - "description": "Size of overlap between chunks in tokens to store in vector store to better capture relevant context", - "default": 0, - "type": "integer" - }, - "text_fields": { - "title": "Text fields to embed", - "description": "List of fields in the record that should be used to calculate the embedding. The field list is applied to all streams in the same way and non-existing fields are ignored. If none are defined, all fields are considered text fields. When specifying text fields, you can access nested fields in the record by using dot notation, e.g. `user.name` will access the `name` field in the `user` object. It's also possible to use wildcards to access all fields in an object, e.g. `users.*.name` will access all `names` fields in all entries of the `users` array.", - "default": [], - "always_show": true, - "examples": ["text", "user.name", "users.*.name"], - "type": "array", - "items": { "type": "string" } - }, - "metadata_fields": { - "title": "Fields to store as metadata", - "description": "List of fields in the record that should be stored as metadata. The field list is applied to all streams in the same way and non-existing fields are ignored. If none are defined, all fields are considered metadata fields. When specifying text fields, you can access nested fields in the record by using dot notation, e.g. `user.name` will access the `name` field in the `user` object. It's also possible to use wildcards to access all fields in an object, e.g. `users.*.name` will access all `names` fields in all entries of the `users` array. When specifying nested paths, all matching values are flattened into an array set to a field named by the path.", - "default": [], - "always_show": true, - "examples": ["age", "user", "user.name"], - "type": "array", - "items": { "type": "string" } - }, - "field_name_mappings": { - "title": "Field name mappings", - "description": "List of fields to rename. Not applicable for nested fields, but can be used to rename fields already flattened via dot notation.", - "default": [], - "type": "array", - "items": { - "title": "FieldNameMappingConfigModel", - "type": "object", - "properties": { - "from_field": { - "title": "From field name", - "description": "The field name in the source", - "type": "string" - }, - "to_field": { - "title": "To field name", - "description": "The field name to use in the destination", - "type": "string" - } - }, - "required": ["from_field", "to_field"] - } - }, - "text_splitter": { - "title": "Text splitter", - "description": "Split text fields into chunks based on the specified method.", - "type": "object", - "oneOf": [ - { - "title": "By Separator", - "type": "object", - "properties": { - "mode": { - "title": "Mode", - "default": "separator", - "const": "separator", - "enum": ["separator"], - "type": "string" - }, - "separators": { - "title": "Separators", - "description": "List of separator strings to split text fields by. The separator itself needs to be wrapped in double quotes, e.g. to split by the dot character, use \".\". To split by a newline, use \"\\n\".", - "default": ["\"\\n\\n\"", "\"\\n\"", "\" \"", "\"\""], - "type": "array", - "items": { "type": "string" } - }, - "keep_separator": { - "title": "Keep separator", - "description": "Whether to keep the separator in the resulting chunks", - "default": false, - "type": "boolean" - } - }, - "required": ["mode"], - "description": "Split the text by the list of separators until the chunk size is reached, using the earlier mentioned separators where possible. This is useful for splitting text fields by paragraphs, sentences, words, etc." - }, - { - "title": "By Markdown header", - "type": "object", - "properties": { - "mode": { - "title": "Mode", - "default": "markdown", - "const": "markdown", - "enum": ["markdown"], - "type": "string" - }, - "split_level": { - "title": "Split level", - "description": "Level of markdown headers to split text fields by. Headings down to the specified level will be used as split points", - "default": 1, - "minimum": 1, - "maximum": 6, - "type": "integer" - } - }, - "required": ["mode"], - "description": "Split the text by Markdown headers down to the specified header level. If the chunk size fits multiple sections, they will be combined into a single chunk." - }, - { - "title": "By Programming Language", - "type": "object", - "properties": { - "mode": { - "title": "Mode", - "default": "code", - "const": "code", - "enum": ["code"], - "type": "string" - }, - "language": { - "title": "Language", - "description": "Split code in suitable places based on the programming language", - "enum": [ - "cpp", - "go", - "java", - "js", - "php", - "proto", - "python", - "rst", - "ruby", - "rust", - "scala", - "swift", - "markdown", - "latex", - "html", - "sol" - ], - "type": "string" - } - }, - "required": ["language", "mode"], - "description": "Split the text by suitable delimiters based on the programming language. This is useful for splitting code into chunks." - } - ] - } - }, - "required": ["chunk_size"], - "group": "processing" - }, "embedding": { "title": "Embedding", "description": "Embedding configuration", @@ -181,8 +23,8 @@ "type": "string" } }, - "required": ["mode"], - "description": "Do not calculate and pass embeddings to Weaviate. Suitable for clusters with configured vectorizers to calculate embeddings within Weaviate or for classes that should only support regular text search." + "description": "Do not calculate and pass embeddings to Weaviate. Suitable for clusters with configured vectorizers to calculate embeddings within Weaviate or for classes that should only support regular text search.", + "required": ["mode"] }, { "title": "Azure OpenAI", @@ -296,8 +138,8 @@ "type": "string" } }, - "required": ["mode"], - "description": "Use a fake embedding made out of random vectors with 1536 embedding dimensions. This is useful for testing the data pipeline without incurring any costs." + "description": "Use a fake embedding made out of random vectors with 1536 embedding dimensions. This is useful for testing the data pipeline without incurring any costs.", + "required": ["mode"] }, { "title": "OpenAI-compatible", @@ -341,6 +183,177 @@ } ] }, + "processing": { + "title": "ProcessingConfigModel", + "type": "object", + "properties": { + "chunk_size": { + "title": "Chunk size", + "description": "Size of chunks in tokens to store in vector store (make sure it is not too big for the context if your LLM)", + "maximum": 8191, + "minimum": 1, + "type": "integer" + }, + "chunk_overlap": { + "title": "Chunk overlap", + "description": "Size of overlap between chunks in tokens to store in vector store to better capture relevant context", + "default": 0, + "type": "integer" + }, + "text_fields": { + "title": "Text fields to embed", + "description": "List of fields in the record that should be used to calculate the embedding. The field list is applied to all streams in the same way and non-existing fields are ignored. If none are defined, all fields are considered text fields. When specifying text fields, you can access nested fields in the record by using dot notation, e.g. `user.name` will access the `name` field in the `user` object. It's also possible to use wildcards to access all fields in an object, e.g. `users.*.name` will access all `names` fields in all entries of the `users` array.", + "default": [], + "always_show": true, + "examples": ["text", "user.name", "users.*.name"], + "type": "array", + "items": { + "type": "string" + } + }, + "metadata_fields": { + "title": "Fields to store as metadata", + "description": "List of fields in the record that should be stored as metadata. The field list is applied to all streams in the same way and non-existing fields are ignored. If none are defined, all fields are considered metadata fields. When specifying text fields, you can access nested fields in the record by using dot notation, e.g. `user.name` will access the `name` field in the `user` object. It's also possible to use wildcards to access all fields in an object, e.g. `users.*.name` will access all `names` fields in all entries of the `users` array. When specifying nested paths, all matching values are flattened into an array set to a field named by the path.", + "default": [], + "always_show": true, + "examples": ["age", "user", "user.name"], + "type": "array", + "items": { + "type": "string" + } + }, + "text_splitter": { + "title": "Text splitter", + "description": "Split text fields into chunks based on the specified method.", + "type": "object", + "oneOf": [ + { + "title": "By Separator", + "type": "object", + "properties": { + "mode": { + "title": "Mode", + "default": "separator", + "const": "separator", + "enum": ["separator"], + "type": "string" + }, + "separators": { + "title": "Separators", + "description": "List of separator strings to split text fields by. The separator itself needs to be wrapped in double quotes, e.g. to split by the dot character, use \".\". To split by a newline, use \"\\n\".", + "default": ["\"\\n\\n\"", "\"\\n\"", "\" \"", "\"\""], + "type": "array", + "items": { + "type": "string" + } + }, + "keep_separator": { + "title": "Keep separator", + "description": "Whether to keep the separator in the resulting chunks", + "default": false, + "type": "boolean" + } + }, + "description": "Split the text by the list of separators until the chunk size is reached, using the earlier mentioned separators where possible. This is useful for splitting text fields by paragraphs, sentences, words, etc.", + "required": ["mode"] + }, + { + "title": "By Markdown header", + "type": "object", + "properties": { + "mode": { + "title": "Mode", + "default": "markdown", + "const": "markdown", + "enum": ["markdown"], + "type": "string" + }, + "split_level": { + "title": "Split level", + "description": "Level of markdown headers to split text fields by. Headings down to the specified level will be used as split points", + "default": 1, + "minimum": 1, + "maximum": 6, + "type": "integer" + } + }, + "description": "Split the text by Markdown headers down to the specified header level. If the chunk size fits multiple sections, they will be combined into a single chunk.", + "required": ["mode"] + }, + { + "title": "By Programming Language", + "type": "object", + "properties": { + "mode": { + "title": "Mode", + "default": "code", + "const": "code", + "enum": ["code"], + "type": "string" + }, + "language": { + "title": "Language", + "description": "Split code in suitable places based on the programming language", + "enum": [ + "cpp", + "go", + "java", + "js", + "php", + "proto", + "python", + "rst", + "ruby", + "rust", + "scala", + "swift", + "markdown", + "latex", + "html", + "sol" + ], + "type": "string" + } + }, + "required": ["language", "mode"], + "description": "Split the text by suitable delimiters based on the programming language. This is useful for splitting code into chunks." + } + ] + }, + "field_name_mappings": { + "title": "Field name mappings", + "description": "List of fields to rename. Not applicable for nested fields, but can be used to rename fields already flattened via dot notation.", + "default": [], + "type": "array", + "items": { + "title": "FieldNameMappingConfigModel", + "type": "object", + "properties": { + "from_field": { + "title": "From field name", + "description": "The field name in the source", + "type": "string" + }, + "to_field": { + "title": "To field name", + "description": "The field name to use in the destination", + "type": "string" + } + }, + "required": ["from_field", "to_field"] + } + } + }, + "required": ["chunk_size"], + "group": "processing" + }, + "omit_raw_text": { + "title": "Do not store raw text", + "description": "Do not store the text that gets embedded along with the vector and the metadata in the destination. If set to true, only the vector and the metadata will be stored - in this case raw text for LLM use cases needs to be retrieved from another source.", + "default": false, + "group": "advanced", + "type": "boolean" + }, "indexing": { "title": "Indexing", "type": "object", @@ -419,8 +432,8 @@ "type": "string" } }, - "required": ["mode"], - "description": "Do not authenticate (suitable for locally running test clusters, do not use for clusters with public IP addresses)" + "description": "Do not authenticate (suitable for locally running test clusters, do not use for clusters with public IP addresses)", + "required": ["mode"] } ] }, @@ -436,6 +449,13 @@ "default": "text", "type": "string" }, + "tenant_id": { + "title": "Tenant ID", + "description": "The tenant ID to use for multi tenancy", + "airbyte_secret": true, + "default": "", + "type": "string" + }, "default_vectorizer": { "title": "Default Vectorizer", "description": "The vectorizer to use if new classes need to be created", @@ -457,14 +477,20 @@ "description": "Additional HTTP headers to send with every request.", "default": [], "examples": [ - { "header_key": "X-OpenAI-Api-Key", "value": "my-openai-api-key" } + { + "header_key": "X-OpenAI-Api-Key", + "value": "my-openai-api-key" + } ], "type": "array", "items": { "title": "Header", "type": "object", "properties": { - "header_key": { "title": "Header Key", "type": "string" }, + "header_key": { + "title": "Header Key", + "type": "string" + }, "value": { "title": "Header Value", "airbyte_secret": true, @@ -478,21 +504,26 @@ "required": ["host", "auth"], "group": "indexing", "description": "Indexing configuration" - }, - "omit_raw_text": { - "title": "Do not store raw text", - "description": "Do not store the text that gets embedded along with the vector and the metadata in the destination. If set to true, only the vector and the metadata will be stored - in this case raw text for LLM use cases needs to be retrieved from another source.", - "default": false, - "group": "advanced", - "type": "boolean" } }, "required": ["embedding", "processing", "indexing"], "groups": [ - { "id": "processing", "title": "Processing" }, - { "id": "embedding", "title": "Embedding" }, - { "id": "indexing", "title": "Indexing" }, - { "id": "advanced", "title": "Advanced" } + { + "id": "processing", + "title": "Processing" + }, + { + "id": "embedding", + "title": "Embedding" + }, + { + "id": "indexing", + "title": "Indexing" + }, + { + "id": "advanced", + "title": "Advanced" + } ] }, "supportsIncremental": true, diff --git a/airbyte-integrations/connectors/destination-weaviate/metadata.yaml b/airbyte-integrations/connectors/destination-weaviate/metadata.yaml index f519260f42f73..29aeefaf0831e 100644 --- a/airbyte-integrations/connectors/destination-weaviate/metadata.yaml +++ b/airbyte-integrations/connectors/destination-weaviate/metadata.yaml @@ -13,7 +13,7 @@ data: connectorSubtype: vectorstore connectorType: destination definitionId: 7b7d7a0d-954c-45a0-bcfc-39a634b97736 - dockerImageTag: 0.2.13 + dockerImageTag: 0.2.14 dockerRepository: airbyte/destination-weaviate documentationUrl: https://docs.airbyte.com/integrations/destinations/weaviate githubIssueLabel: destination-weaviate diff --git a/airbyte-integrations/connectors/destination-weaviate/unit_tests/indexer_test.py b/airbyte-integrations/connectors/destination-weaviate/unit_tests/indexer_test.py index 043a4b6ea68ce..a5b2526e392cb 100644 --- a/airbyte-integrations/connectors/destination-weaviate/unit_tests/indexer_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/unit_tests/indexer_test.py @@ -71,6 +71,32 @@ def test_pre_sync_that_creates_class(self, MockClient): } ) + @patch("destination_weaviate.indexer.weaviate.Client") + def test_pre_sync_that_creates_class_with_multi_tenancy_enabled(self, MockClient): + mock_client = Mock() + self.config.tenant_id = "test_tenant" + mock_client.schema.get_class_tenants.return_value = [] + mock_client.schema.get.return_value = {"classes": []} + MockClient.return_value = mock_client + self.indexer.pre_sync(self.mock_catalog) + mock_client.schema.create_class.assert_called_with( + { + "class": "Test", + "multiTenancyConfig": {"enabled": True}, + "vectorizer": "none", + "properties": [ + { + "name": "_ab_record_id", + "dataType": ["text"], + "description": "Record ID, used for bookkeeping.", + "indexFilterable": True, + "indexSearchable": False, + "tokenization": "field", + } + ], + } + ) + @patch("destination_weaviate.indexer.weaviate.Client") def test_pre_sync_that_deletes(self, MockClient): mock_client = Mock() @@ -104,6 +130,19 @@ def test_index_deletes_by_record_id(self): where={"path": ["_ab_record_id"], "operator": "ContainsAny", "valueStringArray": ["some_id", "some_other_id"]}, ) + def test_index_deletes_by_record_id_with_tenant_id(self): + mock_client = Mock() + self.config.tenant_id = "test_tenant" + self.indexer.client = mock_client + self.indexer.has_record_id_metadata = defaultdict(None) + self.indexer.has_record_id_metadata["Test"] = True + self.indexer.delete(["some_id", "some_other_id"], None, "test") + mock_client.batch.delete_objects.assert_called_with( + class_name="Test", + tenant="test_tenant", + where={"path": ["_ab_record_id"], "operator": "ContainsAny", "valueStringArray": ["some_id", "some_other_id"]}, + ) + @patch("destination_weaviate.indexer.weaviate.Client") def test_index_not_delete_no_metadata_field(self, MockClient): mock_client = Mock() @@ -200,31 +239,39 @@ def test_index_flushes_batch_and_normalizes(self): page_content="some_content", embedding=[1, 2, 3], metadata={ - "someField": "some_value", "complex": {"a": [1, 2, 3]}, "UPPERCASE_NAME": "abc", "id": 12, "empty_list": [], - "referral Agency Name": "test1", - "123StartsWithNumber": "test2", - "special&*chars": "test3", - "with spaces": "test4", - "": "test5", - "_startsWithUnderscore": "test6", - "multiple spaces": "test7", - "SpecialCharacters!@#": "test8" - }, + "someField": "some_value", + "complex": {"a": [1, 2, 3]}, + "UPPERCASE_NAME": "abc", + "id": 12, + "empty_list": [], + "referral Agency Name": "test1", + "123StartsWithNumber": "test2", + "special&*chars": "test3", + "with spaces": "test4", + "": "test5", + "_startsWithUnderscore": "test6", + "multiple spaces": "test7", + "SpecialCharacters!@#": "test8", + }, record=AirbyteRecordMessage(stream="test", data={"someField": "some_value"}, emitted_at=0), ) self.indexer.index([mock_chunk], None, "test") mock_client.batch.add_data_object.assert_called_with( - {"someField": "some_value", "complex": '{"a": [1, 2, 3]}', "uPPERCASE_NAME": "abc", "text": "some_content", "raw_id": 12, - "referral_Agency_Name": "test1", + { + "someField": "some_value", + "complex": '{"a": [1, 2, 3]}', + "uPPERCASE_NAME": "abc", + "text": "some_content", + "raw_id": 12, + "referral_Agency_Name": "test1", "_123StartsWithNumber": "test2", "specialchars": "test3", "with_spaces": "test4", "_": "test5", "_startsWithUnderscore": "test6", "multiple__spaces": "test7", - "specialCharacters": "test8" - - }, + "specialCharacters": "test8", + }, "Test", ANY, vector=[1, 2, 3], diff --git a/docs/integrations/destinations/weaviate.md b/docs/integrations/destinations/weaviate.md index ed98586015b00..c8acb02b50de3 100644 --- a/docs/integrations/destinations/weaviate.md +++ b/docs/integrations/destinations/weaviate.md @@ -79,10 +79,13 @@ You can also create the class in Weaviate in advance if you need more control ov As properties have to start will a lowercase letter in Weaviate and can't contain spaces or special characters. Field names might be updated during the loading process. The field names `id`, `_id` and `_additional` are reserved keywords in Weaviate, so they will be renamed to `raw_id`, `raw__id` and `raw_additional` respectively. +When using [multi-tenancy](https://weaviate.io/developers/weaviate/manage-data/multi-tenancy), the tenant id can be configured in the connector configuration. If not specified, multi-tenancy will be disabled. In case you want to index into an already created class, you need to make sure the class is created with multi-tenancy enabled. In case the class doesn't exist, it will be created with multi-tenancy properly configured. If the class already exists but the tenant id is not associated with the class, the connector will automatically add the tenant id to the class. This allows you to configure multiple connections for different tenants on the same schema. + ## Changelog | Version | Date | Pull Request | Subject | | :------ | :--------- | :--------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------- | +| 0.2.14 | 2023-01-15 | [34229](https://github.com/airbytehq/airbyte/pull/34229) | Allow configuring tenant id | | 0.2.13 | 2023-12-11 | [33303](https://github.com/airbytehq/airbyte/pull/33303) | Fix bug with embedding special tokens | | 0.2.12 | 2023-12-07 | [33218](https://github.com/airbytehq/airbyte/pull/33218) | Normalize metadata field names | | 0.2.11 | 2023-12-01 | [32697](https://github.com/airbytehq/airbyte/pull/32697) | Allow omitting raw text | From a6f4c2e11dd820e8a7d5532eedefa92c57d96c3a Mon Sep 17 00:00:00 2001 From: Anton Karpets Date: Wed, 17 Jan 2024 15:56:58 +0200 Subject: [PATCH 127/574] =?UTF-8?q?=F0=9F=90=9BSource=20Amazon=20Seller=20?= =?UTF-8?q?Partner:=20delete=20deprecated=20streams=20(#34283)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acceptance-test-config.yml | 4 -- ...og_brand_analytics_alternate_purchase.json | 40 ------------------- ...talog_brand_analytics_item_comparison.json | 40 ------------------- .../integration_tests/expected_records.jsonl | 19 ++++----- .../integration_tests/future_state.json | 22 ---------- .../metadata.yaml | 2 +- ...D_ANALYTICS_ALTERNATE_PURCHASE_REPORT.json | 36 ----------------- ...RAND_ANALYTICS_ITEM_COMPARISON_REPORT.json | 36 ----------------- .../source_amazon_seller_partner/source.py | 4 -- .../source_amazon_seller_partner/spec.json | 2 - .../source_amazon_seller_partner/streams.py | 10 ----- .../sources/amazon-seller-partner.md | 3 +- 12 files changed, 10 insertions(+), 208 deletions(-) delete mode 100644 airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/configured_catalog_brand_analytics_alternate_purchase.json delete mode 100644 airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/configured_catalog_brand_analytics_item_comparison.json delete mode 100644 airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/schemas/GET_BRAND_ANALYTICS_ALTERNATE_PURCHASE_REPORT.json delete mode 100644 airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/schemas/GET_BRAND_ANALYTICS_ITEM_COMPARISON_REPORT.json 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 bd284f0163471..e94239156b642 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 @@ -91,8 +91,6 @@ acceptance_tests: bypass_reason: "no records" - name: GET_FBA_SNS_FORECAST_DATA bypass_reason: "no records" - - name: GET_BRAND_ANALYTICS_ITEM_COMPARISON_REPORT - bypass_reason: "no records" - name: GET_AFN_INVENTORY_DATA bypass_reason: "no records" - name: GET_MERCHANT_CANCELLED_LISTINGS_DATA @@ -101,8 +99,6 @@ acceptance_tests: bypass_reason: "no records" - name: GET_LEDGER_SUMMARY_VIEW_DATA bypass_reason: "no records" - - name: GET_BRAND_ANALYTICS_ALTERNATE_PURCHASE_REPORT - bypass_reason: "no records" - name: GET_BRAND_ANALYTICS_SEARCH_TERMS_REPORT bypass_reason: "no records" - name: GET_BRAND_ANALYTICS_REPEAT_PURCHASE_REPORT diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/configured_catalog_brand_analytics_alternate_purchase.json b/airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/configured_catalog_brand_analytics_alternate_purchase.json deleted file mode 100644 index 2ce8fbb81064a..0000000000000 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/configured_catalog_brand_analytics_alternate_purchase.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "streams": [ - { - "stream": { - "name": "GET_BRAND_ANALYTICS_ALTERNATE_PURCHASE_REPORT", - "json_schema": { - "title": "Brand Analytics Alternate Purchase Reports", - "description": "Brand Analytics Alternate Purchase Reports", - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "startDate": { - "type": ["null", "string"], - "format": "date" - }, - "endDate": { - "type": ["null", "string"], - "format": "date" - }, - "asin": { - "type": ["null", "string"] - }, - "purchasedAsin": { - "type": ["null", "string"] - }, - "purchasedRank": { - "type": ["null", "integer"] - }, - "purchasedPct": { - "type": ["null", "number"] - } - } - }, - "supported_sync_modes": ["full_refresh"] - }, - "sync_mode": "full_refresh", - "destination_sync_mode": "overwrite" - } - ] -} diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/configured_catalog_brand_analytics_item_comparison.json b/airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/configured_catalog_brand_analytics_item_comparison.json deleted file mode 100644 index 4d7300e63157f..0000000000000 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/configured_catalog_brand_analytics_item_comparison.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "streams": [ - { - "stream": { - "name": "GET_BRAND_ANALYTICS_ITEM_COMPARISON_REPORT", - "json_schema": { - "title": "Brand Analytics Item Comparison Reports", - "description": "Brand Analytics Item Comparison Reports", - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "startDate": { - "type": ["null", "string"], - "format": "date" - }, - "endDate": { - "type": ["null", "string"], - "format": "date" - }, - "asin": { - "type": ["null", "string"] - }, - "comparedAsin": { - "type": ["null", "string"] - }, - "comparedRank": { - "type": ["null", "integer"] - }, - "comparedPct": { - "type": ["null", "number"] - } - } - }, - "supported_sync_modes": ["full_refresh"] - }, - "sync_mode": "full_refresh", - "destination_sync_mode": "overwrite" - } - ] -} diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/expected_records.jsonl b/airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/expected_records.jsonl index 014937625c8dd..c36b190c842b6 100644 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/expected_records.jsonl +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/expected_records.jsonl @@ -43,17 +43,14 @@ {"stream": "OrderItems", "data": {"TaxCollection": {"Model": "MarketplaceFacilitator", "ResponsibleParty": "Amazon Services, Inc."}, "ProductInfo": {"NumberOfItems": "1"}, "BuyerInfo": {}, "ItemTax": {"CurrencyCode": "USD", "Amount": "0.00"}, "QuantityShipped": 0, "BuyerRequestedCancel": {"IsBuyerRequestedCancel": "false", "BuyerCancelReason": ""}, "ItemPrice": {"CurrencyCode": "USD", "Amount": "14.00"}, "ASIN": "B074K5MDLW", "SellerSKU": "2J-D6V7-C8XI", "Title": "Beyond Meat Beyond Burger Plant-Based Patties 2 pk, 8 oz (Frozen)", "IsGift": "false", "ConditionSubtypeId": "New", "IsTransparency": false, "QuantityOrdered": 0, "PromotionDiscountTax": {"CurrencyCode": "USD", "Amount": "0.00"}, "ConditionId": "New", "PromotionDiscount": {"CurrencyCode": "USD", "Amount": "0.00"}, "OrderItemId": "64356568394218", "LastUpdateDate": "2022-07-29T08:19:16Z", "AmazonOrderId": "113-8871452-8288246"}, "emitted_at": 1701969243138} {"stream": "GET_RESTOCK_INVENTORY_RECOMMENDATIONS_REPORT", "data": {"Country": "US", "Product Name": "Airbyte T-Shirt Black", "FNSKU": "X0041NMBPF", "Merchant SKU": "IA-VREM-8L92", "ASIN": "B0CJ5Q3NLP", "Condition": "New", "Supplier": "unassigned", "Supplier part no.": "", "Currency code": "USD", "Price": "15.00", "Sales last 30 days": "0.0", "Units Sold Last 30 Days": "0", "Total Units": "0", "Inbound": "0", "Available": "0", "FC transfer": "0", "FC Processing": "0", "Customer Order": "0", "Unfulfillable": "0", "Working": "0", "Shipped": "0", "Receiving": "0", "Fulfilled by": "Amazon", "Total Days of Supply (including units from open shipments)": "", "Days of Supply at Amazon Fulfillment Network": "", "Alert": "out_of_stock", "Recommended replenishment qty": "0", "Recommended ship date": "none", "Recommended action": "No action required", "Unit storage size": "", "dataEndTime": "2022-07-31"}, "emitted_at": 1701969512824} {"stream": "GET_RESTOCK_INVENTORY_RECOMMENDATIONS_REPORT", "data": {"Country": "US", "Product Name": "Airbyte Merch White", "FNSKU": "X003X1FG67", "Merchant SKU": "KW-J7BQ-WNKL", "ASIN": "B0CDLLJ5VV", "Condition": "New", "Supplier": "unassigned", "Supplier part no.": "", "Currency code": "USD", "Price": "10.00", "Sales last 30 days": "0.0", "Units Sold Last 30 Days": "0", "Total Units": "0", "Inbound": "0", "Available": "0", "FC transfer": "0", "FC Processing": "0", "Customer Order": "0", "Unfulfillable": "0", "Working": "0", "Shipped": "0", "Receiving": "0", "Fulfilled by": "Amazon", "Total Days of Supply (including units from open shipments)": "", "Days of Supply at Amazon Fulfillment Network": "", "Alert": "out_of_stock", "Recommended replenishment qty": "0", "Recommended ship date": "none", "Recommended action": "No action required", "Unit storage size": "0.1736 ft3", "dataEndTime": "2022-07-31"}, "emitted_at": 1701969512826} -{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18923842351", "settlement-start-date": "2023-10-16T22:51:31+00:00", "settlement-end-date": "2023-11-13T22:51:31+00:00", "deposit-date": "2023-11-15T22:51:31+00:00", "total-amount": "-39.99", "currency": "USD", "transaction-type": "", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": null, "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "", "dataEndTime": "2023-11-13"}, "emitted_at": 1701969629629} -{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18923842351", "settlement-start-date": null, "settlement-end-date": null, "deposit-date": null, "total-amount": "", "currency": "", "transaction-type": "Payable to Amazon", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": "2023-10-16T22:51:31+00:00", "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "-27.54", "dataEndTime": "2023-11-13"}, "emitted_at": 1701969629631} -{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18923842351", "settlement-start-date": null, "settlement-end-date": null, "deposit-date": null, "total-amount": "", "currency": "", "transaction-type": "Subscription Fee", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": "2023-11-09T18:44:35+00:00", "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "-39.99", "dataEndTime": "2023-11-13"}, "emitted_at": 1701969629631} -{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18923842351", "settlement-start-date": null, "settlement-end-date": null, "deposit-date": null, "total-amount": "", "currency": "", "transaction-type": "Successful charge", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": "2023-10-17T00:01:09+00:00", "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "27.54 ", "dataEndTime": "2023-11-13"}, "emitted_at": 1701969629632} -{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18834943411", "settlement-start-date": "2023-10-02T22:51:31+00:00", "settlement-end-date": "2023-10-16T22:51:31+00:00", "deposit-date": "2023-10-18T22:51:31+00:00", "total-amount": "-27.54", "currency": "USD", "transaction-type": "", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": null, "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "", "dataEndTime": "2023-10-16"}, "emitted_at": 1701969660859} -{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18834943411", "settlement-start-date": null, "settlement-end-date": null, "deposit-date": null, "total-amount": "", "currency": "", "transaction-type": "Subscription Fee", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": "2023-10-09T20:49:19+00:00", "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "-39.99", "dataEndTime": "2023-10-16"}, "emitted_at": 1701969660860} -{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18834943411", "settlement-start-date": null, "settlement-end-date": null, "deposit-date": null, "total-amount": "", "currency": "", "transaction-type": "Previous Reserve Amount Balance", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": "2023-10-02T22:58:21+00:00", "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "12.45", "dataEndTime": "2023-10-16"}, "emitted_at": 1701969660861} -{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18654297941", "settlement-start-date": "2023-09-18T22:51:31+00:00", "settlement-end-date": "2023-10-02T22:51:31+00:00", "deposit-date": "2023-10-04T22:51:31+00:00", "total-amount": "0.00", "currency": "USD", "transaction-type": "", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": null, "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "", "dataEndTime": "2023-10-02"}, "emitted_at": 1701969692191} -{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18654297941", "settlement-start-date": null, "settlement-end-date": null, "deposit-date": null, "total-amount": "", "currency": "", "transaction-type": "Order", "order-id": "111-1308361-8778604", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "D7vNnKlKr", "marketplace-name": "Amazon.com", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "MFN", "posted-date": "2023-09-26T12:06:28+00:00", "order-item-code": "85435093931281", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "IA-VREM-8L92", "quantity-purchased": "1", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "", "dataEndTime": "2023-10-02"}, "emitted_at": 1701969692192} -{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18654297941", "settlement-start-date": null, "settlement-end-date": null, "deposit-date": null, "total-amount": "", "currency": "", "transaction-type": "Order", "order-id": "111-1308361-8778604", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "D7vNnKlKr", "marketplace-name": "Amazon.com", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "MFN", "posted-date": "2023-09-26T12:06:28+00:00", "order-item-code": "85435093931281", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "IA-VREM-8L92", "quantity-purchased": "", "price-type": "Principal", "price-amount": "15.00", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "", "dataEndTime": "2023-10-02"}, "emitted_at": 1701969692192} -{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18654297941", "settlement-start-date": null, "settlement-end-date": null, "deposit-date": null, "total-amount": "", "currency": "", "transaction-type": "Order", "order-id": "111-1308361-8778604", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "D7vNnKlKr", "marketplace-name": "Amazon.com", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "MFN", "posted-date": "2023-09-26T12:06:28+00:00", "order-item-code": "85435093931281", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "IA-VREM-8L92", "quantity-purchased": "", "price-type": "Tax", "price-amount": "0.86", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "", "dataEndTime": "2023-10-02"}, "emitted_at": 1701969692193} +{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "19009771651", "settlement-start-date": "2023-11-13T22:51:31+00:00", "settlement-end-date": "2023-12-11T22:51:31+00:00", "deposit-date": "2023-12-13T22:51:31+00:00", "total-amount": "-39.99", "currency": "USD", "transaction-type": "", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": null, "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "", "dataEndTime": "2023-12-11"}, "emitted_at": 1705396604115} +{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "19009771651", "settlement-start-date": null, "settlement-end-date": null, "deposit-date": null, "total-amount": "", "currency": "", "transaction-type": "Payable to Amazon", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": "2023-11-13T22:51:31+00:00", "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "-39.99", "dataEndTime": "2023-12-11"}, "emitted_at": 1705396604117} +{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "19009771651", "settlement-start-date": null, "settlement-end-date": null, "deposit-date": null, "total-amount": "", "currency": "", "transaction-type": "Subscription Fee", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": "2023-12-09T20:02:53+00:00", "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "-39.99", "dataEndTime": "2023-12-11"}, "emitted_at": 1705396604118} +{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "19009771651", "settlement-start-date": null, "settlement-end-date": null, "deposit-date": null, "total-amount": "", "currency": "", "transaction-type": "Successful charge", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": "2023-11-13T23:51:01+00:00", "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "39.99 ", "dataEndTime": "2023-12-11"}, "emitted_at": 1705396604118} +{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18923842351", "settlement-start-date": "2023-10-16T22:51:31+00:00", "settlement-end-date": "2023-11-13T22:51:31+00:00", "deposit-date": "2023-11-15T22:51:31+00:00", "total-amount": "-39.99", "currency": "USD", "transaction-type": "", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": null, "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "", "dataEndTime": "2023-11-13"}, "emitted_at": 1705396605853} +{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18923842351", "settlement-start-date": null, "settlement-end-date": null, "deposit-date": null, "total-amount": "", "currency": "", "transaction-type": "Payable to Amazon", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": "2023-10-16T22:51:31+00:00", "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "-27.54", "dataEndTime": "2023-11-13"}, "emitted_at": 1705396605855} +{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18923842351", "settlement-start-date": null, "settlement-end-date": null, "deposit-date": null, "total-amount": "", "currency": "", "transaction-type": "Subscription Fee", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": "2023-11-09T18:44:35+00:00", "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "-39.99", "dataEndTime": "2023-11-13"}, "emitted_at": 1705396605856} +{"stream": "GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE", "data": {"settlement-id": "18923842351", "settlement-start-date": null, "settlement-end-date": null, "deposit-date": null, "total-amount": "", "currency": "", "transaction-type": "Successful charge", "order-id": "", "merchant-order-id": "", "adjustment-id": "", "shipment-id": "", "marketplace-name": "", "shipment-fee-type": "", "shipment-fee-amount": "", "order-fee-type": "", "order-fee-amount": "", "fulfillment-id": "", "posted-date": "2023-10-17T00:01:09+00:00", "order-item-code": "", "merchant-order-item-id": "", "merchant-adjustment-item-id": "", "sku": "", "quantity-purchased": "", "price-type": "", "price-amount": "", "item-related-fee-type": "", "item-related-fee-amount": "", "misc-fee-amount": "", "other-fee-amount": "", "other-fee-reason-description": "", "direct-payment-type": "", "direct-payment-amount": "", "other-amount": "27.54 ", "dataEndTime": "2023-11-13"}, "emitted_at": 1705396605857} {"stream": "GET_MERCHANT_LISTINGS_DATA_BACK_COMPAT", "data": {"item-name": "GiftBox", "item-description": "Monitor and optimize the GiftBox to reward your customers and increase the average order value", "listing-id": "0711ZJUYPNS", "seller-sku": "I0-RALD-N1UR", "price": "5", "quantity": "1000", "open-date": "2022-07-11T01:34:18-07:00", "image-url": "", "item-is-marketplace": "y", "product-id-type": "1", "zshop-shipping-fee": "", "item-note": "", "item-condition": "11", "zshop-category1": "", "zshop-browse-path": "", "zshop-storefront-feature": "", "asin1": "B0B68NBQ1Y", "asin2": "", "asin3": "", "will-ship-internationally": "", "expedited-shipping": "", "zshop-boldface": "", "product-id": "B0B68NBQ1Y", "bid-for-featured-placement": "", "add-delete": "", "pending-quantity": "0", "Business Price": "6.0", "Quantity Price Type": "", "Quantity Lower Bound 1": "", "Quantity Price 1": "", "Quantity Lower Bound 2": "", "Quantity Price 2": "", "Quantity Lower Bound 3": "", "Quantity Price 3": "", "Quantity Lower Bound 4": "", "Quantity Price 4": "", "Quantity Lower Bound 5": "", "Quantity Price 5": "", "merchant-shipping-group": "Migrated Template", "Progressive Price Type": "", "Progressive Lower Bound 1": "", "Progressive Price 1": "", "Progressive Lower Bound 2": "", "Progressive Price 2": "", "Progressive Lower Bound 3": "", "Progressive Price 3": "", "dataEndTime": "2022-07-31"}, "emitted_at": 1701976405556} {"stream": "ListFinancialEvents", "data": {"ShipmentEventList": [], "ShipmentSettleEventList": [], "RefundEventList": [], "GuaranteeClaimEventList": [], "ChargebackEventList": [], "PayWithAmazonEventList": [], "ServiceProviderCreditEventList": [], "RetrochargeEventList": [], "RentalTransactionEventList": [], "PerformanceBondRefundEventList": [], "ProductAdsPaymentEventList": [{"postedDate": "2022-07-28T20:06:07Z", "transactionType": "Charge", "invoiceId": "TR1T7Z7DR-1", "baseValue": {"CurrencyCode": "USD", "CurrencyAmount": -9.08}, "taxValue": {"CurrencyCode": "USD", "CurrencyAmount": 0.0}, "transactionValue": {"CurrencyCode": "USD", "CurrencyAmount": -9.08}}], "ServiceFeeEventList": [], "SellerDealPaymentEventList": [], "DebtRecoveryEventList": [], "LoanServicingEventList": [], "AdjustmentEventList": [], "SAFETReimbursementEventList": [], "SellerReviewEnrollmentPaymentEventList": [], "FBALiquidationEventList": [], "CouponPaymentEventList": [], "ImagingServicesFeeEventList": [], "NetworkComminglingTransactionEventList": [], "AffordabilityExpenseEventList": [], "AffordabilityExpenseReversalEventList": [], "RemovalShipmentEventList": [], "RemovalShipmentAdjustmentEventList": [], "TrialShipmentEventList": [], "TDSReimbursementEventList": [], "AdhocDisbursementEventList": [], "TaxWithholdingEventList": [], "ChargeRefundEventList": [], "FailedAdhocDisbursementEventList": [], "ValueAddedServiceChargeEventList": [], "CapacityReservationBillingEventList": [], "PostedBefore": "2022-07-31T00:00:00Z"}, "emitted_at": 1701976465145} {"stream": "ListFinancialEventGroups", "data": {"FinancialEventGroupId": "6uFLEEa3LQgyvcccMnVQ4Bj-I5zkOVNoM41q8leJzLk", "ProcessingStatus": "Closed", "FundTransferStatus": "Unknown", "OriginalTotal": {"CurrencyCode": "USD", "CurrencyAmount": -58.86}, "FundTransferDate": "2022-08-08T22:51:31Z", "BeginningBalance": {"CurrencyCode": "USD", "CurrencyAmount": -39.99}, "FinancialEventGroupStart": "2021-07-26T22:51:30Z", "FinancialEventGroupEnd": "2022-08-08T22:51:31Z"}, "emitted_at": 1701976502869} diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/future_state.json b/airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/future_state.json index dcdfa0d0f38c6..9bf0b19e4d41b 100644 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/future_state.json +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/integration_tests/future_state.json @@ -32,28 +32,6 @@ } } }, - { - "type": "STREAM", - "stream": { - "stream_state": { - "dataEndTime": "2121-07-01" - }, - "stream_descriptor": { - "name": "GET_BRAND_ANALYTICS_ALTERNATE_PURCHASE_REPORT" - } - } - }, - { - "type": "STREAM", - "stream": { - "stream_state": { - "dataEndTime": "2121-07-01" - }, - "stream_descriptor": { - "name": "GET_BRAND_ANALYTICS_ITEM_COMPARISON_REPORT" - } - } - }, { "type": "STREAM", "stream": { diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/metadata.yaml b/airbyte-integrations/connectors/source-amazon-seller-partner/metadata.yaml index d0c077853b9fa..e6a6b883ff35c 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.0.1 + dockerImageTag: 3.1.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/GET_BRAND_ANALYTICS_ALTERNATE_PURCHASE_REPORT.json b/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/schemas/GET_BRAND_ANALYTICS_ALTERNATE_PURCHASE_REPORT.json deleted file mode 100644 index 1714a688b4de8..0000000000000 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/schemas/GET_BRAND_ANALYTICS_ALTERNATE_PURCHASE_REPORT.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "title": "Brand Analytics Alternate Purchase Reports", - "description": "Brand Analytics Alternate Purchase Reports", - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "startDate": { - "type": ["null", "string"], - "format": "date" - }, - "endDate": { - "type": ["null", "string"], - "format": "date" - }, - "asin": { - "type": ["null", "string"] - }, - "purchasedAsin": { - "type": ["null", "string"] - }, - "purchasedRank": { - "type": ["null", "integer"] - }, - "purchasedPct": { - "type": ["null", "number"] - }, - "dataEndTime": { - "type": ["null", "string"], - "format": "date" - }, - "queryEndDate": { - "type": ["null", "string"], - "format": "date" - } - } -} diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/schemas/GET_BRAND_ANALYTICS_ITEM_COMPARISON_REPORT.json b/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/schemas/GET_BRAND_ANALYTICS_ITEM_COMPARISON_REPORT.json deleted file mode 100644 index ee646b8a14c83..0000000000000 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/schemas/GET_BRAND_ANALYTICS_ITEM_COMPARISON_REPORT.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "title": "Brand Analytics Item Comparison Reports", - "description": "Brand Analytics Item Comparison Reports", - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "startDate": { - "type": ["null", "string"], - "format": "date" - }, - "endDate": { - "type": ["null", "string"], - "format": "date" - }, - "asin": { - "type": ["null", "string"] - }, - "comparedAsin": { - "type": ["null", "string"] - }, - "comparedRank": { - "type": ["null", "integer"] - }, - "comparedPct": { - "type": ["null", "number"] - }, - "dataEndTime": { - "type": ["null", "string"], - "format": "date" - }, - "queryEndDate": { - "type": ["null", "string"], - "format": "date" - } - } -} 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 87cfc34e636a5..6f7c6a4b9c5f4 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 @@ -15,8 +15,6 @@ from source_amazon_seller_partner.auth import AWSAuthenticator from source_amazon_seller_partner.constants import get_marketplaces from source_amazon_seller_partner.streams import ( - BrandAnalyticsAlternatePurchaseReports, - BrandAnalyticsItemComparisonReports, BrandAnalyticsMarketBasketReports, BrandAnalyticsRepeatPurchaseReports, BrandAnalyticsSearchTermsReports, @@ -187,8 +185,6 @@ def streams(self, config: Mapping[str, Any]) -> List[Stream]: BrandAnalyticsMarketBasketReports, BrandAnalyticsSearchTermsReports, BrandAnalyticsRepeatPurchaseReports, - BrandAnalyticsAlternatePurchaseReports, - BrandAnalyticsItemComparisonReports, SellerAnalyticsSalesAndTrafficReports, VendorSalesReports, VendorInventoryReports, diff --git a/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/spec.json b/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/spec.json index a74cf6ea90793..9f84b550d8e73 100644 --- a/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/spec.json +++ b/airbyte-integrations/connectors/source-amazon-seller-partner/source_amazon_seller_partner/spec.json @@ -132,8 +132,6 @@ "GET_AFN_INVENTORY_DATA", "GET_AFN_INVENTORY_DATA_BY_COUNTRY", "GET_AMAZON_FULFILLED_SHIPMENTS_DATA_GENERAL", - "GET_BRAND_ANALYTICS_ALTERNATE_PURCHASE_REPORT", - "GET_BRAND_ANALYTICS_ITEM_COMPARISON_REPORT", "GET_BRAND_ANALYTICS_MARKET_BASKET_REPORT", "GET_BRAND_ANALYTICS_REPEAT_PURCHASE_REPORT", "GET_BRAND_ANALYTICS_SEARCH_TERMS_REPORT", 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 d65c6db580773..c0d4afcb3c607 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 @@ -785,16 +785,6 @@ class BrandAnalyticsRepeatPurchaseReports(IncrementalAnalyticsStream): result_key = "dataByAsin" -class BrandAnalyticsAlternatePurchaseReports(IncrementalAnalyticsStream): - name = "GET_BRAND_ANALYTICS_ALTERNATE_PURCHASE_REPORT" - result_key = "dataByAsin" - - -class BrandAnalyticsItemComparisonReports(IncrementalAnalyticsStream): - name = "GET_BRAND_ANALYTICS_ITEM_COMPARISON_REPORT" - result_key = "dataByAsin" - - class VendorInventoryReports(IncrementalAnalyticsStream): """ Field definitions: https://developer-docs.amazon.com/sp-api/docs/report-type-values#vendor-retail-analytics-reports diff --git a/docs/integrations/sources/amazon-seller-partner.md b/docs/integrations/sources/amazon-seller-partner.md index 1d3f2606e3b73..7bcb6d06f3ed8 100644 --- a/docs/integrations/sources/amazon-seller-partner.md +++ b/docs/integrations/sources/amazon-seller-partner.md @@ -77,8 +77,6 @@ The Amazon Seller Partner source connector supports the following [sync modes](h - [Active Listings Report](https://developer-docs.amazon.com/sp-api/docs/report-type-values-inventory) \(incremental\) - [All Listings Report](https://developer-docs.amazon.com/sp-api/docs/report-type-values-inventory) \(incremental\) - [Amazon Search Terms Report](https://developer-docs.amazon.com/sp-api/docs/report-type-values-analytics#brand-analytics-reports) \(only available in OSS, incremental\) -- [Brand Analytics Alternate Purchase Report](https://developer-docs.amazon.com/sp-api/docs/report-type-values-analytics#brand-analytics-reports) \(only available in OSS, incremental\) -- [Brand Analytics Item Comparison Report](https://developer-docs.amazon.com/sp-api/docs/report-type-values-analytics#brand-analytics-reports) \(only available in OSS, incremental\) - [Browse Tree Report](https://developer-docs.amazon.com/sp-api/docs/report-type-values-browse-tree) \(incremental\) - [Canceled Listings Report](https://developer-docs.amazon.com/sp-api/docs/report-type-values-inventory) \(incremental\) - [FBA Amazon Fulfilled Inventory Report](https://developer-docs.amazon.com/sp-api/docs/report-type-values-fba#fba-inventory-reports) \(incremental\) @@ -155,6 +153,7 @@ Information about rate limits you may find [here](https://developer-docs.amazon. | Version | Date | Pull Request | Subject | |:---------|:-----------|:------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `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 | From 03c725b9506cdc393b0f1d65888088b49d73fd83 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 17 Jan 2024 14:58:26 +0100 Subject: [PATCH 128/574] Unify lowcode tags (#34284) --- .../source-configuration-based/metadata.yaml.hbs | 2 +- .../connectors/source-apify-dataset/metadata.yaml | 2 +- airbyte-integrations/connectors/source-appfollow/metadata.yaml | 2 +- airbyte-integrations/connectors/source-auth0/metadata.yaml | 2 +- airbyte-integrations/connectors/source-babelforce/metadata.yaml | 2 +- airbyte-integrations/connectors/source-chargify/metadata.yaml | 2 +- airbyte-integrations/connectors/source-clockify/metadata.yaml | 2 +- .../connectors/source-commercetools/metadata.yaml | 2 +- airbyte-integrations/connectors/source-copper/metadata.yaml | 2 +- .../connectors/source-customer-io/metadata.yaml | 2 +- airbyte-integrations/connectors/source-dixa/metadata.yaml | 2 +- airbyte-integrations/connectors/source-dockerhub/metadata.yaml | 2 +- airbyte-integrations/connectors/source-drift/metadata.yaml | 2 +- .../connectors/source-exchange-rates/metadata.yaml | 2 +- airbyte-integrations/connectors/source-fastbill/metadata.yaml | 2 +- .../connectors/source-freshcaller/metadata.yaml | 2 +- .../connectors/source-freshservice/metadata.yaml | 2 +- airbyte-integrations/connectors/source-glassfrog/metadata.yaml | 2 +- airbyte-integrations/connectors/source-harness/metadata.yaml | 2 +- airbyte-integrations/connectors/source-hubplanner/metadata.yaml | 2 +- airbyte-integrations/connectors/source-insightly/metadata.yaml | 2 +- airbyte-integrations/connectors/source-lemlist/metadata.yaml | 2 +- airbyte-integrations/connectors/source-nasa/metadata.yaml | 2 +- airbyte-integrations/connectors/source-onesignal/metadata.yaml | 2 +- .../connectors/source-open-exchange-rates/metadata.yaml | 2 +- .../connectors/source-openweather/metadata.yaml | 2 +- airbyte-integrations/connectors/source-opsgenie/metadata.yaml | 2 +- airbyte-integrations/connectors/source-orbit/metadata.yaml | 2 +- airbyte-integrations/connectors/source-pagerduty/metadata.yaml | 2 +- airbyte-integrations/connectors/source-persistiq/metadata.yaml | 2 +- airbyte-integrations/connectors/source-pipedrive/metadata.yaml | 2 +- airbyte-integrations/connectors/source-pokeapi/metadata.yaml | 2 +- .../connectors/source-public-apis/metadata.yaml | 2 +- airbyte-integrations/connectors/source-qonto/metadata.yaml | 2 +- airbyte-integrations/connectors/source-qualaroo/metadata.yaml | 2 +- airbyte-integrations/connectors/source-serpstat/metadata.yaml | 2 +- airbyte-integrations/connectors/source-shortio/metadata.yaml | 2 +- airbyte-integrations/connectors/source-todoist/metadata.yaml | 2 +- .../connectors/source-visma-economic/metadata.yaml | 2 +- airbyte-integrations/connectors/source-wrike/metadata.yaml | 2 +- airbyte-integrations/connectors/source-younium/metadata.yaml | 2 +- .../connectors/source-zendesk-sell/metadata.yaml | 2 +- airbyte-integrations/connectors/source-zenefits/metadata.yaml | 2 +- 43 files changed, 43 insertions(+), 43 deletions(-) diff --git a/airbyte-integrations/connector-templates/source-configuration-based/metadata.yaml.hbs b/airbyte-integrations/connector-templates/source-configuration-based/metadata.yaml.hbs index f162a414b44db..418482a0b2463 100644 --- a/airbyte-integrations/connector-templates/source-configuration-based/metadata.yaml.hbs +++ b/airbyte-integrations/connector-templates/source-configuration-based/metadata.yaml.hbs @@ -26,5 +26,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/{{dashCase name}} tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-apify-dataset/metadata.yaml b/airbyte-integrations/connectors/source-apify-dataset/metadata.yaml index f3421f116b826..0e4e0668f3d49 100644 --- a/airbyte-integrations/connectors/source-apify-dataset/metadata.yaml +++ b/airbyte-integrations/connectors/source-apify-dataset/metadata.yaml @@ -29,5 +29,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/apify-dataset tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-appfollow/metadata.yaml b/airbyte-integrations/connectors/source-appfollow/metadata.yaml index e65309bd6e971..7e9cb43f0cac0 100644 --- a/airbyte-integrations/connectors/source-appfollow/metadata.yaml +++ b/airbyte-integrations/connectors/source-appfollow/metadata.yaml @@ -20,7 +20,7 @@ data: releaseStage: alpha documentationUrl: https://docs.airbyte.com/integrations/sources/appfollow tags: - - language:lowcode + - language:low-code releases: breakingChanges: 1.0.0: diff --git a/airbyte-integrations/connectors/source-auth0/metadata.yaml b/airbyte-integrations/connectors/source-auth0/metadata.yaml index 5eb0080a09cb9..9566ee9d78002 100644 --- a/airbyte-integrations/connectors/source-auth0/metadata.yaml +++ b/airbyte-integrations/connectors/source-auth0/metadata.yaml @@ -26,5 +26,5 @@ data: releaseStage: alpha supportLevel: community tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-babelforce/metadata.yaml b/airbyte-integrations/connectors/source-babelforce/metadata.yaml index 9425eae3c84b8..f4f4e35ea4ba8 100644 --- a/airbyte-integrations/connectors/source-babelforce/metadata.yaml +++ b/airbyte-integrations/connectors/source-babelforce/metadata.yaml @@ -20,7 +20,7 @@ data: releaseStage: alpha documentationUrl: https://docs.airbyte.com/integrations/sources/babelforce tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-chargify/metadata.yaml b/airbyte-integrations/connectors/source-chargify/metadata.yaml index f211a8bfecbce..cc211d82e70cb 100644 --- a/airbyte-integrations/connectors/source-chargify/metadata.yaml +++ b/airbyte-integrations/connectors/source-chargify/metadata.yaml @@ -21,7 +21,7 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/chargify tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-clockify/metadata.yaml b/airbyte-integrations/connectors/source-clockify/metadata.yaml index f26a0e6f214cc..90baae2689d83 100644 --- a/airbyte-integrations/connectors/source-clockify/metadata.yaml +++ b/airbyte-integrations/connectors/source-clockify/metadata.yaml @@ -21,7 +21,7 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/clockify tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-commercetools/metadata.yaml b/airbyte-integrations/connectors/source-commercetools/metadata.yaml index dcbb88d3fafe4..5af0802c2c0ec 100644 --- a/airbyte-integrations/connectors/source-commercetools/metadata.yaml +++ b/airbyte-integrations/connectors/source-commercetools/metadata.yaml @@ -20,7 +20,7 @@ data: releaseStage: alpha documentationUrl: https://docs.airbyte.com/integrations/sources/commercetools tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-copper/metadata.yaml b/airbyte-integrations/connectors/source-copper/metadata.yaml index cfa55715d0c4a..283dd98e3a15f 100644 --- a/airbyte-integrations/connectors/source-copper/metadata.yaml +++ b/airbyte-integrations/connectors/source-copper/metadata.yaml @@ -20,5 +20,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/copper tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-customer-io/metadata.yaml b/airbyte-integrations/connectors/source-customer-io/metadata.yaml index 548f40de65ea8..3cf0252a2512a 100644 --- a/airbyte-integrations/connectors/source-customer-io/metadata.yaml +++ b/airbyte-integrations/connectors/source-customer-io/metadata.yaml @@ -21,5 +21,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/customer-io tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-dixa/metadata.yaml b/airbyte-integrations/connectors/source-dixa/metadata.yaml index 2bd71f0c367a4..9d45d0bb45f72 100644 --- a/airbyte-integrations/connectors/source-dixa/metadata.yaml +++ b/airbyte-integrations/connectors/source-dixa/metadata.yaml @@ -21,7 +21,7 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/dixa tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-dockerhub/metadata.yaml b/airbyte-integrations/connectors/source-dockerhub/metadata.yaml index 3afe525444126..e0b403736301e 100644 --- a/airbyte-integrations/connectors/source-dockerhub/metadata.yaml +++ b/airbyte-integrations/connectors/source-dockerhub/metadata.yaml @@ -21,7 +21,7 @@ data: releaseStage: alpha documentationUrl: https://docs.airbyte.com/integrations/sources/dockerhub tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-drift/metadata.yaml b/airbyte-integrations/connectors/source-drift/metadata.yaml index f17ce487e2361..12e3531ac2c31 100644 --- a/airbyte-integrations/connectors/source-drift/metadata.yaml +++ b/airbyte-integrations/connectors/source-drift/metadata.yaml @@ -21,7 +21,7 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/drift tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-exchange-rates/metadata.yaml b/airbyte-integrations/connectors/source-exchange-rates/metadata.yaml index dc7062891b8fd..20d3fa38cb7c6 100644 --- a/airbyte-integrations/connectors/source-exchange-rates/metadata.yaml +++ b/airbyte-integrations/connectors/source-exchange-rates/metadata.yaml @@ -22,5 +22,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/exchange-rates tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-fastbill/metadata.yaml b/airbyte-integrations/connectors/source-fastbill/metadata.yaml index 805d8dd61b3c1..9a6e795f561db 100644 --- a/airbyte-integrations/connectors/source-fastbill/metadata.yaml +++ b/airbyte-integrations/connectors/source-fastbill/metadata.yaml @@ -21,5 +21,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/fastbill tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-freshcaller/metadata.yaml b/airbyte-integrations/connectors/source-freshcaller/metadata.yaml index 5d8dc4615aa96..ac2d4fb292611 100644 --- a/airbyte-integrations/connectors/source-freshcaller/metadata.yaml +++ b/airbyte-integrations/connectors/source-freshcaller/metadata.yaml @@ -17,7 +17,7 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/freshcaller tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-freshservice/metadata.yaml b/airbyte-integrations/connectors/source-freshservice/metadata.yaml index 672168762c1aa..43c5dd48d13f5 100644 --- a/airbyte-integrations/connectors/source-freshservice/metadata.yaml +++ b/airbyte-integrations/connectors/source-freshservice/metadata.yaml @@ -21,5 +21,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/freshservice tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-glassfrog/metadata.yaml b/airbyte-integrations/connectors/source-glassfrog/metadata.yaml index 051a5ef1d90cd..b75ae80dbeb8a 100644 --- a/airbyte-integrations/connectors/source-glassfrog/metadata.yaml +++ b/airbyte-integrations/connectors/source-glassfrog/metadata.yaml @@ -20,7 +20,7 @@ data: releaseStage: alpha documentationUrl: https://docs.airbyte.com/integrations/sources/glassfrog tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-harness/metadata.yaml b/airbyte-integrations/connectors/source-harness/metadata.yaml index 857504fc25052..a33d83b938598 100644 --- a/airbyte-integrations/connectors/source-harness/metadata.yaml +++ b/airbyte-integrations/connectors/source-harness/metadata.yaml @@ -21,5 +21,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/harness tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-hubplanner/metadata.yaml b/airbyte-integrations/connectors/source-hubplanner/metadata.yaml index ab23f4f28def0..59487dacb6f16 100644 --- a/airbyte-integrations/connectors/source-hubplanner/metadata.yaml +++ b/airbyte-integrations/connectors/source-hubplanner/metadata.yaml @@ -21,7 +21,7 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/hubplanner tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-insightly/metadata.yaml b/airbyte-integrations/connectors/source-insightly/metadata.yaml index 321d751ffa7a5..78f6fc859bf65 100644 --- a/airbyte-integrations/connectors/source-insightly/metadata.yaml +++ b/airbyte-integrations/connectors/source-insightly/metadata.yaml @@ -20,7 +20,7 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/insightly tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-lemlist/metadata.yaml b/airbyte-integrations/connectors/source-lemlist/metadata.yaml index 1b9d60189c197..cf4bd7c33ff89 100644 --- a/airbyte-integrations/connectors/source-lemlist/metadata.yaml +++ b/airbyte-integrations/connectors/source-lemlist/metadata.yaml @@ -21,5 +21,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/lemlist tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-nasa/metadata.yaml b/airbyte-integrations/connectors/source-nasa/metadata.yaml index f5fa8ac2e653e..1cd017174495b 100644 --- a/airbyte-integrations/connectors/source-nasa/metadata.yaml +++ b/airbyte-integrations/connectors/source-nasa/metadata.yaml @@ -21,5 +21,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/nasa tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-onesignal/metadata.yaml b/airbyte-integrations/connectors/source-onesignal/metadata.yaml index c63f53736d6d8..cedb38e32be9d 100644 --- a/airbyte-integrations/connectors/source-onesignal/metadata.yaml +++ b/airbyte-integrations/connectors/source-onesignal/metadata.yaml @@ -20,6 +20,6 @@ data: releaseDate: 2023-08-31 releaseStage: alpha tags: - - language:lowcode + - language:low-code supportLevel: community metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-open-exchange-rates/metadata.yaml b/airbyte-integrations/connectors/source-open-exchange-rates/metadata.yaml index f93f966771279..7c5f21637fae8 100644 --- a/airbyte-integrations/connectors/source-open-exchange-rates/metadata.yaml +++ b/airbyte-integrations/connectors/source-open-exchange-rates/metadata.yaml @@ -21,5 +21,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/open-exchange-rates tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-openweather/metadata.yaml b/airbyte-integrations/connectors/source-openweather/metadata.yaml index 1e71bb97fc7f2..b15d5744b4f97 100644 --- a/airbyte-integrations/connectors/source-openweather/metadata.yaml +++ b/airbyte-integrations/connectors/source-openweather/metadata.yaml @@ -20,5 +20,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/openweather tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-opsgenie/metadata.yaml b/airbyte-integrations/connectors/source-opsgenie/metadata.yaml index 4af87df040a97..17edbf7b36e53 100644 --- a/airbyte-integrations/connectors/source-opsgenie/metadata.yaml +++ b/airbyte-integrations/connectors/source-opsgenie/metadata.yaml @@ -15,7 +15,7 @@ data: releaseStage: alpha documentationUrl: https://docs.airbyte.com/integrations/sources/opsgenie tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-orbit/metadata.yaml b/airbyte-integrations/connectors/source-orbit/metadata.yaml index 2d19e453ccdc5..5accdc1941407 100644 --- a/airbyte-integrations/connectors/source-orbit/metadata.yaml +++ b/airbyte-integrations/connectors/source-orbit/metadata.yaml @@ -21,7 +21,7 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/orbit tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-pagerduty/metadata.yaml b/airbyte-integrations/connectors/source-pagerduty/metadata.yaml index 403e4bd393d76..0b471649a4a4d 100644 --- a/airbyte-integrations/connectors/source-pagerduty/metadata.yaml +++ b/airbyte-integrations/connectors/source-pagerduty/metadata.yaml @@ -21,7 +21,7 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/pagerduty tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-persistiq/metadata.yaml b/airbyte-integrations/connectors/source-persistiq/metadata.yaml index ded7693cd2c9a..2d06105f0138a 100644 --- a/airbyte-integrations/connectors/source-persistiq/metadata.yaml +++ b/airbyte-integrations/connectors/source-persistiq/metadata.yaml @@ -21,5 +21,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/persistiq tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-pipedrive/metadata.yaml b/airbyte-integrations/connectors/source-pipedrive/metadata.yaml index 74b572e3fad6e..5f0810ee45565 100644 --- a/airbyte-integrations/connectors/source-pipedrive/metadata.yaml +++ b/airbyte-integrations/connectors/source-pipedrive/metadata.yaml @@ -29,5 +29,5 @@ data: releaseStage: alpha supportLevel: community tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-pokeapi/metadata.yaml b/airbyte-integrations/connectors/source-pokeapi/metadata.yaml index 85047f016528f..076a75a780a48 100644 --- a/airbyte-integrations/connectors/source-pokeapi/metadata.yaml +++ b/airbyte-integrations/connectors/source-pokeapi/metadata.yaml @@ -21,5 +21,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/pokeapi tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-public-apis/metadata.yaml b/airbyte-integrations/connectors/source-public-apis/metadata.yaml index 0df0a0c7da8e2..c83aba288893c 100644 --- a/airbyte-integrations/connectors/source-public-apis/metadata.yaml +++ b/airbyte-integrations/connectors/source-public-apis/metadata.yaml @@ -23,5 +23,5 @@ data: releaseStage: alpha supportLevel: community tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-qonto/metadata.yaml b/airbyte-integrations/connectors/source-qonto/metadata.yaml index 83239b6c7d130..05c0393aa6ec3 100644 --- a/airbyte-integrations/connectors/source-qonto/metadata.yaml +++ b/airbyte-integrations/connectors/source-qonto/metadata.yaml @@ -13,5 +13,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/qonto tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-qualaroo/metadata.yaml b/airbyte-integrations/connectors/source-qualaroo/metadata.yaml index 7364d8abe5c36..e291b82f879a1 100644 --- a/airbyte-integrations/connectors/source-qualaroo/metadata.yaml +++ b/airbyte-integrations/connectors/source-qualaroo/metadata.yaml @@ -21,5 +21,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/qualaroo tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-serpstat/metadata.yaml b/airbyte-integrations/connectors/source-serpstat/metadata.yaml index e764c9c80059a..fb9d7de247376 100644 --- a/airbyte-integrations/connectors/source-serpstat/metadata.yaml +++ b/airbyte-integrations/connectors/source-serpstat/metadata.yaml @@ -18,5 +18,5 @@ data: releaseStage: alpha documentationUrl: https://docs.airbyte.com/integrations/sources/serpstat tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-shortio/metadata.yaml b/airbyte-integrations/connectors/source-shortio/metadata.yaml index ad64fec0533ce..6eced88a6f028 100644 --- a/airbyte-integrations/connectors/source-shortio/metadata.yaml +++ b/airbyte-integrations/connectors/source-shortio/metadata.yaml @@ -23,7 +23,7 @@ data: documentationUrl: https://docs.airbyte.com/integrations/sources/shortio tags: - language:python - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-todoist/metadata.yaml b/airbyte-integrations/connectors/source-todoist/metadata.yaml index c6fd1c2b58ecb..b578c8f07755b 100644 --- a/airbyte-integrations/connectors/source-todoist/metadata.yaml +++ b/airbyte-integrations/connectors/source-todoist/metadata.yaml @@ -26,5 +26,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/todoist tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-visma-economic/metadata.yaml b/airbyte-integrations/connectors/source-visma-economic/metadata.yaml index ec378e98d2b48..56e6d7330ce5f 100644 --- a/airbyte-integrations/connectors/source-visma-economic/metadata.yaml +++ b/airbyte-integrations/connectors/source-visma-economic/metadata.yaml @@ -21,5 +21,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/visma-economic tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-wrike/metadata.yaml b/airbyte-integrations/connectors/source-wrike/metadata.yaml index 9e57391670c9e..7a6309f77cb92 100644 --- a/airbyte-integrations/connectors/source-wrike/metadata.yaml +++ b/airbyte-integrations/connectors/source-wrike/metadata.yaml @@ -23,5 +23,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/wrike tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-younium/metadata.yaml b/airbyte-integrations/connectors/source-younium/metadata.yaml index 892a1ee423c0f..c8d91423df437 100644 --- a/airbyte-integrations/connectors/source-younium/metadata.yaml +++ b/airbyte-integrations/connectors/source-younium/metadata.yaml @@ -23,5 +23,5 @@ data: supportLevel: community documentationUrl: https://docs.airbyte.com/integrations/sources/younium tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-zendesk-sell/metadata.yaml b/airbyte-integrations/connectors/source-zendesk-sell/metadata.yaml index 803cca0d539ba..5f11fc4305bf4 100644 --- a/airbyte-integrations/connectors/source-zendesk-sell/metadata.yaml +++ b/airbyte-integrations/connectors/source-zendesk-sell/metadata.yaml @@ -19,7 +19,7 @@ data: releaseStage: alpha documentationUrl: https://docs.airbyte.com/integrations/sources/zendesk-sell tags: - - language:lowcode + - language:low-code ab_internal: sl: 100 ql: 100 diff --git a/airbyte-integrations/connectors/source-zenefits/metadata.yaml b/airbyte-integrations/connectors/source-zenefits/metadata.yaml index 7392e0333eec4..b5eec18af0581 100644 --- a/airbyte-integrations/connectors/source-zenefits/metadata.yaml +++ b/airbyte-integrations/connectors/source-zenefits/metadata.yaml @@ -24,5 +24,5 @@ data: ql: 100 supportLevel: community tags: - - language:lowcode + - language:low-code metadataSpecVersion: "1.0" From be06855dc02bdb1ebbf5dfee716c156b824a017f Mon Sep 17 00:00:00 2001 From: Akash Kulkarni <113392464+akashkulk@users.noreply.github.com> Date: Wed, 17 Jan 2024 09:04:31 -0800 Subject: [PATCH 129/574] [Source-mongo] : Relax minimum document discovery size to 100 (#34314) --- .../source-mongodb-v2/integration_tests/expected_spec.json | 2 +- airbyte-integrations/connectors/source-mongodb-v2/metadata.yaml | 2 +- .../connectors/source-mongodb-v2/src/main/resources/spec.json | 2 +- docs/integrations/sources/mongodb-v2.md | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/airbyte-integrations/connectors/source-mongodb-v2/integration_tests/expected_spec.json b/airbyte-integrations/connectors/source-mongodb-v2/integration_tests/expected_spec.json index 54e3d7aa189c5..8de6fd3bc30ea 100644 --- a/airbyte-integrations/connectors/source-mongodb-v2/integration_tests/expected_spec.json +++ b/airbyte-integrations/connectors/source-mongodb-v2/integration_tests/expected_spec.json @@ -164,7 +164,7 @@ "description": "The maximum number of documents to sample when attempting to discover the unique fields for a collection.", "default": 10000, "order": 10, - "minimum": 1000, + "minimum": 100, "maximum": 100000, "group": "advanced" } diff --git a/airbyte-integrations/connectors/source-mongodb-v2/metadata.yaml b/airbyte-integrations/connectors/source-mongodb-v2/metadata.yaml index fb5be28de49d6..086852451f42d 100644 --- a/airbyte-integrations/connectors/source-mongodb-v2/metadata.yaml +++ b/airbyte-integrations/connectors/source-mongodb-v2/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: database connectorType: source definitionId: b2e713cd-cc36-4c0a-b5bd-b47cb8a0561e - dockerImageTag: 1.2.1 + dockerImageTag: 1.2.2 dockerRepository: airbyte/source-mongodb-v2 documentationUrl: https://docs.airbyte.com/integrations/sources/mongodb-v2 githubIssueLabel: source-mongodb-v2 diff --git a/airbyte-integrations/connectors/source-mongodb-v2/src/main/resources/spec.json b/airbyte-integrations/connectors/source-mongodb-v2/src/main/resources/spec.json index ae6b822110ca0..4acd5c67d25f8 100644 --- a/airbyte-integrations/connectors/source-mongodb-v2/src/main/resources/spec.json +++ b/airbyte-integrations/connectors/source-mongodb-v2/src/main/resources/spec.json @@ -164,7 +164,7 @@ "description": "The maximum number of documents to sample when attempting to discover the unique fields for a collection.", "default": 10000, "order": 10, - "minimum": 1000, + "minimum": 100, "maximum": 100000, "group": "advanced" } diff --git a/docs/integrations/sources/mongodb-v2.md b/docs/integrations/sources/mongodb-v2.md index bc9ffb1820e29..c7821542174bb 100644 --- a/docs/integrations/sources/mongodb-v2.md +++ b/docs/integrations/sources/mongodb-v2.md @@ -214,6 +214,7 @@ For more information regarding configuration parameters, please see [MongoDb Doc | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:----------------------------------------------------------------------------------------------------------| +| 1.2.2 | 2024-01-16 | [34314](https://github.com/airbytehq/airbyte/pull/34314) | Reduce minimum document discovery size to 100. | | 1.2.1 | 2023-12-18 | [33549](https://github.com/airbytehq/airbyte/pull/33549) | Add logging to understand op log size. | | 1.2.0 | 2023-12-18 | [33438](https://github.com/airbytehq/airbyte/pull/33438) | Remove LEGACY state flag | | 1.1.0 | 2023-12-14 | [32328](https://github.com/airbytehq/airbyte/pull/32328) | Schema less mode in mongodb. | From 63c6961e78c6a5c77ccc70c588ba934dbd2fa885 Mon Sep 17 00:00:00 2001 From: Anatolii Yatsuk <35109939+tolik0@users.noreply.github.com> Date: Wed, 17 Jan 2024 19:15:09 +0200 Subject: [PATCH 130/574] =?UTF-8?q?=E2=9C=A8=20Source=20S3:=20Add=20IAM=20?= =?UTF-8?q?Role=20Authentication=20(#33818)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../source-s3/acceptance-test-config.yml | 18 ++++ .../source-s3/integration_tests/acceptance.py | 34 +++++++- .../integration_tests/cloud_spec.json | 13 +++ .../source-s3/integration_tests/spec.json | 13 +++ .../connectors/source-s3/metadata.yaml | 2 +- .../connectors/source-s3/setup.py | 8 +- .../connectors/source-s3/source_s3/source.py | 10 ++- .../source-s3/source_s3/v4/config.py | 9 +- .../source-s3/source_s3/v4/stream_reader.py | 69 ++++++++++++++-- .../unit_tests/v4/test_stream_reader.py | 31 +++++++ docs/integrations/sources/s3.md | 82 +++++++++++++++++-- 11 files changed, 263 insertions(+), 26 deletions(-) diff --git a/airbyte-integrations/connectors/source-s3/acceptance-test-config.yml b/airbyte-integrations/connectors/source-s3/acceptance-test-config.yml index 252460fff68d6..4d8db46aba243 100644 --- a/airbyte-integrations/connectors/source-s3/acceptance-test-config.yml +++ b/airbyte-integrations/connectors/source-s3/acceptance-test-config.yml @@ -6,6 +6,11 @@ acceptance_tests: path: integration_tests/expected_records/csv.jsonl exact_order: true timeout_seconds: 1800 + - config_path: secrets/config_iam_role.json + expect_records: + path: integration_tests/expected_records/csv.jsonl + exact_order: true + timeout_seconds: 1800 - config_path: secrets/v4_csv_custom_encoding_config.json expect_records: path: integration_tests/expected_records/legacy_csv_custom_encoding.jsonl @@ -110,6 +115,8 @@ acceptance_tests: tests: - config_path: secrets/config.json status: succeed + - config_path: secrets/config_iam_role.json + status: succeed - config_path: secrets/v4_csv_custom_encoding_config.json status: succeed - config_path: secrets/v4_csv_custom_format_config.json @@ -148,6 +155,9 @@ acceptance_tests: status: failed discovery: tests: + - config_path: secrets/config_iam_role.json + backward_compatibility_tests_config: + disable_for_version: "4.4.0" # new authentication added - IAM role - config_path: secrets/config.json backward_compatibility_tests_config: disable_for_version: "4.0.3" # removing the `streams.*.file_type` field which was redundant with `streams.*.format` @@ -201,6 +211,9 @@ acceptance_tests: - config_path: secrets/config.json configured_catalog_path: integration_tests/configured_catalogs/csv.json timeout_seconds: 1800 + - config_path: secrets/config_iam_role.json + configured_catalog_path: integration_tests/configured_catalogs/csv.json + timeout_seconds: 1800 - config_path: secrets/v4_parquet_config.json configured_catalog_path: integration_tests/configured_catalogs/parquet.json timeout_seconds: 1800 @@ -236,6 +249,11 @@ acceptance_tests: future_state: future_state_path: integration_tests/abnormal_state.json timeout_seconds: 1800 + - config_path: secrets/config_iam_role.json + configured_catalog_path: integration_tests/configured_catalogs/csv.json + future_state: + future_state_path: integration_tests/abnormal_state.json + timeout_seconds: 1800 - config_path: secrets/v4_parquet_config.json configured_catalog_path: integration_tests/configured_catalogs/parquet.json future_state: diff --git a/airbyte-integrations/connectors/source-s3/integration_tests/acceptance.py b/airbyte-integrations/connectors/source-s3/integration_tests/acceptance.py index 6b0c294530cd2..706e9eba88bea 100644 --- a/airbyte-integrations/connectors/source-s3/integration_tests/acceptance.py +++ b/airbyte-integrations/connectors/source-s3/integration_tests/acceptance.py @@ -3,14 +3,46 @@ # +import json +import logging +from pathlib import Path from typing import Iterable import pytest +import yaml pytest_plugins = ("connector_acceptance_test.plugin",) +logger = logging.getLogger("airbyte") @pytest.fixture(scope="session", autouse=True) def connector_setup() -> Iterable[None]: - """This fixture is a placeholder for external resources that acceptance test might require.""" + """This fixture is responsible for configuring AWS credentials that are used for assuming role during the IAM role based authentication.""" + config_file_path = "secrets/config_iam_role.json" + acceptance_test_config_file_path = "acceptance-test-config.yml" + + # Read environment variables from the JSON file + with open(config_file_path, "r") as file: + config = json.load(file) + + # Prepare environment variables to append to the YAML file + env_vars = { + "custom_environment_variables": { + "AWS_ASSUME_ROLE_EXTERNAL_ID": config["acceptance_test_aws_external_id"], + "AWS_ACCESS_KEY_ID": config["acceptance_test_aws_access_key_id"], + "AWS_SECRET_ACCESS_KEY": config["acceptance_test_aws_secret_access_key"], + } + } + + # Append environment variables to the YAML file + yaml_path = Path(acceptance_test_config_file_path) + if yaml_path.is_file(): + with open(acceptance_test_config_file_path, "r") as file: + existing_data = yaml.safe_load(file) or {} + existing_data.update(env_vars) + with open(acceptance_test_config_file_path, "w") as file: + yaml.safe_dump(existing_data, file) + else: + raise Exception(f"{acceptance_test_config_file_path} does not exist.") + yield diff --git a/airbyte-integrations/connectors/source-s3/integration_tests/cloud_spec.json b/airbyte-integrations/connectors/source-s3/integration_tests/cloud_spec.json index b2593df8e5c94..ed084d3b08d3d 100644 --- a/airbyte-integrations/connectors/source-s3/integration_tests/cloud_spec.json +++ b/airbyte-integrations/connectors/source-s3/integration_tests/cloud_spec.json @@ -358,6 +358,12 @@ "order": 2, "type": "string" }, + "role_arn": { + "title": "AWS Role ARN", + "description": "Specifies the Amazon Resource Name (ARN) of an IAM role that you want to use to perform operations requested using this profile. Set the External ID to the Airbyte workspace ID, which can be found in the URL of this page.", + "order": 6, + "type": "string" + }, "aws_secret_access_key": { "title": "AWS Secret Access Key", "description": "In order to access private Buckets stored on AWS S3, this connector requires credentials with the proper permissions. If accessing publicly available data, this field is not necessary.", @@ -610,6 +616,13 @@ "order": 2, "type": "string" }, + "role_arn": { + "title": "AWS Role ARN", + "description": "Specifies the Amazon Resource Name (ARN) of an IAM role that you want to use to perform operations requested using this profile. Set the External ID to the Airbyte workspace ID, which can be found in the URL of this page.", + "always_show": true, + "order": 6, + "type": "string" + }, "path_prefix": { "title": "Path Prefix", "description": "By providing a path-like prefix (e.g. myFolder/thisTable/) under which all the relevant files sit, we can optimize finding these in S3. This is optional but recommended if your bucket contains many folders/files which you don't need to replicate.", diff --git a/airbyte-integrations/connectors/source-s3/integration_tests/spec.json b/airbyte-integrations/connectors/source-s3/integration_tests/spec.json index dd3309240ada9..76a48ffb09a5d 100644 --- a/airbyte-integrations/connectors/source-s3/integration_tests/spec.json +++ b/airbyte-integrations/connectors/source-s3/integration_tests/spec.json @@ -365,6 +365,12 @@ "order": 3, "type": "string" }, + "role_arn": { + "title": "AWS Role ARN", + "description": "Specifies the Amazon Resource Name (ARN) of an IAM role that you want to use to perform operations requested using this profile. Set the External ID to the Airbyte workspace ID, which can be found in the URL of this page.", + "order": 6, + "type": "string" + }, "endpoint": { "title": "Endpoint", "description": "Endpoint to an S3 compatible service. Leave empty to use AWS.", @@ -609,6 +615,13 @@ "order": 2, "type": "string" }, + "role_arn": { + "title": "AWS Role ARN", + "description": "Specifies the Amazon Resource Name (ARN) of an IAM role that you want to use to perform operations requested using this profile. Set the External ID to the Airbyte workspace ID, which can be found in the URL of this page.", + "always_show": true, + "order": 6, + "type": "string" + }, "path_prefix": { "title": "Path Prefix", "description": "By providing a path-like prefix (e.g. myFolder/thisTable/) under which all the relevant files sit, we can optimize finding these in S3. This is optional but recommended if your bucket contains many folders/files which you don't need to replicate.", diff --git a/airbyte-integrations/connectors/source-s3/metadata.yaml b/airbyte-integrations/connectors/source-s3/metadata.yaml index 8f13506605fdf..d660125e526be 100644 --- a/airbyte-integrations/connectors/source-s3/metadata.yaml +++ b/airbyte-integrations/connectors/source-s3/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: file connectorType: source definitionId: 69589781-7828-43c5-9f63-8925b1c1ccc2 - dockerImageTag: 4.3.1 + dockerImageTag: 4.4.0 dockerRepository: airbyte/source-s3 documentationUrl: https://docs.airbyte.com/integrations/sources/s3 githubIssueLabel: source-s3 diff --git a/airbyte-integrations/connectors/source-s3/setup.py b/airbyte-integrations/connectors/source-s3/setup.py index e2d0000f29499..cd2c48e2924dc 100644 --- a/airbyte-integrations/connectors/source-s3/setup.py +++ b/airbyte-integrations/connectors/source-s3/setup.py @@ -14,13 +14,7 @@ "python-snappy==0.6.1", ] -TEST_REQUIREMENTS = [ - "requests-mock~=1.9.3", - "pytest-mock~=3.6.1", - "pytest~=6.1", - "pandas==2.0.3", - "docker", -] +TEST_REQUIREMENTS = ["requests-mock~=1.9.3", "pytest-mock~=3.6.1", "pytest~=6.1", "pandas==2.0.3", "docker", "moto"] setup( name="source_s3", diff --git a/airbyte-integrations/connectors/source-s3/source_s3/source.py b/airbyte-integrations/connectors/source-s3/source_s3/source.py index 8621fe4bbb5af..224f7b036e4a6 100644 --- a/airbyte-integrations/connectors/source-s3/source_s3/source.py +++ b/airbyte-integrations/connectors/source-s3/source_s3/source.py @@ -1,8 +1,6 @@ # # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - - from typing import Optional from pydantic import BaseModel, Field @@ -39,6 +37,14 @@ class Config: always_show=True, order=2, ) + role_arn: Optional[str] = Field( + title=f"AWS Role ARN", + default=None, + description="Specifies the Amazon Resource Name (ARN) of an IAM role that you want to use to perform operations " + f"requested using this profile. Set the External ID to the Airbyte workspace ID, which can be found in the URL of this page.", + always_show=True, + order=6, + ) path_prefix: str = Field( default="", description="By providing a path-like prefix (e.g. myFolder/thisTable/) under which all the relevant files sit, " diff --git a/airbyte-integrations/connectors/source-s3/source_s3/v4/config.py b/airbyte-integrations/connectors/source-s3/source_s3/v4/config.py index 6275c0954388e..55c3b5708f592 100644 --- a/airbyte-integrations/connectors/source-s3/source_s3/v4/config.py +++ b/airbyte-integrations/connectors/source-s3/source_s3/v4/config.py @@ -1,7 +1,6 @@ # # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # - from typing import Any, Dict, Optional import dpath.util @@ -31,6 +30,14 @@ def documentation_url(cls) -> AnyUrl: order=2, ) + role_arn: Optional[str] = Field( + title=f"AWS Role ARN", + default=None, + description="Specifies the Amazon Resource Name (ARN) of an IAM role that you want to use to perform operations " + f"requested using this profile. Set the External ID to the Airbyte workspace ID, which can be found in the URL of this page.", + order=6, + ) + aws_secret_access_key: Optional[str] = Field( title="AWS Secret Access Key", default=None, diff --git a/airbyte-integrations/connectors/source-s3/source_s3/v4/stream_reader.py b/airbyte-integrations/connectors/source-s3/source_s3/v4/stream_reader.py index d8bfbd5b16bca..0457dba4ee368 100644 --- a/airbyte-integrations/connectors/source-s3/source_s3/v4/stream_reader.py +++ b/airbyte-integrations/connectors/source-s3/source_s3/v4/stream_reader.py @@ -5,6 +5,7 @@ import logging from datetime import datetime from io import IOBase +from os import getenv from typing import Iterable, List, Optional, Set import boto3.session @@ -16,10 +17,14 @@ from airbyte_cdk.sources.file_based.remote_file import RemoteFile from botocore.client import BaseClient from botocore.client import Config as ClientConfig +from botocore.credentials import RefreshableCredentials from botocore.exceptions import ClientError +from botocore.session import get_session from source_s3.v4.config import Config from source_s3.v4.zip_reader import DecompressedStream, RemoteFileInsideArchive, ZipContentReader, ZipFileHandler +AWS_EXTERNAL_ID = getenv("AWS_ASSUME_ROLE_EXTERNAL_ID") + class SourceS3StreamReader(AbstractFileBasedStreamReader): def __init__(self): @@ -52,14 +57,66 @@ def s3_client(self) -> BaseClient: raise ValueError("Source config is missing; cannot create the S3 client.") if self._s3_client is None: client_kv_args = _get_s3_compatible_client_args(self.config) if self.config.endpoint else {} - self._s3_client = boto3.client( - "s3", - aws_access_key_id=self.config.aws_access_key_id, - aws_secret_access_key=self.config.aws_secret_access_key, - **client_kv_args, - ) + + if self.config.role_arn: + self._s3_client = self._get_iam_s3_client(client_kv_args) + else: + self._s3_client = boto3.client( + "s3", + aws_access_key_id=self.config.aws_access_key_id, + aws_secret_access_key=self.config.aws_secret_access_key, + **client_kv_args, + ) + return self._s3_client + def _get_iam_s3_client(self, client_kv_args: dict) -> BaseClient: + """ + Creates an S3 client using AWS Security Token Service (STS) with assumed role credentials. This method handles + the authentication process by assuming an IAM role, optionally using an external ID for enhanced security. + The obtained credentials are set to auto-refresh upon expiration, ensuring uninterrupted access to the S3 service. + + :param client_kv_args: A dictionary of key-value pairs for the boto3 S3 client constructor. + :return: An instance of a boto3 S3 client with the assumed role credentials. + + The method assumes a role specified in the `self.config.role_arn` and creates a session with the S3 service. + If `AWS_ASSUME_ROLE_EXTERNAL_ID` environment variable is set, it will be used during the role assumption for additional security. + """ + + def refresh(): + client = boto3.client("sts") + if AWS_EXTERNAL_ID: + role = client.assume_role( + RoleArn=self.config.role_arn, + RoleSessionName="airbyte-source-s3", + ExternalId=AWS_EXTERNAL_ID, + ) + else: + role = client.assume_role( + RoleArn=self.config.role_arn, + RoleSessionName="airbyte-source-s3", + ) + + creds = role.get("Credentials", {}) + return { + "access_key": creds["AccessKeyId"], + "secret_key": creds["SecretAccessKey"], + "token": creds["SessionToken"], + "expiry_time": creds["Expiration"].isoformat(), + } + + session_credentials = RefreshableCredentials.create_from_metadata( + metadata=refresh(), + refresh_using=refresh, + method="sts-assume-role", + ) + + session = get_session() + session._credentials = session_credentials + autorefresh_session = boto3.Session(botocore_session=session) + + return autorefresh_session.client("s3", **client_kv_args) + def get_matching_files(self, globs: List[str], prefix: Optional[str], logger: logging.Logger) -> Iterable[RemoteFile]: """ Get all files matching the specified glob patterns. diff --git a/airbyte-integrations/connectors/source-s3/unit_tests/v4/test_stream_reader.py b/airbyte-integrations/connectors/source-s3/unit_tests/v4/test_stream_reader.py index 05d7f7873be17..b1bede862d222 100644 --- a/airbyte-integrations/connectors/source-s3/unit_tests/v4/test_stream_reader.py +++ b/airbyte-integrations/connectors/source-s3/unit_tests/v4/test_stream_reader.py @@ -16,6 +16,7 @@ from airbyte_cdk.sources.file_based.file_based_stream_reader import FileReadMode from airbyte_cdk.sources.file_based.remote_file import RemoteFile from botocore.stub import Stubber +from moto import mock_sts from pydantic import AnyUrl from source_s3.v4.config import Config from source_s3.v4.stream_reader import SourceS3StreamReader @@ -238,3 +239,33 @@ def set_stub(reader: SourceS3StreamReader, contents: List[Dict[str, Any]], multi ) s3_stub.activate() return s3_stub + + +@mock_sts +@patch("source_s3.v4.stream_reader.boto3.client") +def test_get_iam_s3_client(boto3_client_mock): + # Mock the STS client assume_role method + boto3_client_mock.return_value.assume_role.return_value = { + "Credentials": { + "AccessKeyId": "assumed_access_key_id", + "SecretAccessKey": "assumed_secret_access_key", + "SessionToken": "assumed_session_token", + "Expiration": datetime.now(), + } + } + + # Instantiate your stream reader and set the config + reader = SourceS3StreamReader() + reader.config = Config( + bucket="test", + role_arn="arn:aws:iam::123456789012:role/my-role", + streams=[], + endpoint=None, + ) + + # Call _get_iam_s3_client + with Stubber(reader.s3_client): + s3_client = reader._get_iam_s3_client({}) + + # Assertions to validate the s3 client + assert s3_client is not None diff --git a/docs/integrations/sources/s3.md b/docs/integrations/sources/s3.md index 36e5eb5fef31c..2cea82eff718b 100644 --- a/docs/integrations/sources/s3.md +++ b/docs/integrations/sources/s3.md @@ -15,7 +15,9 @@ Please note that using cloud storage may incur egress costs. Egress refers to da ### Step 1: Set up Amazon S3 -**If you are syncing from a private bucket**, you will need to provide both an `AWS Access Key ID` and `AWS Secret Access Key` to authenticate the connection. The IAM user associated with the credentials must be granted `read` and `list` permissions for the bucket and its objects. If you are unfamiliar with configuring AWS permissions, you can follow these steps to obtain the necessary permissions and credentials: +**If you are syncing from a private bucket**, you need to authenticate the connection. This can be done either by using an `IAM User` (with `AWS Access Key ID` and `Secret Access Key`) or an `IAM Role` (with `Role ARN`). Begin by creating a policy with the necessary permissions: + +#### Create a Policy 1. Log in to your Amazon AWS account and open the [IAM console](https://console.aws.amazon.com/iam/home#home). 2. In the IAM dashboard, select **Policies**, then click **Create Policy**. @@ -45,10 +47,69 @@ At this time, object-level permissions alone are not sufficient to successfully ::: 4. Give your policy a descriptive name, then click **Create policy**. -5. In the IAM dashboard, click **Users**. Select an existing IAM user or create a new one by clicking **Add users**. -6. If you are using an _existing_ IAM user, click the **Add permissions** dropdown menu and select **Add permissions**. If you are creating a _new_ user, you will be taken to the Permissions screen after selecting a name. -7. Select **Attach policies directly**, then find and check the box for your new policy. Click **Next**, then **Add permissions**. -8. After successfully creating your user, select the **Security credentials** tab and click **Create access key**. You will be prompted to select a use case and add optional tags to your access key. Click **Create access key** to generate the keys. + +#### Option 1: Using an IAM Role (Most secure) + + +:::note +This authentication method is currently in the testing phase. To enable it for your workspace, please contact our Support Team. +::: + + +1. In the IAM dashboard, click **Roles**, then **Create role**. +2. Choose the appropriate trust entity and attach the policy you created. +3. Set up a trust relationship for the role. For example for **AWS account** trusted entity use default AWS account on your instance (it will be used to assume role). To use **External ID** set it to environment variables as `export AWS_ASSUME_ROLE_EXTERNAL_ID="{your-external-id}"`. Edit the trust relationship policy to reflect this: +``` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::{your-aws-account-id}:user/{your-username}" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "{your-external-id}" + } + } + } + ] +} +``` + + +2. Choose the **AWS account** trusted entity type. +3. Set up a trust relationship for the role. This allows the Airbyte instance's AWS account to assume this role. You will also need to specify an external ID, which is a secret key that the trusting service (Airbyte) and the trusted role (the role you're creating) both know. This ID is used to prevent the "confused deputy" problem. The External ID should be your Airbyte workspace ID, which can be found in the URL of your workspace page. Edit the trust relationship policy to include the external ID: +``` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::094410056844:user/delegated_access_user" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "{your-airbyte-workspace-id}" + } + } + } + ] +} +``` + +4. Complete the role creation and note the Role ARN. + +#### Option 2: Using an IAM User + +1. In the IAM dashboard, click **Users**. Select an existing IAM user or create a new one by clicking **Add users**. +2. If you are using an _existing_ IAM user, click the **Add permissions** dropdown menu and select **Add permissions**. If you are creating a _new_ user, you will be taken to the Permissions screen after selecting a name. +3. Select **Attach policies directly**, then find and check the box for your new policy. Click **Next**, then **Add permissions**. +4. After successfully creating your user, select the **Security credentials** tab and click **Create access key**. You will be prompted to select a use case and add optional tags to your access key. Click **Create access key** to generate the keys. :::caution Your `Secret Access Key` will only be visible once upon creation. Be sure to copy and store it securely for future use. @@ -69,7 +130,11 @@ For more information on managing your access keys, please refer to the 3. Give a **Name** to the stream 4. (Optional) - If you want to enforce a specific schema, you can enter a **Input schema**. By default, this value is set to `{}` and will automatically infer the schema from the file\(s\) you are replicating. For details on providing a custom schema, refer to the [User Schema section](#user-schema). 5. Optionally, enter the **Globs** which dictates which files to be synced. This is a regular expression that allows Airbyte to pattern match the specific files to replicate. If you are replicating all the files within your bucket, use `**` as the pattern. For more precise pattern matching options, refer to the [Path Patterns section](#path-patterns) below. -6. **If you are syncing from a private bucket**, you must fill the **AWS Access Key ID** and **AWS Secret Access Key** fields with the appropriate credentials to authenticate the connection. All other fields are optional and can be left empty. Refer to the [S3 Provider Settings section](#s3-provider-settings) below for more information on each field. +6. **To authenticate your private bucket**: + - If using an IAM role, enter the **AWS Role ARN**. + - If using IAM user credentials, fill the **AWS Access Key ID** and **AWS Secret Access Key** fields with the appropriate credentials. + +All other fields are optional and can be left empty. Refer to the [S3 Provider Settings section](#s3-provider-settings) below for more information on each field. ## Supported sync modes @@ -256,8 +321,9 @@ To perform the text extraction from PDF and Docx files, the connector uses the [ | Version | Date | Pull Request | Subject | |:--------|:-----------|:----------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------| -| 4.3.1 | 2024-01-04 | [33937](https://github.com/airbytehq/airbyte/pull/33937) | Prepare for airbyte-lib | -| 4.3.0 | 2023-12-14 | [33411](https://github.com/airbytehq/airbyte/pull/33411) | Bump CDK version to auto-set primary key for document file streams and support raw txt files | +| 4.4.0 | 2023-01-12 | [33818](https://github.com/airbytehq/airbyte/pull/33818) | Add IAM Role Authentication | +| 4.3.1 | 2024-01-04 | [33937](https://github.com/airbytehq/airbyte/pull/33937) | Prepare for airbyte-lib | +| 4.3.0 | 2023-12-14 | [33411](https://github.com/airbytehq/airbyte/pull/33411) | Bump CDK version to auto-set primary key for document file streams and support raw txt files | | 4.2.4 | 2023-12-06 | [33187](https://github.com/airbytehq/airbyte/pull/33187) | Bump CDK version to hide source-defined primary key | | 4.2.3 | 2023-11-16 | [32608](https://github.com/airbytehq/airbyte/pull/32608) | Improve document file type parser | | 4.2.2 | 2023-11-20 | [32677](https://github.com/airbytehq/airbyte/pull/32677) | Only read files with ".zip" extension as zipped files | From 006338257c96df99b3138b0fa85640637f314577 Mon Sep 17 00:00:00 2001 From: Edward Gao Date: Wed, 17 Jan 2024 10:32:21 -0800 Subject: [PATCH 131/574] Destination postgres: DV2 beta implementation (#34177) --- airbyte-cdk/java/airbyte-cdk/README.md | 1 + .../cdk/db/factory/DataSourceFactory.java | 76 ++--- .../io/airbyte/cdk/db/jdbc/JdbcDatabase.java | 41 +-- .../src/main/resources/version.properties | 2 +- .../jdbc/AbstractJdbcDestination.java | 19 +- .../destination/jdbc/JdbcSqlOperations.java | 26 +- .../JdbcDestinationHandler.java | 34 ++- .../typing_deduping/JdbcSqlGenerator.java | 38 ++- .../JdbcSqlGeneratorIntegrationTest.java | 4 +- .../JdbcTypingDedupingTest.java | 113 +++++++ .../typing_deduping/DefaultTyperDeduper.java | 1 + .../BaseSqlGeneratorIntegrationTest.java | 21 ++ .../alltypes_unsafe_inputrecords.jsonl | 3 + .../build.gradle | 2 +- .../gradle.properties | 4 +- .../metadata.yaml | 2 +- .../destination-postgres/build.gradle | 2 +- .../destination-postgres/gradle.properties | 4 +- .../destination-postgres/metadata.yaml | 2 +- .../postgres/PostgresDestination.java | 28 +- .../postgres/PostgresSqlOperations.java | 8 +- .../typing_deduping/PostgresSqlGenerator.java | 279 ++++++++++++++++++ ...PostgresRawOverrideTypingDedupingTest.java | 22 ++ .../PostgresSqlGeneratorIntegrationTest.java | 153 ++++++++++ .../PostgresTypingDedupingTest.java | 75 +++++ ...orchange_expectedrecords_dedup_final.jsonl | 3 + ...rsorchange_expectedrecords_dedup_raw.jsonl | 4 + .../sync1_expectedrecords_dedup_final.jsonl | 4 + .../sync1_expectedrecords_dedup_final2.jsonl | 1 + ...sync1_expectedrecords_nondedup_final.jsonl | 5 + .../dat/sync1_expectedrecords_raw.jsonl | 5 + .../dat/sync1_expectedrecords_raw2.jsonl | 1 + ...ectedrecords_incremental_dedup_final.jsonl | 3 + ...xpectedrecords_incremental_dedup_raw.jsonl | 7 + ...ctedrecords_fullrefresh_append_final.jsonl | 8 + ...drecords_fullrefresh_overwrite_final.jsonl | 3 + ...tedrecords_fullrefresh_overwrite_raw.jsonl | 3 + ...ectedrecords_incremental_dedup_final.jsonl | 3 + ...ctedrecords_incremental_dedup_final2.jsonl | 1 + .../dat/sync2_expectedrecords_raw.jsonl | 9 + .../dat/sync2_expectedrecords_raw2.jsonl | 2 + .../alltypes_expectedrecords_final.jsonl | 8 + .../alltypes_expectedrecords_raw.jsonl | 6 + ...crementaldedup_expectedrecords_final.jsonl | 2 + ...incrementaldedup_expectedrecords_raw.jsonl | 3 + ...ypes_in_string_expectedrecords_final.jsonl | 5 + ..._types_in_string_expectedrecords_raw.jsonl | 5 + .../nocolumns_expectedrecords_final.jsonl | 1 + .../nocolumns_expectedrecords_raw.jsonl | 1 + ...servedkeywords_expectedrecords_final.jsonl | 1 + ...mestampformats_expectedrecords_final.jsonl | 16 + ...irdcolumnnames_expectedrecords_final.jsonl | 9 + ...weirdcolumnnames_expectedrecords_raw.jsonl | 1 + docs/integrations/destinations/postgres.md | 3 +- 54 files changed, 972 insertions(+), 111 deletions(-) create mode 100644 airbyte-cdk/java/airbyte-cdk/db-destinations/src/testFixtures/java/io/airbyte/cdk/integrations/standardtest/destination/typing_deduping/JdbcTypingDedupingTest.java create mode 100644 airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/resources/sqlgenerator/alltypes_unsafe_inputrecords.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/main/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresSqlGenerator.java create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresRawOverrideTypingDedupingTest.java create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresSqlGeneratorIntegrationTest.java create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresTypingDedupingTest.java create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_cursorchange_expectedrecords_dedup_final.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_cursorchange_expectedrecords_dedup_raw.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_dedup_final.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_dedup_final2.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_nondedup_final.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_raw.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_raw2.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_cursorchange_expectedrecords_incremental_dedup_final.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_cursorchange_expectedrecords_incremental_dedup_raw.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_fullrefresh_append_final.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_fullrefresh_overwrite_final.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_fullrefresh_overwrite_raw.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_incremental_dedup_final.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_incremental_dedup_final2.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_raw.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_raw2.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/incrementaldedup_expectedrecords_final.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/incrementaldedup_expectedrecords_raw.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/json_types_in_string_expectedrecords_final.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/json_types_in_string_expectedrecords_raw.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/nocolumns_expectedrecords_final.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/nocolumns_expectedrecords_raw.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/reservedkeywords_expectedrecords_final.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/timestampformats_expectedrecords_final.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/weirdcolumnnames_expectedrecords_final.jsonl create mode 100644 airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/weirdcolumnnames_expectedrecords_raw.jsonl diff --git a/airbyte-cdk/java/airbyte-cdk/README.md b/airbyte-cdk/java/airbyte-cdk/README.md index d572437df71c0..47539a8cc70f1 100644 --- a/airbyte-cdk/java/airbyte-cdk/README.md +++ b/airbyte-cdk/java/airbyte-cdk/README.md @@ -166,6 +166,7 @@ MavenLocal debugging steps: | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0.13.0 | 2024-01-16 | [\#34177](https://github.com/airbytehq/airbyte/pull/34177) | Add `useExpensiveSafeCasting` param in JdbcSqlGenerator methods; add JdbcTypingDedupingTest fixture; other DV2-related changes | | 0.12.1 | 2024-01-11 | [\#34186](https://github.com/airbytehq/airbyte/pull/34186) | Add hook for additional destination specific checks to JDBC destination check method | | 0.12.0 | 2024-01-10 | [\#33875](https://github.com/airbytehq/airbyte/pull/33875) | Upgrade sshd-mina to 2.11.1 | | 0.11.5 | 2024-01-10 | [\#34119](https://github.com/airbytehq/airbyte/pull/34119) | Remove wal2json support for postgres+debezium. | diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/db/factory/DataSourceFactory.java b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/db/factory/DataSourceFactory.java index 99da3aa21fe16..a4324a30ebf71 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/db/factory/DataSourceFactory.java +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/db/factory/DataSourceFactory.java @@ -32,11 +32,7 @@ public static DataSource create(final String username, final String password, final String driverClassName, final String jdbcConnectionString) { - return new DataSourceBuilder() - .withDriverClassName(driverClassName) - .withJdbcUrl(jdbcConnectionString) - .withPassword(password) - .withUsername(username) + return new DataSourceBuilder(username, password, driverClassName, jdbcConnectionString) .build(); } @@ -56,12 +52,8 @@ public static DataSource create(final String username, final String jdbcConnectionString, final Map connectionProperties, final Duration connectionTimeout) { - return new DataSourceBuilder() + return new DataSourceBuilder(username, password, driverClassName, jdbcConnectionString) .withConnectionProperties(connectionProperties) - .withDriverClassName(driverClassName) - .withJdbcUrl(jdbcConnectionString) - .withPassword(password) - .withUsername(username) .withConnectionTimeout(connectionTimeout) .build(); } @@ -83,13 +75,7 @@ public static DataSource create(final String username, final int port, final String database, final String driverClassName) { - return new DataSourceBuilder() - .withDatabase(database) - .withDriverClassName(driverClassName) - .withHost(host) - .withPort(port) - .withPassword(password) - .withUsername(username) + return new DataSourceBuilder(username, password, driverClassName, host, port, database) .build(); } @@ -112,14 +98,8 @@ public static DataSource create(final String username, final String database, final String driverClassName, final Map connectionProperties) { - return new DataSourceBuilder() + return new DataSourceBuilder(username, password, driverClassName, host, port, database) .withConnectionProperties(connectionProperties) - .withDatabase(database) - .withDriverClassName(driverClassName) - .withHost(host) - .withPort(port) - .withPassword(password) - .withUsername(username) .build(); } @@ -139,13 +119,7 @@ public static DataSource createPostgres(final String username, final String host, final int port, final String database) { - return new DataSourceBuilder() - .withDatabase(database) - .withDriverClassName("org.postgresql.Driver") - .withHost(host) - .withPort(port) - .withPassword(password) - .withUsername(username) + return new DataSourceBuilder(username, password, "org.postgresql.Driver", host, port, database) .build(); } @@ -158,7 +132,7 @@ public static DataSource createPostgres(final String username, */ public static void close(final DataSource dataSource) throws Exception { if (dataSource != null) { - if (dataSource instanceof AutoCloseable closeable) { + if (dataSource instanceof final AutoCloseable closeable) { closeable.close(); } } @@ -167,7 +141,7 @@ public static void close(final DataSource dataSource) throws Exception { /** * Builder class used to configure and construct {@link DataSource} instances. */ - private static class DataSourceBuilder { + public static class DataSourceBuilder { private Map connectionProperties = Map.of(); private String database; @@ -180,8 +154,35 @@ private static class DataSourceBuilder { private String password; private int port = 5432; private String username; + private String connectionInitSql; - private DataSourceBuilder() {} + private DataSourceBuilder(final String username, + final String password, + final String driverClassName) { + this.username = username; + this.password = password; + this.driverClassName = driverClassName; + } + + public DataSourceBuilder(final String username, + final String password, + final String driverClassName, + final String jdbcUrl) { + this(username, password, driverClassName); + this.jdbcUrl = jdbcUrl; + } + + public DataSourceBuilder(final String username, + final String password, + final String driverClassName, + final String host, + final int port, + final String database) { + this(username, password, driverClassName); + this.host = host; + this.port = port; + this.database = database; + } public DataSourceBuilder withConnectionProperties(final Map connectionProperties) { if (connectionProperties != null) { @@ -248,6 +249,11 @@ public DataSourceBuilder withUsername(final String username) { return this; } + public DataSourceBuilder withConnectionInitSql(final String sql) { + this.connectionInitSql = sql; + return this; + } + public DataSource build() { final DatabaseDriver databaseDriver = DatabaseDriver.findByDriverClassName(driverClassName); @@ -272,6 +278,8 @@ public DataSource build() { */ config.setInitializationFailTimeout(Integer.MIN_VALUE); + config.setConnectionInitSql(connectionInitSql); + connectionProperties.forEach(config::addDataSourceProperty); return new HikariDataSource(config); diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/db/jdbc/JdbcDatabase.java b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/db/jdbc/JdbcDatabase.java index ff7db2e6a5ffa..8557aaecece67 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/db/jdbc/JdbcDatabase.java +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/java/io/airbyte/cdk/db/jdbc/JdbcDatabase.java @@ -174,15 +174,17 @@ public List queryJsons(final CheckedFunction stream = unsafeQuery(c -> { - PreparedStatement statement = c.prepareStatement(sql); - int i = 1; - for (String param : params) { - statement.setString(i, param); - ++i; - } - return statement; - }, rs -> rs.getInt(1))) { + try (final Stream stream = unsafeQuery( + c -> getPreparedStatement(sql, params, c), + rs -> rs.getInt(1))) { + return stream.findFirst().get(); + } + } + + public boolean queryBoolean(final String sql, final String... params) throws SQLException { + try (final Stream stream = unsafeQuery( + c -> getPreparedStatement(sql, params, c), + rs -> rs.getBoolean(1))) { return stream.findFirst().get(); } } @@ -216,15 +218,8 @@ public List queryJsons(final String sql, final String... params) throw } public ResultSetMetaData queryMetadata(final String sql, final String... params) throws SQLException { - try (final Stream q = unsafeQuery(c -> { - PreparedStatement statement = c.prepareStatement(sql); - int i = 1; - for (String param : params) { - statement.setString(i, param); - ++i; - } - return statement; - }, + try (final Stream q = unsafeQuery( + c -> getPreparedStatement(sql, params, c), ResultSet::getMetaData)) { return q.findFirst().orElse(null); } @@ -232,4 +227,14 @@ public ResultSetMetaData queryMetadata(final String sql, final String... params) public abstract DatabaseMetaData getMetaData() throws SQLException; + private static PreparedStatement getPreparedStatement(String sql, String[] params, Connection c) throws SQLException { + PreparedStatement statement = c.prepareStatement(sql); + int i = 1; + for (String param : params) { + statement.setString(i, param); + i++; + } + return statement; + } + } diff --git a/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties b/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties index db02062e29913..f6cee2374148c 100644 --- a/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties +++ b/airbyte-cdk/java/airbyte-cdk/core/src/main/resources/version.properties @@ -1 +1 @@ -version=0.12.1 +version=0.13.0 diff --git a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/AbstractJdbcDestination.java b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/AbstractJdbcDestination.java index ff93320b19bd2..d25b6ecb4296b 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/AbstractJdbcDestination.java +++ b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/AbstractJdbcDestination.java @@ -200,17 +200,26 @@ private static PartialAirbyteMessage getDummyRecord() { .withSerialized(dummyDataToInsert.toString()); } + /** + * Subclasses which need to modify the DataSource should override + * {@link #modifyDataSourceBuilder(DataSourceFactory.DataSourceBuilder)} rather than this method. + */ @VisibleForTesting public DataSource getDataSource(final JsonNode config) { final JsonNode jdbcConfig = toJdbcConfig(config); final Map connectionProperties = getConnectionProperties(config); - return DataSourceFactory.create( + final DataSourceFactory.DataSourceBuilder builder = new DataSourceFactory.DataSourceBuilder( jdbcConfig.get(JdbcUtils.USERNAME_KEY).asText(), jdbcConfig.has(JdbcUtils.PASSWORD_KEY) ? jdbcConfig.get(JdbcUtils.PASSWORD_KEY).asText() : null, driverClassName, - jdbcConfig.get(JdbcUtils.JDBC_URL_KEY).asText(), - connectionProperties, - getConnectionTimeout(connectionProperties)); + jdbcConfig.get(JdbcUtils.JDBC_URL_KEY).asText()) + .withConnectionProperties(connectionProperties) + .withConnectionTimeout(getConnectionTimeout(connectionProperties)); + return modifyDataSourceBuilder(builder).build(); + } + + protected DataSourceFactory.DataSourceBuilder modifyDataSourceBuilder(final DataSourceFactory.DataSourceBuilder builder) { + return builder; } @VisibleForTesting @@ -287,7 +296,7 @@ public SerializedAirbyteMessageConsumer getSerializedMessageConsumer(final JsonN final var migrator = new JdbcV1V2Migrator(namingResolver, database, databaseName); final NoopV2TableMigrator v2TableMigrator = new NoopV2TableMigrator(); final DestinationHandler destinationHandler = getDestinationHandler(databaseName, database); - boolean disableTypeDedupe = config.has(DISABLE_TYPE_DEDUPE) && config.get(DISABLE_TYPE_DEDUPE).asBoolean(false); + final boolean disableTypeDedupe = config.has(DISABLE_TYPE_DEDUPE) && config.get(DISABLE_TYPE_DEDUPE).asBoolean(false); final TyperDeduper typerDeduper; if (disableTypeDedupe) { typerDeduper = new NoOpTyperDeduperWithV1V2Migrations<>(sqlGenerator, destinationHandler, parsedCatalog, migrator, v2TableMigrator, diff --git a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/JdbcSqlOperations.java b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/JdbcSqlOperations.java index a7db620058fc8..7ce3a8a7a01cb 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/JdbcSqlOperations.java +++ b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/JdbcSqlOperations.java @@ -87,22 +87,26 @@ public String createTableQuery(final JdbcDatabase database, final String schemaN protected String createTableQueryV1(final String schemaName, final String tableName) { return String.format( - "CREATE TABLE IF NOT EXISTS %s.%s ( \n" - + "%s VARCHAR PRIMARY KEY,\n" - + "%s JSONB,\n" - + "%s TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP\n" - + ");\n", + """ + CREATE TABLE IF NOT EXISTS %s.%s ( + %s VARCHAR PRIMARY KEY, + %s JSONB, + %s TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + ); + """, schemaName, tableName, JavaBaseConstants.COLUMN_NAME_AB_ID, JavaBaseConstants.COLUMN_NAME_DATA, JavaBaseConstants.COLUMN_NAME_EMITTED_AT); } protected String createTableQueryV2(final String schemaName, final String tableName) { return String.format( - "CREATE TABLE IF NOT EXISTS %s.%s ( \n" - + "%s VARCHAR PRIMARY KEY,\n" - + "%s JSONB,\n" - + "%s TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP\n" - + "%s TIMESTAMP WITH TIME ZONE DEFAULT NULL\n" - + ");\n", + """ + CREATE TABLE IF NOT EXISTS %s.%s ( + %s VARCHAR PRIMARY KEY, + %s JSONB, + %s TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + %s TIMESTAMP WITH TIME ZONE DEFAULT NULL + ); + """, schemaName, tableName, JavaBaseConstants.COLUMN_NAME_AB_RAW_ID, JavaBaseConstants.COLUMN_NAME_DATA, JavaBaseConstants.COLUMN_NAME_AB_EXTRACTED_AT, JavaBaseConstants.COLUMN_NAME_AB_LOADED_AT); } diff --git a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/typing_deduping/JdbcDestinationHandler.java b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/typing_deduping/JdbcDestinationHandler.java index 46acc8b3d0255..3981c07c5ad23 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/typing_deduping/JdbcDestinationHandler.java +++ b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/typing_deduping/JdbcDestinationHandler.java @@ -4,6 +4,12 @@ package io.airbyte.cdk.integrations.destination.jdbc.typing_deduping; +import static org.jooq.impl.DSL.exists; +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.name; +import static org.jooq.impl.DSL.select; +import static org.jooq.impl.DSL.selectOne; + import io.airbyte.cdk.db.jdbc.JdbcDatabase; import io.airbyte.cdk.integrations.destination.jdbc.ColumnDefinition; import io.airbyte.cdk.integrations.destination.jdbc.CustomSqlType; @@ -26,6 +32,7 @@ import java.util.UUID; import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; +import org.jooq.conf.ParamType; import org.jooq.impl.DSL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,18 +58,13 @@ public Optional findExistingTable(final StreamId id) throws Exc @Override public boolean isFinalTableEmpty(final StreamId id) throws Exception { - final int rowCount = jdbcDatabase.queryInt( - """ - SELECT row_count - FROM information_schema.tables - WHERE table_catalog = ? - AND table_schema = ? - AND table_name = ? - """, - databaseName, - id.finalNamespace(), - id.finalName()); - return rowCount == 0; + return !jdbcDatabase.queryBoolean( + select( + field(exists( + selectOne() + .from(name(id.finalNamespace(), id.finalName())) + .limit(1)))) + .getSQL(ParamType.INLINED)); } @Override @@ -83,8 +85,8 @@ public InitialRawTableState getInitialRawTableState(final StreamId id) throws Ex // but it's also the only method in the JdbcDatabase interface to return non-string/int types try (final Stream timestampStream = jdbcDatabase.unsafeQuery( conn -> conn.prepareStatement( - DSL.select(DSL.field("MIN(_airbyte_extracted_at)").as("min_timestamp")) - .from(DSL.name(id.rawNamespace(), id.rawName())) + select(field("MIN(_airbyte_extracted_at)").as("min_timestamp")) + .from(name(id.rawNamespace(), id.rawName())) .where(DSL.condition("_airbyte_loaded_at IS NULL")) .getSQL()), record -> record.getTimestamp("min_timestamp"))) { @@ -102,8 +104,8 @@ record -> record.getTimestamp("min_timestamp"))) { // This second query just finds the newest raw record. try (final Stream timestampStream = jdbcDatabase.unsafeQuery( conn -> conn.prepareStatement( - DSL.select(DSL.field("MAX(_airbyte_extracted_at)").as("min_timestamp")) - .from(DSL.name(id.rawNamespace(), id.rawName())) + select(field("MAX(_airbyte_extracted_at)").as("min_timestamp")) + .from(name(id.rawNamespace(), id.rawName())) .getSQL()), record -> record.getTimestamp("min_timestamp"))) { // Filter for nonNull values in case the query returned NULL (i.e. no raw records at all). diff --git a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/typing_deduping/JdbcSqlGenerator.java b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/typing_deduping/JdbcSqlGenerator.java index a66f6ac5594e1..0feb56ca9a9ee 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/typing_deduping/JdbcSqlGenerator.java +++ b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/main/java/io/airbyte/cdk/integrations/destination/jdbc/typing_deduping/JdbcSqlGenerator.java @@ -44,10 +44,12 @@ import io.airbyte.protocol.models.v0.DestinationSyncMode; import java.sql.Timestamp; import java.time.Instant; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Stream; import org.jooq.CommonTableExpression; import org.jooq.Condition; import org.jooq.CreateSchemaFinalStep; @@ -259,11 +261,15 @@ public Sql createTable(final StreamConfig stream, final String suffix, final boo // TODO: Use Naming transformer to sanitize these strings with redshift restrictions. final String finalTableIdentifier = stream.id().finalName() + suffix.toLowerCase(); if (!force) { - return Sql.of(createTableSql(stream.id().finalNamespace(), finalTableIdentifier, stream.columns())); + return transactionally(Stream.concat( + Stream.of(createTableSql(stream.id().finalNamespace(), finalTableIdentifier, stream.columns())), + createIndexSql(stream, suffix).stream()).toList()); } - return transactionally( - dropTableIfExists(quotedName(stream.id().finalNamespace(), finalTableIdentifier)).getSQL(ParamType.INLINED), - createTableSql(stream.id().finalNamespace(), finalTableIdentifier, stream.columns())); + return transactionally(Stream.concat( + Stream.of( + dropTableIfExists(quotedName(stream.id().finalNamespace(), finalTableIdentifier)).getSQL(ParamType.INLINED), + createTableSql(stream.id().finalNamespace(), finalTableIdentifier, stream.columns())), + createIndexSql(stream, suffix).stream()).toList()); } @Override @@ -419,10 +425,18 @@ protected String createTableSql(final String namespace, final String tableName, final DSLContext dsl = getDslContext(); final CreateTableColumnStep createTableSql = dsl .createTable(quotedName(namespace, tableName)) - .columns(buildFinalTableFields(columns, getFinalTableMetaColumns(true)));; + .columns(buildFinalTableFields(columns, getFinalTableMetaColumns(true))); return createTableSql.getSQL(); } + /** + * Subclasses may override this method to add additional indexes after their CREATE TABLE statement. + * This is useful if the destination's CREATE TABLE statement does not accept an index definition. + */ + protected List createIndexSql(final StreamConfig stream, final String suffix) { + return Collections.emptyList(); + } + protected String beginTransaction() { return "BEGIN"; } @@ -471,22 +485,26 @@ private String checkpointRawTable(final String schemaName, final String tableNam .getSQL(ParamType.INLINED); } - protected Field castedField(final Field field, final AirbyteType type, final String alias) { + protected Field castedField( + final Field field, + final AirbyteType type, + final String alias, + final boolean useExpensiveSaferCasting) { if (type instanceof final AirbyteProtocolType airbyteProtocolType) { - return castedField(field, airbyteProtocolType).as(quotedName(alias)); - + return castedField(field, airbyteProtocolType, useExpensiveSaferCasting).as(quotedName(alias)); } + // Redshift SUPER can silently cast an array type to struct and vice versa. return switch (type.getTypeName()) { case Struct.TYPE, UnsupportedOneOf.TYPE -> cast(field, getStructType()).as(quotedName(alias)); case Array.TYPE -> cast(field, getArrayType()).as(quotedName(alias)); // No nested Unions supported so this will definitely not result in infinite recursion. - case Union.TYPE -> castedField(field, ((Union) type).chooseType(), alias); + case Union.TYPE -> castedField(field, ((Union) type).chooseType(), alias, useExpensiveSaferCasting); default -> throw new IllegalArgumentException("Unsupported AirbyteType: " + type); }; } - protected Field castedField(final Field field, final AirbyteProtocolType type) { + protected Field castedField(final Field field, final AirbyteProtocolType type, final boolean useExpensiveSaferCasting) { return cast(field, toDialectType(type)); } diff --git a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/testFixtures/java/io/airbyte/cdk/integrations/standardtest/destination/typing_deduping/JdbcSqlGeneratorIntegrationTest.java b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/testFixtures/java/io/airbyte/cdk/integrations/standardtest/destination/typing_deduping/JdbcSqlGeneratorIntegrationTest.java index 3f46cc2f28ea6..a71711cf17c42 100644 --- a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/testFixtures/java/io/airbyte/cdk/integrations/standardtest/destination/typing_deduping/JdbcSqlGeneratorIntegrationTest.java +++ b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/testFixtures/java/io/airbyte/cdk/integrations/standardtest/destination/typing_deduping/JdbcSqlGeneratorIntegrationTest.java @@ -12,6 +12,8 @@ import static io.airbyte.cdk.integrations.base.JavaBaseConstants.COLUMN_NAME_DATA; import static io.airbyte.cdk.integrations.base.JavaBaseConstants.COLUMN_NAME_EMITTED_AT; import static io.airbyte.cdk.integrations.base.JavaBaseConstants.LEGACY_RAW_TABLE_COLUMNS; +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.quotedName; import com.fasterxml.jackson.databind.JsonNode; import io.airbyte.cdk.db.jdbc.JdbcDatabase; @@ -68,7 +70,7 @@ private void insertRecords(final Name tableName, final List columnNames, throws SQLException { InsertValuesStepN insert = getDslContext().insertInto( DSL.table(tableName), - columnNames.stream().map(DSL::field).toList()); + columnNames.stream().map(columnName -> field(quotedName(columnName))).toList()); for (final JsonNode record : records) { insert = insert.values( columnNames.stream() diff --git a/airbyte-cdk/java/airbyte-cdk/db-destinations/src/testFixtures/java/io/airbyte/cdk/integrations/standardtest/destination/typing_deduping/JdbcTypingDedupingTest.java b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/testFixtures/java/io/airbyte/cdk/integrations/standardtest/destination/typing_deduping/JdbcTypingDedupingTest.java new file mode 100644 index 0000000000000..f77448d621701 --- /dev/null +++ b/airbyte-cdk/java/airbyte-cdk/db-destinations/src/testFixtures/java/io/airbyte/cdk/integrations/standardtest/destination/typing_deduping/JdbcTypingDedupingTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2023 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.cdk.integrations.standardtest.destination.typing_deduping; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.airbyte.cdk.db.JdbcCompatibleSourceOperations; +import io.airbyte.cdk.db.factory.DataSourceFactory; +import io.airbyte.cdk.db.jdbc.DefaultJdbcDatabase; +import io.airbyte.cdk.db.jdbc.JdbcDatabase; +import io.airbyte.cdk.db.jdbc.JdbcUtils; +import io.airbyte.cdk.integrations.base.JavaBaseConstants; +import io.airbyte.integrations.base.destination.typing_deduping.BaseTypingDedupingTest; +import io.airbyte.integrations.base.destination.typing_deduping.StreamId; +import java.util.List; +import javax.sql.DataSource; +import org.jooq.impl.DSL; + +/** + * This class is largely the same as + * {@link io.airbyte.integrations.destination.snowflake.typing_deduping.AbstractSnowflakeTypingDedupingTest}. + * But (a) it uses jooq to construct the sql statements, and (b) it doesn't need to upcase anything. + * At some point we might (?) want to do a refactor to combine them. + */ +public abstract class JdbcTypingDedupingTest extends BaseTypingDedupingTest { + + private JdbcDatabase database; + private DataSource dataSource; + + /** + * Get the config as declared in GSM (or directly from the testcontainer). This class will do + * further modification to the config to ensure test isolation.i + */ + protected abstract ObjectNode getBaseConfig(); + + protected abstract DataSource getDataSource(JsonNode config); + + /** + * Subclasses may need to return a custom source operations if the default one does not handle + * vendor-specific types correctly. For example, you most likely need to override this method to + * deserialize JSON columns to JsonNode. + */ + protected JdbcCompatibleSourceOperations getSourceOperations() { + return JdbcUtils.getDefaultSourceOperations(); + } + + /** + * Subclasses using a config with a nonstandard raw table schema should override this method. + */ + protected String getRawSchema() { + return JavaBaseConstants.DEFAULT_AIRBYTE_INTERNAL_NAMESPACE; + } + + /** + * Subclasses using a config where the default schema is not in the {@code schema} key should + * override this method and {@link #setDefaultSchema(JsonNode, String)}. + */ + protected String getDefaultSchema(final JsonNode config) { + return config.get("schema").asText(); + } + + /** + * Subclasses using a config where the default schema is not in the {@code schema} key should + * override this method and {@link #getDefaultSchema(JsonNode)}. + */ + protected void setDefaultSchema(final JsonNode config, final String schema) { + ((ObjectNode) config).put("schema", schema); + } + + @Override + protected JsonNode generateConfig() { + final JsonNode config = getBaseConfig(); + setDefaultSchema(config, "typing_deduping_default_schema" + getUniqueSuffix()); + dataSource = getDataSource(config); + database = new DefaultJdbcDatabase(dataSource, getSourceOperations()); + return config; + } + + @Override + protected List dumpRawTableRecords(String streamNamespace, final String streamName) throws Exception { + if (streamNamespace == null) { + streamNamespace = getDefaultSchema(getConfig()); + } + final String tableName = StreamId.concatenateRawTableName(streamNamespace, streamName); + final String schema = getRawSchema(); + return database.queryJsons(DSL.selectFrom(DSL.name(schema, tableName)).getSQL()); + } + + @Override + protected List dumpFinalTableRecords(String streamNamespace, final String streamName) throws Exception { + if (streamNamespace == null) { + streamNamespace = getDefaultSchema(getConfig()); + } + return database.queryJsons(DSL.selectFrom(DSL.name(streamNamespace, streamName)).getSQL()); + } + + @Override + protected void teardownStreamAndNamespace(String streamNamespace, final String streamName) throws Exception { + if (streamNamespace == null) { + streamNamespace = getDefaultSchema(getConfig()); + } + database.execute(DSL.dropTableIfExists(DSL.name(getRawSchema(), StreamId.concatenateRawTableName(streamNamespace, streamName))).getSQL()); + database.execute(DSL.dropSchemaIfExists(DSL.name(streamNamespace)).cascade().getSQL()); + } + + @Override + protected void globalTeardown() throws Exception { + DataSourceFactory.close(dataSource); + } + +} diff --git a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/main/java/io/airbyte/integrations/base/destination/typing_deduping/DefaultTyperDeduper.java b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/main/java/io/airbyte/integrations/base/destination/typing_deduping/DefaultTyperDeduper.java index 9fff9fd8e1166..d01f47060ba44 100644 --- a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/main/java/io/airbyte/integrations/base/destination/typing_deduping/DefaultTyperDeduper.java +++ b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/main/java/io/airbyte/integrations/base/destination/typing_deduping/DefaultTyperDeduper.java @@ -121,6 +121,7 @@ public void prepareTables() throws Exception { } overwriteStreamsWithTmpTable = ConcurrentHashMap.newKeySet(); LOGGER.info("Preparing tables"); + prepareSchemas(parsedCatalog); final Set>> prepareTablesTasks = new HashSet<>(); for (final StreamConfig stream : parsedCatalog.streams()) { diff --git a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/java/io/airbyte/integrations/base/destination/typing_deduping/BaseSqlGeneratorIntegrationTest.java b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/java/io/airbyte/integrations/base/destination/typing_deduping/BaseSqlGeneratorIntegrationTest.java index e094930853bfd..cfc7eae3fa8a4 100644 --- a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/java/io/airbyte/integrations/base/destination/typing_deduping/BaseSqlGeneratorIntegrationTest.java +++ b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/java/io/airbyte/integrations/base/destination/typing_deduping/BaseSqlGeneratorIntegrationTest.java @@ -194,6 +194,7 @@ protected Map getFinalMetadataColumnNames() { public void setup() throws Exception { generator = getSqlGenerator(); destinationHandler = getDestinationHandler(); + final ColumnId id1 = generator.buildColumnId("id1"); final ColumnId id2 = generator.buildColumnId("id2"); primaryKey = List.of(id1, id2); @@ -425,6 +426,26 @@ public void allTypes() throws Exception { assertFalse(destinationHandler.isFinalTableEmpty(streamId), "Final table should not be empty after T+D"); } + /** + * Run a basic test to verify that we don't throw an exception on basic data values. + */ + @Test + public void allTypesUnsafe() throws Exception { + createRawTable(streamId); + createFinalTable(incrementalDedupStream, ""); + insertRawTableRecords( + streamId, + BaseTypingDedupingTest.readRecords("sqlgenerator/alltypes_unsafe_inputrecords.jsonl")); + + assertTrue(destinationHandler.isFinalTableEmpty(streamId), "Final table should be empty before T+D"); + + // Instead of using the full T+D transaction, explicitly run with useSafeCasting=false. + final Sql unsafeSql = generator.updateTable(incrementalDedupStream, "", Optional.empty(), false); + destinationHandler.execute(unsafeSql); + + assertFalse(destinationHandler.isFinalTableEmpty(streamId), "Final table should not be empty after T+D"); + } + /** * Run through some plausible T+D scenarios to verify that we correctly identify the min raw * timestamp. diff --git a/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/resources/sqlgenerator/alltypes_unsafe_inputrecords.jsonl b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/resources/sqlgenerator/alltypes_unsafe_inputrecords.jsonl new file mode 100644 index 0000000000000..55a509408d14d --- /dev/null +++ b/airbyte-cdk/java/airbyte-cdk/typing-deduping/src/testFixtures/resources/sqlgenerator/alltypes_unsafe_inputrecords.jsonl @@ -0,0 +1,3 @@ +// this is a strict subset of the alltypes_inputrecords file. All these records have valid values, i.e. can be processed with unsafe casting. +{"_airbyte_raw_id": "14ba7c7f-e398-4e69-ac22-28d578400dbc", "_airbyte_extracted_at": "2023-01-01T00:00:00Z", "_airbyte_data": {"id1": 1, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": ["foo"], "struct": {"foo": "bar"}, "string": "foo", "number": 42.1, "integer": 42, "boolean": true, "timestamp_with_timezone": "2023-01-23T12:34:56Z", "timestamp_without_timezone": "2023-01-23T12:34:56", "time_with_timezone": "12:34:56Z", "time_without_timezone": "12:34:56", "date": "2023-01-23", "unknown": {}}} +{"_airbyte_raw_id": "53ce75a5-5bcc-47a3-b45c-96c2015cfe35", "_airbyte_extracted_at": "2023-01-01T00:00:00Z", "_airbyte_data": {"id1": 2, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": null, "struct": null, "string": null, "number": null, "integer": null, "boolean": null, "timestamp_with_timezone": null, "timestamp_without_timezone": null, "time_with_timezone": null, "time_without_timezone": null, "date": null, "unknown": null}} diff --git a/airbyte-integrations/connectors/destination-postgres-strict-encrypt/build.gradle b/airbyte-integrations/connectors/destination-postgres-strict-encrypt/build.gradle index 11cfb6f26b788..4f56294fe85e9 100644 --- a/airbyte-integrations/connectors/destination-postgres-strict-encrypt/build.gradle +++ b/airbyte-integrations/connectors/destination-postgres-strict-encrypt/build.gradle @@ -4,7 +4,7 @@ plugins { } airbyteJavaConnector { - cdkVersionRequired = '0.12.0' + cdkVersionRequired = '0.13.0' features = [ 'db-sources', // required for tests 'db-destinations' diff --git a/airbyte-integrations/connectors/destination-postgres-strict-encrypt/gradle.properties b/airbyte-integrations/connectors/destination-postgres-strict-encrypt/gradle.properties index 2b147dcf7175a..4dbe8b8729dfe 100644 --- a/airbyte-integrations/connectors/destination-postgres-strict-encrypt/gradle.properties +++ b/airbyte-integrations/connectors/destination-postgres-strict-encrypt/gradle.properties @@ -1,3 +1 @@ -# currently limit the number of parallel threads until further investigation into the issues \ -# where integration tests run into race conditions -testExecutionConcurrency=1 +testExecutionConcurrency=-1 diff --git a/airbyte-integrations/connectors/destination-postgres-strict-encrypt/metadata.yaml b/airbyte-integrations/connectors/destination-postgres-strict-encrypt/metadata.yaml index 9cd1928961ec3..d4f379fdbe5cf 100644 --- a/airbyte-integrations/connectors/destination-postgres-strict-encrypt/metadata.yaml +++ b/airbyte-integrations/connectors/destination-postgres-strict-encrypt/metadata.yaml @@ -9,7 +9,7 @@ data: connectorSubtype: database connectorType: destination definitionId: 25c5221d-dce2-4163-ade9-739ef790f503 - dockerImageTag: 0.5.3 + dockerImageTag: 0.5.4 dockerRepository: airbyte/destination-postgres-strict-encrypt githubIssueLabel: destination-postgres icon: postgresql.svg diff --git a/airbyte-integrations/connectors/destination-postgres/build.gradle b/airbyte-integrations/connectors/destination-postgres/build.gradle index ed2e3d3ffcbdd..e5c75a6632c41 100644 --- a/airbyte-integrations/connectors/destination-postgres/build.gradle +++ b/airbyte-integrations/connectors/destination-postgres/build.gradle @@ -4,7 +4,7 @@ plugins { } airbyteJavaConnector { - cdkVersionRequired = '0.12.0' + cdkVersionRequired = '0.13.0' features = [ 'db-sources', // required for tests 'db-destinations', diff --git a/airbyte-integrations/connectors/destination-postgres/gradle.properties b/airbyte-integrations/connectors/destination-postgres/gradle.properties index 2b147dcf7175a..4dbe8b8729dfe 100644 --- a/airbyte-integrations/connectors/destination-postgres/gradle.properties +++ b/airbyte-integrations/connectors/destination-postgres/gradle.properties @@ -1,3 +1 @@ -# currently limit the number of parallel threads until further investigation into the issues \ -# where integration tests run into race conditions -testExecutionConcurrency=1 +testExecutionConcurrency=-1 diff --git a/airbyte-integrations/connectors/destination-postgres/metadata.yaml b/airbyte-integrations/connectors/destination-postgres/metadata.yaml index 601a751d1ceef..23020e463a0b5 100644 --- a/airbyte-integrations/connectors/destination-postgres/metadata.yaml +++ b/airbyte-integrations/connectors/destination-postgres/metadata.yaml @@ -5,7 +5,7 @@ data: connectorSubtype: database connectorType: destination definitionId: 25c5221d-dce2-4163-ade9-739ef790f503 - dockerImageTag: 0.5.3 + dockerImageTag: 0.5.4 dockerRepository: airbyte/destination-postgres documentationUrl: https://docs.airbyte.com/integrations/destinations/postgres githubIssueLabel: destination-postgres diff --git a/airbyte-integrations/connectors/destination-postgres/src/main/java/io/airbyte/integrations/destination/postgres/PostgresDestination.java b/airbyte-integrations/connectors/destination-postgres/src/main/java/io/airbyte/integrations/destination/postgres/PostgresDestination.java index 78f4709cfefe6..3a2a36ce446b9 100644 --- a/airbyte-integrations/connectors/destination-postgres/src/main/java/io/airbyte/integrations/destination/postgres/PostgresDestination.java +++ b/airbyte-integrations/connectors/destination-postgres/src/main/java/io/airbyte/integrations/destination/postgres/PostgresDestination.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableMap; +import io.airbyte.cdk.db.factory.DataSourceFactory; import io.airbyte.cdk.db.factory.DatabaseDriver; import io.airbyte.cdk.db.jdbc.JdbcUtils; import io.airbyte.cdk.integrations.base.Destination; @@ -20,6 +21,7 @@ import io.airbyte.cdk.integrations.destination.jdbc.AbstractJdbcDestination; import io.airbyte.cdk.integrations.destination.jdbc.typing_deduping.JdbcSqlGenerator; import io.airbyte.commons.json.Jsons; +import io.airbyte.integrations.destination.postgres.typing_deduping.PostgresSqlGenerator; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.HashMap; @@ -42,6 +44,28 @@ public PostgresDestination() { super(DRIVER_CLASS, new PostgresSQLNameTransformer(), new PostgresSqlOperations()); } + @Override + protected DataSourceFactory.DataSourceBuilder modifyDataSourceBuilder(final DataSourceFactory.DataSourceBuilder builder) { + // Anything in the pg_temp schema is only visible to the connection that created it. + // So this creates an airbyte_safe_cast function that only exists for the duration of + // a single connection. + // This avoids issues with creating the same function concurrently (e.g. if multiple syncs run + // at the same time). + // Function definition copied from https://dba.stackexchange.com/a/203986 + return builder.withConnectionInitSql(""" + CREATE FUNCTION pg_temp.airbyte_safe_cast(_in text, INOUT _out ANYELEMENT) + LANGUAGE plpgsql AS + $func$ + BEGIN + EXECUTE format('SELECT %L::%s', $1, pg_typeof(_out)) + INTO _out; + EXCEPTION WHEN others THEN + -- do nothing: _out already carries default + END + $func$; + """); + } + @Override protected Map getDefaultConnectionProperties(final JsonNode config) { final Map additionalParameters = new HashMap<>(); @@ -68,7 +92,7 @@ public JsonNode toJdbcConfig(final JsonNode config) { if (encodedDatabase != null) { try { encodedDatabase = URLEncoder.encode(encodedDatabase, "UTF-8"); - } catch (UnsupportedEncodingException e) { + } catch (final UnsupportedEncodingException e) { // Should never happen e.printStackTrace(); } @@ -96,7 +120,7 @@ public JsonNode toJdbcConfig(final JsonNode config) { @Override protected JdbcSqlGenerator getSqlGenerator() { - throw new UnsupportedOperationException("PostgresDestination#getSqlGenerator is not implemented"); + return new PostgresSqlGenerator(new PostgresSQLNameTransformer()); } public static void main(final String[] args) throws Exception { diff --git a/airbyte-integrations/connectors/destination-postgres/src/main/java/io/airbyte/integrations/destination/postgres/PostgresSqlOperations.java b/airbyte-integrations/connectors/destination-postgres/src/main/java/io/airbyte/integrations/destination/postgres/PostgresSqlOperations.java index 3cf90d4d0a0d4..43236bf65d1d1 100644 --- a/airbyte-integrations/connectors/destination-postgres/src/main/java/io/airbyte/integrations/destination/postgres/PostgresSqlOperations.java +++ b/airbyte-integrations/connectors/destination-postgres/src/main/java/io/airbyte/integrations/destination/postgres/PostgresSqlOperations.java @@ -25,9 +25,13 @@ public PostgresSqlOperations() { } @Override - protected void insertRecordsInternalV2(JdbcDatabase database, List records, String schemaName, String tableName) + protected void insertRecordsInternalV2(final JdbcDatabase database, + final List records, + final String schemaName, + final String tableName) throws Exception { - throw new UnsupportedOperationException("PostgresSqlOperations#insertRecordsInternalV2 is not implemented"); + // idk apparently this just works + insertRecordsInternal(database, records, schemaName, tableName); } @Override diff --git a/airbyte-integrations/connectors/destination-postgres/src/main/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresSqlGenerator.java b/airbyte-integrations/connectors/destination-postgres/src/main/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresSqlGenerator.java new file mode 100644 index 0000000000000..2d6469192ed16 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/main/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresSqlGenerator.java @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2023 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.integrations.destination.postgres.typing_deduping; + +import static io.airbyte.cdk.integrations.base.JavaBaseConstants.COLUMN_NAME_AB_EXTRACTED_AT; +import static io.airbyte.cdk.integrations.base.JavaBaseConstants.COLUMN_NAME_AB_LOADED_AT; +import static io.airbyte.cdk.integrations.base.JavaBaseConstants.COLUMN_NAME_AB_META; +import static io.airbyte.cdk.integrations.base.JavaBaseConstants.COLUMN_NAME_DATA; +import static java.util.Collections.emptyList; +import static org.jooq.impl.DSL.array; +import static org.jooq.impl.DSL.case_; +import static org.jooq.impl.DSL.cast; +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.function; +import static org.jooq.impl.DSL.name; +import static org.jooq.impl.DSL.quotedName; +import static org.jooq.impl.DSL.rowNumber; +import static org.jooq.impl.DSL.val; + +import com.google.common.collect.ImmutableMap; +import io.airbyte.cdk.integrations.base.JavaBaseConstants; +import io.airbyte.cdk.integrations.destination.NamingConventionTransformer; +import io.airbyte.cdk.integrations.destination.jdbc.TableDefinition; +import io.airbyte.cdk.integrations.destination.jdbc.typing_deduping.JdbcSqlGenerator; +import io.airbyte.integrations.base.destination.typing_deduping.AirbyteProtocolType; +import io.airbyte.integrations.base.destination.typing_deduping.AirbyteType; +import io.airbyte.integrations.base.destination.typing_deduping.Array; +import io.airbyte.integrations.base.destination.typing_deduping.ColumnId; +import io.airbyte.integrations.base.destination.typing_deduping.StreamConfig; +import io.airbyte.integrations.base.destination.typing_deduping.Struct; +import io.airbyte.protocol.models.v0.DestinationSyncMode; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.jooq.Condition; +import org.jooq.DataType; +import org.jooq.Field; +import org.jooq.SQLDialect; +import org.jooq.impl.DefaultDataType; +import org.jooq.impl.SQLDataType; + +public class PostgresSqlGenerator extends JdbcSqlGenerator { + + public static final DataType JSONB_TYPE = new DefaultDataType<>(null, Object.class, "jsonb"); + + private static final Map POSTGRES_TYPE_NAME_TO_JDBC_TYPE = ImmutableMap.of( + "numeric", "decimal", + "int8", "bigint", + "bool", "boolean", + "timestamptz", "timestamp with time zone", + "timetz", "time with time zone"); + + public PostgresSqlGenerator(final NamingConventionTransformer namingTransformer) { + super(namingTransformer); + } + + @Override + protected DataType getStructType() { + return JSONB_TYPE; + } + + @Override + protected DataType getArrayType() { + return JSONB_TYPE; + } + + @Override + protected DataType getWidestType() { + return JSONB_TYPE; + } + + @Override + protected SQLDialect getDialect() { + return SQLDialect.POSTGRES; + } + + @Override + protected List createIndexSql(final StreamConfig stream, final String suffix) { + if (stream.destinationSyncMode() == DestinationSyncMode.APPEND_DEDUP && !stream.primaryKey().isEmpty()) { + return List.of( + getDslContext().createIndex().on( + name(stream.id().finalNamespace(), stream.id().finalName() + suffix), + stream.primaryKey().stream() + .map(pk -> quotedName(pk.name())) + .toList()) + .getSQL()); + } else { + return emptyList(); + } + } + + @Override + protected List> extractRawDataFields(final LinkedHashMap columns, final boolean useExpensiveSaferCasting) { + return columns + .entrySet() + .stream() + .map(column -> castedField( + extractColumnAsJson(column.getKey()), + column.getValue(), + column.getKey().name(), + useExpensiveSaferCasting)) + .collect(Collectors.toList()); + } + + @Override + protected Field castedField( + final Field field, + final AirbyteType type, + final String alias, + final boolean useExpensiveSaferCasting) { + return castedField(field, type, useExpensiveSaferCasting).as(quotedName(alias)); + } + + protected Field castedField( + final Field field, + final AirbyteType type, + final boolean useExpensiveSaferCasting) { + if (type instanceof Struct) { + // If this field is a struct, verify that the raw data is an object. + return cast( + case_() + .when(field.isNull().or(jsonTypeof(field).ne("object")), val((Object) null)) + .else_(field), + JSONB_TYPE); + } else if (type instanceof Array) { + // Do the same for arrays. + return cast( + case_() + .when(field.isNull().or(jsonTypeof(field).ne("array")), val((Object) null)) + .else_(field), + JSONB_TYPE); + } else if (type == AirbyteProtocolType.UNKNOWN) { + return cast(field, JSONB_TYPE); + } else if (type == AirbyteProtocolType.STRING) { + // we need to render the jsonb to a normal string. For strings, this is the difference between + // "\"foo\"" and "foo". + // postgres provides the #>> operator, which takes a json path and returns that extraction as a + // string. + // '{}' is an empty json path (it's an empty array literal), so it just stringifies the json value. + return field("{0} #>> '{}'", String.class, field); + } else { + final DataType dialectType = toDialectType(type); + // jsonb can't directly cast to most types, so convert to text first. + // also convert jsonb null to proper sql null. + final Field extractAsText = case_() + .when(field.isNull().or(jsonTypeof(field).eq("null")), val((String) null)) + .else_(cast(field, SQLDataType.VARCHAR)); + if (useExpensiveSaferCasting) { + return function(name("pg_temp", "airbyte_safe_cast"), dialectType, extractAsText, cast(val((Object) null), dialectType)); + } else { + return cast(extractAsText, dialectType); + } + } + } + + // TODO this isn't actually used right now... can we refactor this out? + // (redshift is doing something interesting with this method, so leaving it for now) + @Override + protected Field castedField(final Field field, final AirbyteProtocolType type, final boolean useExpensiveSaferCasting) { + return cast(field, toDialectType(type)); + } + + @Override + protected Field buildAirbyteMetaColumn(final LinkedHashMap columns) { + final Field[] dataFieldErrors = columns + .entrySet() + .stream() + .map(column -> toCastingErrorCaseStmt(column.getKey(), column.getValue())) + .toArray(Field[]::new); + return function( + "JSONB_BUILD_OBJECT", + JSONB_TYPE, + val("errors"), + function("ARRAY_REMOVE", JSONB_TYPE, array(dataFieldErrors), val((String) null))).as(COLUMN_NAME_AB_META); + } + + private Field toCastingErrorCaseStmt(final ColumnId column, final AirbyteType type) { + final Field extract = extractColumnAsJson(column); + if (type instanceof Struct) { + // If this field is a struct, verify that the raw data is an object or null. + return case_() + .when( + extract.isNotNull() + .and(jsonTypeof(extract).notIn("object", "null")), + val("Problem with `" + column.originalName() + "`")) + .else_(val((String) null)); + } else if (type instanceof Array) { + // Do the same for arrays. + return case_() + .when( + extract.isNotNull() + .and(jsonTypeof(extract).notIn("array", "null")), + val("Problem with `" + column.originalName() + "`")) + .else_(val((String) null)); + } else if (type == AirbyteProtocolType.UNKNOWN || type == AirbyteProtocolType.STRING) { + // Unknown types require no casting, so there's never an error. + // Similarly, everything can cast to string without error. + return val((String) null); + } else { + // For other type: If the raw data is not NULL or 'null', but the casted data is NULL, + // then we have a typing error. + return case_() + .when( + extract.isNotNull() + .and(jsonTypeof(extract).ne("null")) + .and(castedField(extract, type, true).isNull()), + val("Problem with `" + column.originalName() + "`")) + .else_(val((String) null)); + } + } + + @Override + protected Condition cdcDeletedAtNotNullCondition() { + return field(name(COLUMN_NAME_AB_LOADED_AT)).isNotNull() + .and(jsonTypeof(extractColumnAsJson(cdcDeletedAtColumn)).ne("null")); + } + + @Override + protected Field getRowNumber(final List primaryKeys, final Optional cursor) { + // literally identical to redshift's getRowNumber implementation, changes here probably should + // be reflected there + final List> primaryKeyFields = + primaryKeys != null ? primaryKeys.stream().map(columnId -> field(quotedName(columnId.name()))).collect(Collectors.toList()) + : new ArrayList<>(); + final List> orderedFields = new ArrayList<>(); + // We can still use Jooq's field to get the quoted name with raw sql templating. + // jooq's .desc returns SortField instead of Field and NULLS LAST doesn't work with it + cursor.ifPresent(columnId -> orderedFields.add(field("{0} desc NULLS LAST", field(quotedName(columnId.name()))))); + orderedFields.add(field("{0} desc", quotedName(COLUMN_NAME_AB_EXTRACTED_AT))); + return rowNumber() + .over() + .partitionBy(primaryKeyFields) + .orderBy(orderedFields).as(ROW_NUMBER_COLUMN_NAME); + } + + @Override + public boolean existingSchemaMatchesStreamConfig(final StreamConfig stream, final TableDefinition existingTable) { + // Check that the columns match, with special handling for the metadata columns. + // This is mostly identical to the redshift implementation, but swaps super to jsonb + final LinkedHashMap intendedColumns = stream.columns().entrySet().stream() + .collect(LinkedHashMap::new, + (map, column) -> map.put(column.getKey().name(), toDialectType(column.getValue()).getTypeName()), + LinkedHashMap::putAll); + final LinkedHashMap actualColumns = existingTable.columns().entrySet().stream() + .filter(column -> JavaBaseConstants.V2_FINAL_TABLE_METADATA_COLUMNS.stream() + .noneMatch(airbyteColumnName -> airbyteColumnName.equals(column.getKey()))) + .collect(LinkedHashMap::new, + (map, column) -> map.put(column.getKey(), jdbcTypeNameFromPostgresTypeName(column.getValue().type())), + LinkedHashMap::putAll); + + final boolean sameColumns = actualColumns.equals(intendedColumns) + && "varchar".equals(existingTable.columns().get(JavaBaseConstants.COLUMN_NAME_AB_RAW_ID).type()) + && "timestamptz".equals(existingTable.columns().get(JavaBaseConstants.COLUMN_NAME_AB_EXTRACTED_AT).type()) + && "jsonb".equals(existingTable.columns().get(JavaBaseConstants.COLUMN_NAME_AB_META).type()); + + return sameColumns; + } + + /** + * Extract a raw field, leaving it as jsonb + */ + private Field extractColumnAsJson(final ColumnId column) { + return field("{0} -> {1}", name(COLUMN_NAME_DATA), val(column.originalName())); + } + + private Field jsonTypeof(final Field field) { + return function("JSONB_TYPEOF", SQLDataType.VARCHAR, field); + } + + private static String jdbcTypeNameFromPostgresTypeName(final String redshiftType) { + return POSTGRES_TYPE_NAME_TO_JDBC_TYPE.getOrDefault(redshiftType, redshiftType); + } + +} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresRawOverrideTypingDedupingTest.java b/airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresRawOverrideTypingDedupingTest.java new file mode 100644 index 0000000000000..f31c3325d2267 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresRawOverrideTypingDedupingTest.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.integrations.destination.postgres.typing_deduping; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +public class PostgresRawOverrideTypingDedupingTest extends PostgresTypingDedupingTest { + + @Override + protected ObjectNode getBaseConfig() { + return super.getBaseConfig() + .put("raw_data_schema", "overridden_raw_dataset"); + } + + @Override + protected String getRawSchema() { + return "overridden_raw_dataset"; + } + +} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresSqlGeneratorIntegrationTest.java b/airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresSqlGeneratorIntegrationTest.java new file mode 100644 index 0000000000000..ee80c3e12ab58 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresSqlGeneratorIntegrationTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2023 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.integrations.destination.postgres.typing_deduping; + +import static io.airbyte.integrations.destination.postgres.typing_deduping.PostgresSqlGenerator.JSONB_TYPE; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.airbyte.cdk.db.jdbc.DefaultJdbcDatabase; +import io.airbyte.cdk.db.jdbc.JdbcDatabase; +import io.airbyte.cdk.db.jdbc.JdbcSourceOperations; +import io.airbyte.cdk.db.jdbc.JdbcUtils; +import io.airbyte.cdk.integrations.destination.jdbc.TableDefinition; +import io.airbyte.cdk.integrations.destination.jdbc.typing_deduping.JdbcDestinationHandler; +import io.airbyte.cdk.integrations.destination.jdbc.typing_deduping.JdbcSqlGenerator; +import io.airbyte.cdk.integrations.standardtest.destination.typing_deduping.JdbcSqlGeneratorIntegrationTest; +import io.airbyte.commons.json.Jsons; +import io.airbyte.integrations.base.destination.typing_deduping.DestinationHandler; +import io.airbyte.integrations.base.destination.typing_deduping.Sql; +import io.airbyte.integrations.destination.postgres.PostgresDestination; +import io.airbyte.integrations.destination.postgres.PostgresSQLNameTransformer; +import io.airbyte.integrations.destination.postgres.PostgresTestDatabase; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; +import javax.sql.DataSource; +import org.jooq.DataType; +import org.jooq.Field; +import org.jooq.SQLDialect; +import org.jooq.impl.DSL; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class PostgresSqlGeneratorIntegrationTest extends JdbcSqlGeneratorIntegrationTest { + + private static PostgresTestDatabase testContainer; + private static String databaseName; + private static JdbcDatabase database; + + /** + * See + * {@link io.airbyte.integrations.destination.redshift.typing_deduping.RedshiftSqlGeneratorIntegrationTest.RedshiftSourceOperations}. + * Copied here to avoid weird dependencies. + */ + public static class PostgresSourceOperations extends JdbcSourceOperations { + + @Override + public void copyToJsonField(final ResultSet resultSet, final int colIndex, final ObjectNode json) throws SQLException { + final String columnName = resultSet.getMetaData().getColumnName(colIndex); + final String columnTypeName = resultSet.getMetaData().getColumnTypeName(colIndex).toLowerCase(); + + switch (columnTypeName) { + // JSONB has no equivalent in JDBCType + case "jsonb" -> json.set(columnName, Jsons.deserializeExact(resultSet.getString(colIndex))); + // For some reason, the driver maps these to their timezoneless equivalents (TIME and TIMESTAMP) + case "timetz" -> putTimeWithTimezone(json, columnName, resultSet, colIndex); + case "timestamptz" -> putTimestampWithTimezone(json, columnName, resultSet, colIndex); + default -> super.copyToJsonField(resultSet, colIndex, json); + } + } + + } + + @BeforeAll + public static void setupPostgres() { + testContainer = PostgresTestDatabase.in(PostgresTestDatabase.BaseImage.POSTGRES_13); + final JsonNode config = testContainer.configBuilder() + .with("schema", "public") + .withDatabase() + .withHostAndPort() + .withCredentials() + .withoutSsl() + .build(); + + databaseName = config.get(JdbcUtils.DATABASE_KEY).asText(); + final PostgresDestination postgresDestination = new PostgresDestination(); + final DataSource dataSource = postgresDestination.getDataSource(config); + database = new DefaultJdbcDatabase(dataSource, new PostgresSourceOperations()); + } + + @AfterAll + public static void teardownPostgres() { + testContainer.close(); + } + + @Override + protected JdbcDatabase getDatabase() { + return database; + } + + @Override + protected DataType getStructType() { + return JSONB_TYPE; + } + + @Override + protected JdbcSqlGenerator getSqlGenerator() { + return new PostgresSqlGenerator(new PostgresSQLNameTransformer()); + } + + @Override + protected DestinationHandler getDestinationHandler() { + return new JdbcDestinationHandler(databaseName, database); + } + + @Override + protected SQLDialect getSqlDialect() { + return SQLDialect.POSTGRES; + } + + @Override + protected Field toJsonValue(final String valueAsString) { + return DSL.cast(DSL.val(valueAsString), JSONB_TYPE); + } + + @Test + @Override + public void testCreateTableIncremental() throws Exception { + final Sql sql = generator.createTable(incrementalDedupStream, "", false); + destinationHandler.execute(sql); + + final Optional existingTable = destinationHandler.findExistingTable(incrementalDedupStream.id()); + + assertTrue(existingTable.isPresent()); + assertAll( + () -> assertEquals("varchar", existingTable.get().columns().get("_airbyte_raw_id").type()), + () -> assertEquals("timestamptz", existingTable.get().columns().get("_airbyte_extracted_at").type()), + () -> assertEquals("jsonb", existingTable.get().columns().get("_airbyte_meta").type()), + () -> assertEquals("int8", existingTable.get().columns().get("id1").type()), + () -> assertEquals("int8", existingTable.get().columns().get("id2").type()), + () -> assertEquals("timestamptz", existingTable.get().columns().get("updated_at").type()), + () -> assertEquals("jsonb", existingTable.get().columns().get("struct").type()), + () -> assertEquals("jsonb", existingTable.get().columns().get("array").type()), + () -> assertEquals("varchar", existingTable.get().columns().get("string").type()), + () -> assertEquals("numeric", existingTable.get().columns().get("number").type()), + () -> assertEquals("int8", existingTable.get().columns().get("integer").type()), + () -> assertEquals("bool", existingTable.get().columns().get("boolean").type()), + () -> assertEquals("timestamptz", existingTable.get().columns().get("timestamp_with_timezone").type()), + () -> assertEquals("timestamp", existingTable.get().columns().get("timestamp_without_timezone").type()), + () -> assertEquals("timetz", existingTable.get().columns().get("time_with_timezone").type()), + () -> assertEquals("time", existingTable.get().columns().get("time_without_timezone").type()), + () -> assertEquals("date", existingTable.get().columns().get("date").type()), + () -> assertEquals("jsonb", existingTable.get().columns().get("unknown").type())); + // TODO assert on table indexing, etc. + } + +} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresTypingDedupingTest.java b/airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresTypingDedupingTest.java new file mode 100644 index 0000000000000..dbcb13a67781f --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/java/io/airbyte/integrations/destination/postgres/typing_deduping/PostgresTypingDedupingTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.integrations.destination.postgres.typing_deduping; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.airbyte.cdk.db.JdbcCompatibleSourceOperations; +import io.airbyte.cdk.integrations.standardtest.destination.typing_deduping.JdbcTypingDedupingTest; +import io.airbyte.integrations.base.destination.typing_deduping.SqlGenerator; +import io.airbyte.integrations.destination.postgres.PostgresDestination; +import io.airbyte.integrations.destination.postgres.PostgresSQLNameTransformer; +import io.airbyte.integrations.destination.postgres.PostgresTestDatabase; +import javax.sql.DataSource; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; + +public class PostgresTypingDedupingTest extends JdbcTypingDedupingTest { + + protected static PostgresTestDatabase testContainer; + + @BeforeAll + public static void setupPostgres() { + testContainer = PostgresTestDatabase.in(PostgresTestDatabase.BaseImage.POSTGRES_13); + } + + @AfterAll + public static void teardownPostgres() { + testContainer.close(); + } + + @Override + protected ObjectNode getBaseConfig() { + final ObjectNode config = (ObjectNode) testContainer.configBuilder() + .with("schema", "public") + .withDatabase() + .withResolvedHostAndPort() + .withCredentials() + .withoutSsl() + .build(); + return config.put("use_1s1t_format", true); + } + + @Override + protected DataSource getDataSource(final JsonNode config) { + // Intentionally ignore the config and rebuild it. + // The config param has the resolved (i.e. in-docker) host/port. + // We need the unresolved host/port since the test wrapper code is running from the docker host + // rather than in a container. + return new PostgresDestination().getDataSource(testContainer.configBuilder() + .with("schema", "public") + .withDatabase() + .withHostAndPort() + .withCredentials() + .withoutSsl() + .build()); + } + + @Override + protected String getImageName() { + return "airbyte/destination-postgres:dev"; + } + + @Override + protected SqlGenerator getSqlGenerator() { + return new PostgresSqlGenerator(new PostgresSQLNameTransformer()); + } + + @Override + protected JdbcCompatibleSourceOperations getSourceOperations() { + return new PostgresSqlGeneratorIntegrationTest.PostgresSourceOperations(); + } + +} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_cursorchange_expectedrecords_dedup_final.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_cursorchange_expectedrecords_dedup_final.jsonl new file mode 100644 index 0000000000000..9f11b2293a95b --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_cursorchange_expectedrecords_dedup_final.jsonl @@ -0,0 +1,3 @@ +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":[]}, "id1": 1, "id2": 200, "old_cursor": 1, "name": "Alice", "address": {"city": "Los Angeles", "state": "CA"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":[]}, "id1": 1, "id2": 201, "old_cursor": 2, "name": "Bob", "address": {"city": "Boston", "state": "MA"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":["Problem with `age`", "Problem with `registration_date`"]}, "id1": 2, "id2": 200, "old_cursor": 3, "name": "Charlie"} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_cursorchange_expectedrecords_dedup_raw.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_cursorchange_expectedrecords_dedup_raw.jsonl new file mode 100644 index 0000000000000..7f75f0f804e25 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_cursorchange_expectedrecords_dedup_raw.jsonl @@ -0,0 +1,4 @@ +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 1, "id2": 200, "old_cursor": 0, "_ab_cdc_deleted_at": null, "name" :"Alice", "address": {"city": "San Francisco", "state": "CA"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 1, "id2": 200, "old_cursor": 1, "_ab_cdc_deleted_at": null, "name": "Alice", "address": {"city": "Los Angeles", "state": "CA"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 1, "id2": 201, "old_cursor": 2, "name": "Bob", "address": {"city": "Boston", "state": "MA"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 2, "id2": 200, "old_cursor": 3, "name": "Charlie", "age": "this is not an integer", "registration_date": "this is not a date"}} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_dedup_final.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_dedup_final.jsonl new file mode 100644 index 0000000000000..c805113dc6c20 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_dedup_final.jsonl @@ -0,0 +1,4 @@ +// Keep the Alice record with more recent updated_at +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":[]}, "id1": 1, "id2": 200, "updated_at": "2000-01-01T00:01:00.000000Z", "name": "Alice", "address": {"city": "Los Angeles", "state": "CA"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":[]}, "id1": 1, "id2": 201, "updated_at": "2000-01-01T00:02:00.000000Z", "name": "Bob", "address": {"city": "Boston", "state": "MA"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":["Problem with `age`", "Problem with `registration_date`"]}, "id1": 2, "id2": 200, "updated_at": "2000-01-01T00:03:00.000000Z", "name": "Charlie"} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_dedup_final2.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_dedup_final2.jsonl new file mode 100644 index 0000000000000..b2bf47df66c11 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_dedup_final2.jsonl @@ -0,0 +1 @@ +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":[]}, "id1": 1, "id2": 200, "updated_at": "2001-01-01T00:00:00.000000Z", "name": "Someone completely different"} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_nondedup_final.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_nondedup_final.jsonl new file mode 100644 index 0000000000000..8aa8521830614 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_nondedup_final.jsonl @@ -0,0 +1,5 @@ +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":[]}, "id1": 1, "id2": 200, "updated_at": "2000-01-01T00:00:00.000000Z", "name": "Alice", "address": {"city": "San Francisco", "state": "CA"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":[]}, "id1": 1, "id2": 200, "updated_at": "2000-01-01T00:01:00.000000Z", "name": "Alice", "address": {"city": "Los Angeles", "state": "CA"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":[]}, "id1": 1, "id2": 201, "updated_at": "2000-01-01T00:02:00.000000Z", "name": "Bob", "address": {"city": "Boston", "state": "MA"}} +// Invalid columns are nulled out (i.e. SQL null, not JSON null) +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":["Problem with `age`", "Problem with `registration_date`"]}, "id1": 2, "id2": 200, "updated_at": "2000-01-01T00:03:00.000000Z", "name": "Charlie"} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_raw.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_raw.jsonl new file mode 100644 index 0000000000000..80fac124d28dc --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_raw.jsonl @@ -0,0 +1,5 @@ +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 1, "id2": 200, "updated_at": "2000-01-01T00:00:00Z", "_ab_cdc_deleted_at": null, "name": "Alice", "address": {"city": "San Francisco", "state": "CA"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 1, "id2": 200, "updated_at": "2000-01-01T00:01:00Z", "_ab_cdc_deleted_at": null, "name": "Alice", "address": {"city": "Los Angeles", "state": "CA"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 1, "id2": 201, "updated_at": "2000-01-01T00:02:00Z", "name": "Bob", "address": {"city": "Boston", "state": "MA"}}} +// Invalid data is still allowed in the raw table. +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 2, "id2": 200, "updated_at": "2000-01-01T00:03:00Z", "name": "Charlie", "age": "this is not an integer", "registration_date": "this is not a date"}} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_raw2.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_raw2.jsonl new file mode 100644 index 0000000000000..b489accda1bb7 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync1_expectedrecords_raw2.jsonl @@ -0,0 +1 @@ +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 1, "id2": 200, "updated_at": "2001-01-01T00:00:00Z", "_ab_cdc_deleted_at": null, "name": "Someone completely different"}} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_cursorchange_expectedrecords_incremental_dedup_final.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_cursorchange_expectedrecords_incremental_dedup_final.jsonl new file mode 100644 index 0000000000000..c26d4a49aacd7 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_cursorchange_expectedrecords_incremental_dedup_final.jsonl @@ -0,0 +1,3 @@ +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_meta":{"errors":[]}, "id1": 1, "id2": 200, "updated_at": "2000-01-02T00:00:00.000000Z", "name": "Alice", "address": {"city": "Seattle", "state": "WA"}} +// Charlie wasn't reemitted with updated_at, so it still has a null cursor +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":["Problem with `age`", "Problem with `registration_date`"]}, "id1": 2, "id2": 200, "name": "Charlie"} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_cursorchange_expectedrecords_incremental_dedup_raw.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_cursorchange_expectedrecords_incremental_dedup_raw.jsonl new file mode 100644 index 0000000000000..03f28e155af53 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_cursorchange_expectedrecords_incremental_dedup_raw.jsonl @@ -0,0 +1,7 @@ +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 1, "id2": 200, "old_cursor": 0, "_ab_cdc_deleted_at": null, "name" :"Alice", "address": {"city": "San Francisco", "state": "CA"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 1, "id2": 200, "old_cursor": 1, "_ab_cdc_deleted_at": null, "name": "Alice", "address": {"city": "Los Angeles", "state": "CA"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 1, "id2": 201, "old_cursor": 2, "name": "Bob", "address": {"city": "Boston", "state": "MA"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 2, "id2": 200, "old_cursor": 3, "name": "Charlie", "age": "this is not an integer", "registration_date": "this is not a date"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_data": {"id1": 1, "id2": 200, "updated_at": "2000-01-02T00:00:00Z", "_ab_cdc_deleted_at": null, "name": "Alice", "address": {"city": "Seattle", "state": "WA"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_data": {"id1": 1, "id2": 201, "updated_at": "2000-01-02T00:00:00Z", "_ab_cdc_deleted_at": null, "name": "Bob", "address": {"city": "New York", "state": "NY"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_data": {"id1": 1, "id2": 201, "updated_at": "2000-01-02T00:01:00Z", "_ab_cdc_deleted_at": "1970-01-01T00:00:00Z"}} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_fullrefresh_append_final.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_fullrefresh_append_final.jsonl new file mode 100644 index 0000000000000..6e9258bab2552 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_fullrefresh_append_final.jsonl @@ -0,0 +1,8 @@ +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":[]}, "id1": 1, "id2": 200, "updated_at": "2000-01-01T00:00:00.000000Z", "name": "Alice", "address": {"city": "San Francisco", "state": "CA"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":[]}, "id1": 1, "id2": 200, "updated_at": "2000-01-01T00:01:00.000000Z", "name": "Alice", "address": {"city": "Los Angeles", "state": "CA"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":[]}, "id1": 1, "id2": 201, "updated_at": "2000-01-01T00:02:00.000000Z", "name": "Bob", "address": {"city": "Boston", "state": "MA"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":["Problem with `age`", "Problem with `registration_date`"]}, "id1": 2, "id2": 200, "updated_at": "2000-01-01T00:03:00.000000Z", "name": "Charlie"} + +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_meta":{"errors":[]}, "id1": 1, "id2": 200, "updated_at": "2000-01-02T00:00:00.000000Z", "name": "Alice", "address": {"city": "Seattle", "state": "WA"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_meta":{"errors":[]}, "id1": 1, "id2": 201, "updated_at": "2000-01-02T00:00:00.000000Z", "name": "Bob", "address": {"city": "New York", "state": "NY"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_meta":{"errors":[]}, "id1": 1, "id2": 201, "updated_at": "2000-01-02T00:01:00.000000Z", "_ab_cdc_deleted_at": "1970-01-01T00:00:00.000000Z"} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_fullrefresh_overwrite_final.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_fullrefresh_overwrite_final.jsonl new file mode 100644 index 0000000000000..9d1f1499469fc --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_fullrefresh_overwrite_final.jsonl @@ -0,0 +1,3 @@ +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_meta":{"errors":[]}, "id1": 1, "id2": 200, "updated_at": "2000-01-02T00:00:00.000000Z", "name": "Alice", "address": {"city": "Seattle", "state": "WA"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_meta":{"errors":[]}, "id1": 1, "id2": 201, "updated_at": "2000-01-02T00:00:00.000000Z", "name": "Bob", "address": {"city": "New York", "state": "NY"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_meta":{"errors":[]}, "id1": 1, "id2": 201, "updated_at": "2000-01-02T00:01:00.000000Z", "_ab_cdc_deleted_at": "1970-01-01T00:00:00.000000Z"} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_fullrefresh_overwrite_raw.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_fullrefresh_overwrite_raw.jsonl new file mode 100644 index 0000000000000..33bc3280be274 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_fullrefresh_overwrite_raw.jsonl @@ -0,0 +1,3 @@ +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_data": {"id1": 1, "id2": 200, "updated_at": "2000-01-02T00:00:00Z", "_ab_cdc_deleted_at": null, "name": "Alice", "address": {"city": "Seattle", "state": "WA"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_data": {"id1": 1, "id2": 201, "updated_at": "2000-01-02T00:00:00Z", "_ab_cdc_deleted_at": null, "name": "Bob", "address": {"city": "New York", "state": "NY"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_data": {"id1": 1, "id2": 201, "updated_at": "2000-01-02T00:01:00Z", "_ab_cdc_deleted_at": "1970-01-01T00:00:00Z"}} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_incremental_dedup_final.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_incremental_dedup_final.jsonl new file mode 100644 index 0000000000000..13c59b2f99121 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_incremental_dedup_final.jsonl @@ -0,0 +1,3 @@ +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_meta":{"errors":[]}, "id1": 1, "id2": 200, "updated_at": "2000-01-02T00:00:00.000000Z", "name": "Alice", "address": {"city": "Seattle", "state": "WA"}} +// Delete Bob, keep Charlie +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_meta": {"errors":["Problem with `age`", "Problem with `registration_date`"]}, "id1": 2, "id2": 200, "updated_at": "2000-01-01T00:03:00.000000Z", "name": "Charlie"} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_incremental_dedup_final2.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_incremental_dedup_final2.jsonl new file mode 100644 index 0000000000000..53c304c89d311 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_incremental_dedup_final2.jsonl @@ -0,0 +1 @@ +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_meta":{"errors":[]}, "id1": 1, "id2": 200, "updated_at": "2001-01-02T00:00:00.000000Z", "name": "Someone completely different v2"} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_raw.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_raw.jsonl new file mode 100644 index 0000000000000..32a7e57b1c147 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_raw.jsonl @@ -0,0 +1,9 @@ +// We keep the records from the first sync +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 1, "id2": 200, "updated_at": "2000-01-01T00:00:00Z", "_ab_cdc_deleted_at": null, "name": "Alice", "address": {"city": "San Francisco", "state": "CA"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 1, "id2": 200, "updated_at": "2000-01-01T00:01:00Z", "_ab_cdc_deleted_at": null, "name": "Alice", "address": {"city": "Los Angeles", "state": "CA"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 1, "id2": 201, "updated_at": "2000-01-01T00:02:00Z", "name": "Bob", "address": {"city": "Boston", "state": "MA"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 2, "id2": 200, "updated_at": "2000-01-01T00:03:00Z", "name": "Charlie", "age": "this is not an integer", "registration_date": "this is not a date"}} +// And append the records from the second sync +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_data": {"id1": 1, "id2": 200, "updated_at": "2000-01-02T00:00:00Z", "_ab_cdc_deleted_at": null, "name": "Alice", "address": {"city": "Seattle", "state": "WA"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_data": {"id1": 1, "id2": 201, "updated_at": "2000-01-02T00:00:00Z", "_ab_cdc_deleted_at": null, "name": "Bob", "address": {"city": "New York", "state": "NY"}}} +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_data": {"id1": 1, "id2": 201, "updated_at": "2000-01-02T00:01:00Z", "_ab_cdc_deleted_at": "1970-01-01T00:00:00Z"}} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_raw2.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_raw2.jsonl new file mode 100644 index 0000000000000..88b8ee7746c1c --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/dat/sync2_expectedrecords_raw2.jsonl @@ -0,0 +1,2 @@ +{"_airbyte_extracted_at": "1970-01-01T00:00:01.000000Z", "_airbyte_data": {"id1": 1, "id2": 200, "updated_at": "2001-01-01T00:00:00Z", "_ab_cdc_deleted_at": null, "name": "Someone completely different"}} +{"_airbyte_extracted_at": "1970-01-01T00:00:02.000000Z", "_airbyte_data": {"id1": 1, "id2": 200, "updated_at": "2001-01-02T00:00:00Z", "_ab_cdc_deleted_at": null, "name": "Someone completely different v2"}} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl new file mode 100644 index 0000000000000..76d0442ebe798 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_final.jsonl @@ -0,0 +1,8 @@ +{"id1": 1, "id2": 100, "updated_at": "2023-01-01T01:00:00.000000Z", "array": ["foo"], "struct": {"foo": "bar"}, "string": "foo", "number": 42.1, "integer": 42, "boolean": true, "timestamp_with_timezone": "2023-01-23T12:34:56.000000Z", "timestamp_without_timezone": "2023-01-23T12:34:56", "time_with_timezone": "12:34:56Z", "time_without_timezone": "12:34:56", "date": "2023-01-23", "unknown": {}, "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}} +{"id1": 2, "id2": 100, "updated_at": "2023-01-01T01:00:00.000000Z", "unknown": null, "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}} +{"id1": 3, "id2": 100, "updated_at": "2023-01-01T01:00:00.000000Z", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}} +{"id1": 4, "id2": 100, "updated_at": "2023-01-01T01:00:00.000000Z", "unknown": null, "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": ["Problem with `struct`", "Problem with `array`", "Problem with `number`", "Problem with `integer`", "Problem with `boolean`","Problem with `timestamp_with_timezone`", "Problem with `timestamp_without_timezone`", "Problem with `time_with_timezone`","Problem with `time_without_timezone`", "Problem with `date`"]}} +// Note that for numbers where we parse the value to JSON (struct, array, unknown) we lose precision. +// But for numbers where we create a NUMBER column, we do not lose precision (see the `number` column). +{"id1": 5, "id2": 100, "updated_at": "2023-01-01T01:00:00.000000Z", "number": 67.174118, "struct": {"nested_number": 67.174118}, "array": [67.174118], "unknown": 67.174118, "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}} +{"id1": 6, "id2": 100, "updated_at": "2023-01-01T01:00:00.000000Z", "IamACaseSensitiveColumnName": "Case senstive value", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl new file mode 100644 index 0000000000000..6b99169ececf1 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/alltypes_expectedrecords_raw.jsonl @@ -0,0 +1,6 @@ +{"_airbyte_raw_id": "14ba7c7f-e398-4e69-ac22-28d578400dbc", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 1, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": ["foo"], "struct": {"foo": "bar"}, "string": "foo", "number": 42.1, "integer": 42, "boolean": true, "timestamp_with_timezone": "2023-01-23T12:34:56Z", "timestamp_without_timezone": "2023-01-23T12:34:56", "time_with_timezone": "12:34:56Z", "time_without_timezone": "12:34:56", "date": "2023-01-23", "unknown": {}}} +{"_airbyte_raw_id": "53ce75a5-5bcc-47a3-b45c-96c2015cfe35", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 2, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": null, "struct": null, "string": null, "number": null, "integer": null, "boolean": null, "timestamp_with_timezone": null, "timestamp_without_timezone": null, "time_with_timezone": null, "time_without_timezone": null, "date": null, "unknown": null}} +{"_airbyte_raw_id": "7e1fac0c-017e-4ad6-bc78-334a34d64fbe", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 3, "id2": 100, "updated_at": "2023-01-01T01:00:00Z"}} +{"_airbyte_raw_id": "84242b60-3a34-4531-ad75-a26702960a9a", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 4, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": {}, "struct": [], "string": null, "number": "foo", "integer": "bar", "boolean": "fizz", "timestamp_with_timezone": {}, "timestamp_without_timezone": {}, "time_with_timezone": {}, "time_without_timezone": {}, "date": "airbyte", "unknown": null}} +{"_airbyte_raw_id": "a4a783b5-7729-4d0b-b659-48ceb08713f1", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 5, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "number": 67.174118, "struct": {"nested_number": 67.174118}, "array": [67.174118], "unknown": 67.174118}} +{"_airbyte_raw_id": "7e1fac0c-017e-4ad6-bc78-334a34d64fce", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 6, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "IamACaseSensitiveColumnName": "Case senstive value"}} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/incrementaldedup_expectedrecords_final.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/incrementaldedup_expectedrecords_final.jsonl new file mode 100644 index 0000000000000..5842f7b37e42b --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/incrementaldedup_expectedrecords_final.jsonl @@ -0,0 +1,2 @@ +{"_airbyte_raw_id": "80c99b54-54b4-43bd-b51b-1f67dafa2c52", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}, "id1": 1, "id2": 100, "updated_at": "2023-01-01T02:00:00.000000Z", "string": "Alice", "struct": {"city": "San Diego", "state": "CA"}, "integer": 84} +{"_airbyte_raw_id": "ad690bfb-c2c2-4172-bd73-a16c86ccbb67", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": ["Problem with `integer`"]}, "id1": 2, "id2": 100, "updated_at": "2023-01-01T03:00:00.000000Z", "string": "Bob"} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/incrementaldedup_expectedrecords_raw.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/incrementaldedup_expectedrecords_raw.jsonl new file mode 100644 index 0000000000000..63569975abc23 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/incrementaldedup_expectedrecords_raw.jsonl @@ -0,0 +1,3 @@ +{"_airbyte_raw_id": "d7b81af0-01da-4846-a650-cc398986bc99", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 1, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "string": "Alice", "struct": {"city": "San Francisco", "state": "CA"}, "integer": 42}} +{"_airbyte_raw_id": "80c99b54-54b4-43bd-b51b-1f67dafa2c52", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 1, "id2": 100, "updated_at": "2023-01-01T02:00:00Z", "string": "Alice", "struct": {"city": "San Diego", "state": "CA"}, "integer": 84}} +{"_airbyte_raw_id": "ad690bfb-c2c2-4172-bd73-a16c86ccbb67", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 2, "id2": 100, "updated_at": "2023-01-01T03:00:00Z", "string": "Bob", "integer": "oops"}} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/json_types_in_string_expectedrecords_final.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/json_types_in_string_expectedrecords_final.jsonl new file mode 100644 index 0000000000000..edcc0cc462d6b --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/json_types_in_string_expectedrecords_final.jsonl @@ -0,0 +1,5 @@ +{"id1": 1, "id2": 100, "updated_at": "2023-01-01T01:00:00.000000Z", "array": ["foo"], "struct": {"foo": "bar"}, "string": "[\"I\", \"am\", \"an\", \"array\"]", "number": 42.1, "integer": 42, "boolean": true, "timestamp_with_timezone": "2023-01-23T12:34:56.000000Z", "timestamp_without_timezone": "2023-01-23T12:34:56", "time_with_timezone": "12:34:56Z", "time_without_timezone": "12:34:56", "date": "2023-01-23", "unknown": {}, "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}} +{"id1": 2, "id2": 100, "updated_at": "2023-01-01T01:00:00.000000Z", "array": ["foo"], "struct": {"foo": "bar"}, "string": "{\"I\": \"am\", \"an\": \"object\"}", "number": 42.1, "integer": 42, "boolean": true, "timestamp_with_timezone": "2023-01-23T12:34:56.000000Z", "timestamp_without_timezone": "2023-01-23T12:34:56", "time_with_timezone": "12:34:56Z", "time_without_timezone": "12:34:56", "date": "2023-01-23", "unknown": {}, "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}} +{"id1": 3, "id2": 100, "updated_at": "2023-01-01T01:00:00.000000Z", "array": ["foo"], "struct": {"foo": "bar"}, "string": "true", "number": 42.1, "integer": 42, "boolean": true, "timestamp_with_timezone": "2023-01-23T12:34:56.000000Z", "timestamp_without_timezone": "2023-01-23T12:34:56", "time_with_timezone": "12:34:56Z", "time_without_timezone": "12:34:56", "date": "2023-01-23", "unknown": {}, "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}} +{"id1": 4, "id2": 100, "updated_at": "2023-01-01T01:00:00.000000Z", "array": ["foo"], "struct": {"foo": "bar"}, "string": "3.14", "number": 42.1, "integer": 42, "boolean": true, "timestamp_with_timezone": "2023-01-23T12:34:56.000000Z", "timestamp_without_timezone": "2023-01-23T12:34:56", "time_with_timezone": "12:34:56Z", "time_without_timezone": "12:34:56", "date": "2023-01-23", "unknown": {}, "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}} +{"id1": 5, "id2": 100, "updated_at": "2023-01-01T01:00:00.000000Z", "array": ["foo"], "struct": {"foo": "bar"}, "string": "I am a valid json string", "number": 42.1, "integer": 42, "boolean": true, "timestamp_with_timezone": "2023-01-23T12:34:56.000000Z", "timestamp_without_timezone": "2023-01-23T12:34:56", "time_with_timezone": "12:34:56Z", "time_without_timezone": "12:34:56", "date": "2023-01-23", "unknown": {}, "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/json_types_in_string_expectedrecords_raw.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/json_types_in_string_expectedrecords_raw.jsonl new file mode 100644 index 0000000000000..5c10203c7837f --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/json_types_in_string_expectedrecords_raw.jsonl @@ -0,0 +1,5 @@ +{"_airbyte_raw_id": "14ba7c7f-e398-4e69-ac22-28d578400dbc", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 1, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": ["foo"], "struct": {"foo": "bar"}, "string": ["I", "am", "an", "array"], "number": 42.1, "integer": 42, "boolean": true, "timestamp_with_timezone": "2023-01-23T12:34:56Z", "timestamp_without_timezone": "2023-01-23T12:34:56", "time_with_timezone": "12:34:56Z", "time_without_timezone": "12:34:56", "date": "2023-01-23", "unknown": {}}} +{"_airbyte_raw_id": "53ce75a5-5bcc-47a3-b45c-96c2015cfe35", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 2, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": ["foo"], "struct": {"foo": "bar"}, "string": {"I": "am", "an": "object"}, "number": 42.1, "integer": 42, "boolean": true, "timestamp_with_timezone": "2023-01-23T12:34:56Z", "timestamp_without_timezone": "2023-01-23T12:34:56", "time_with_timezone": "12:34:56Z", "time_without_timezone": "12:34:56", "date": "2023-01-23", "unknown": {}}} +{"_airbyte_raw_id": "7e1fac0c-017e-4ad6-bc78-334a34d64fbe", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 3, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": ["foo"], "struct": {"foo": "bar"}, "string": true, "number": 42.1, "integer": 42, "boolean": true, "timestamp_with_timezone": "2023-01-23T12:34:56Z", "timestamp_without_timezone": "2023-01-23T12:34:56", "time_with_timezone": "12:34:56Z", "time_without_timezone": "12:34:56", "date": "2023-01-23", "unknown": {}}} +{"_airbyte_raw_id": "84242b60-3a34-4531-ad75-a26702960a9a", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 4, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": ["foo"], "struct": {"foo": "bar"}, "string": 3.14, "number": 42.1, "integer": 42, "boolean": true, "timestamp_with_timezone": "2023-01-23T12:34:56Z", "timestamp_without_timezone": "2023-01-23T12:34:56", "time_with_timezone": "12:34:56Z", "time_without_timezone": "12:34:56", "date": "2023-01-23", "unknown": {}}} +{"_airbyte_raw_id": "a4a783b5-7729-4d0b-b659-48ceb08713f1", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 5, "id2": 100, "updated_at": "2023-01-01T01:00:00Z", "array": ["foo"], "struct": {"foo": "bar"}, "string": "I am a valid json string", "number": 42.1, "integer": 42, "boolean": true, "timestamp_with_timezone": "2023-01-23T12:34:56Z", "timestamp_without_timezone": "2023-01-23T12:34:56", "time_with_timezone": "12:34:56Z", "time_without_timezone": "12:34:56", "date": "2023-01-23", "unknown": {}}} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/nocolumns_expectedrecords_final.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/nocolumns_expectedrecords_final.jsonl new file mode 100644 index 0000000000000..4ecd95d83b637 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/nocolumns_expectedrecords_final.jsonl @@ -0,0 +1 @@ +{"_airbyte_raw_id": "14ba7c7f-e398-4e69-ac22-28d578400dbc", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/nocolumns_expectedrecords_raw.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/nocolumns_expectedrecords_raw.jsonl new file mode 100644 index 0000000000000..cd7c03aba6774 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/nocolumns_expectedrecords_raw.jsonl @@ -0,0 +1 @@ +{"_airbyte_raw_id": "14ba7c7f-e398-4e69-ac22-28d578400dbc", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {}} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/reservedkeywords_expectedrecords_final.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/reservedkeywords_expectedrecords_final.jsonl new file mode 100644 index 0000000000000..b34ad054ab33c --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/reservedkeywords_expectedrecords_final.jsonl @@ -0,0 +1 @@ +{"_airbyte_raw_id":"b2e0efc4-38a8-47ba-970c-8103f09f08d5","_airbyte_extracted_at":"2023-01-01T00:00:00.000000Z","_airbyte_meta":{"errors":[]}, "current_date": "foo", "join": "bar"} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/timestampformats_expectedrecords_final.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/timestampformats_expectedrecords_final.jsonl new file mode 100644 index 0000000000000..78ded5f99d0e9 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/timestampformats_expectedrecords_final.jsonl @@ -0,0 +1,16 @@ +// https://docs.aws.amazon.com/redshift/latest/dg/r_Datetime_types.html#r_Datetime_types-timetz +// TIME, TIMETZ, TIMESTAMP, TIMESTAMPTZ values are UTC in user tables. +// Note that redshift stores precision to microseconds. Java deserialization in tests preserves them only for non-zero values +// except for timestamp with time zone where Z is required at end for even zero values +{"_airbyte_raw_id": "14ba7c7f-e398-4e69-ac22-28d578400dbc", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}, "timestamp_with_timezone": "2023-01-23T12:34:56.000000Z", "time_with_timezone": "12:34:56Z"} +{"_airbyte_raw_id": "05028c5f-7813-4e9c-bd4b-387d1f8ba435", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}, "timestamp_with_timezone": "2023-01-23T20:34:56.000000Z", "time_with_timezone": "12:34:56-08:00"} +{"_airbyte_raw_id": "95dfb0c6-6a67-4ba0-9935-643bebc90437", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}, "timestamp_with_timezone": "2023-01-23T20:34:56.000000Z", "time_with_timezone": "12:34:56-08:00"} +{"_airbyte_raw_id": "f3d8abe2-bb0f-4caf-8ddc-0641df02f3a9", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}, "timestamp_with_timezone": "2023-01-23T20:34:56.000000Z", "time_with_timezone": "12:34:56-08:00"} +{"_airbyte_raw_id": "a81ed40a-2a49-488d-9714-d53e8b052968", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}, "timestamp_with_timezone": "2023-01-23T04:34:56.000000Z", "time_with_timezone": "12:34:56+08:00"} +{"_airbyte_raw_id": "c07763a0-89e6-4cb7-b7d0-7a34a7c9918a", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}, "timestamp_with_timezone": "2023-01-23T04:34:56.000000Z", "time_with_timezone": "12:34:56+08:00"} +{"_airbyte_raw_id": "358d3b52-50ab-4e06-9094-039386f9bf0d", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}, "timestamp_with_timezone": "2023-01-23T04:34:56.000000Z", "time_with_timezone": "12:34:56+08:00"} +{"_airbyte_raw_id": "db8200ac-b2b9-4b95-a053-8a0343042751", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}, "timestamp_with_timezone": "2023-01-23T12:34:56.123000Z", "time_with_timezone": "12:34:56.123Z"} + +{"_airbyte_raw_id": "10ce5d93-6923-4217-a46f-103833837038", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}, "timestamp_without_timezone": "2023-01-23T12:34:56", "time_without_timezone": "12:34:56", "date": "2023-01-23"} +// Bigquery returns 6 decimal places if there are any decimal places... but not for timestamp_with_timezone +{"_airbyte_raw_id": "a7a6e176-7464-4a0b-b55c-b4f936e8d5a1", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}, "timestamp_without_timezone": "2023-01-23T12:34:56.123", "time_without_timezone": "12:34:56.123"} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/weirdcolumnnames_expectedrecords_final.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/weirdcolumnnames_expectedrecords_final.jsonl new file mode 100644 index 0000000000000..adfbd06d6a55a --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/weirdcolumnnames_expectedrecords_final.jsonl @@ -0,0 +1,9 @@ +// column renamings: +// * $starts_with_dollar_sign -> _starts_with_dollar_sign +// * includes"doublequote -> includes_doublequote +// * includes'singlequote -> includes_singlequote +// * includes`backtick -> includes_backtick +// * includes$$doubledollar -> includes__doubledollar +// * includes.period -> includes_period +// * endswithbackslash\ -> endswithbackslash_ +{"_airbyte_raw_id": "7e7330a1-42fb-41ec-a955-52f18bd61964", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_meta": {"errors": []}, "id1": 1, "id2": 100, "updated_at": "2023-01-01T02:00:00.000000Z", "_starts_with_dollar_sign": "foo", "includes_doublequote": "foo", "includes_singlequote": "foo", "includes_backtick": "foo", "includes_period": "foo", "includes__doubledollar": "foo", "endswithbackslash_": "foo"} diff --git a/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/weirdcolumnnames_expectedrecords_raw.jsonl b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/weirdcolumnnames_expectedrecords_raw.jsonl new file mode 100644 index 0000000000000..2b602082a3496 --- /dev/null +++ b/airbyte-integrations/connectors/destination-postgres/src/test-integration/resources/sqlgenerator/weirdcolumnnames_expectedrecords_raw.jsonl @@ -0,0 +1 @@ +{"_airbyte_raw_id": "7e7330a1-42fb-41ec-a955-52f18bd61964", "_airbyte_extracted_at": "2023-01-01T00:00:00.000000Z", "_airbyte_data": {"id1": 1, "id2": 100, "updated_at": "2023-01-01T02:00:00Z", "$starts_with_dollar_sign": "foo", "includes\"doublequote": "foo", "includes'singlequote": "foo", "includes`backtick": "foo", "includes.period": "foo", "includes$$doubledollar": "foo", "endswithbackslash\\": "foo"}} diff --git a/docs/integrations/destinations/postgres.md b/docs/integrations/destinations/postgres.md index 32dbef5d9243c..454b399d0609c 100644 --- a/docs/integrations/destinations/postgres.md +++ b/docs/integrations/destinations/postgres.md @@ -170,6 +170,7 @@ Now that you have set up the Postgres destination connector, check out the follo | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:----------------------------------------------------------------------------------------------------| +| 0.5.4 | 2024-01-11 | [34177](https://github.com/airbytehq/airbyte/pull/34177) | Add code for DV2 beta (no user-visible changes) | | 0.5.3 | 2024-01-10 | [34135](https://github.com/airbytehq/airbyte/pull/34135) | Use published CDK missed in previous release | | 0.5.2 | 2024-01-08 | [33875](https://github.com/airbytehq/airbyte/pull/33875) | Update CDK to get Tunnel heartbeats feature | | 0.5.1 | 2024-01-04 | [33873](https://github.com/airbytehq/airbyte/pull/33873) | Install normalization to enable DV2 beta | @@ -190,4 +191,4 @@ Now that you have set up the Postgres destination connector, check out the follo | 0.3.13 | 2021-12-01 | [\#8371](https://github.com/airbytehq/airbyte/pull/8371) | Fixed incorrect handling "\n" in ssh key | | 0.3.12 | 2021-11-08 | [\#7719](https://github.com/airbytehq/airbyte/pull/7719) | Improve handling of wide rows by buffering records based on their byte size rather than their count | | 0.3.11 | 2021-09-07 | [\#5743](https://github.com/airbytehq/airbyte/pull/5743) | Add SSH Tunnel support | -| 0.3.10 | 2021-08-11 | [\#5336](https://github.com/airbytehq/airbyte/pull/5336) | Destination Postgres: fix \u0000\(NULL\) value processing | \ No newline at end of file +| 0.3.10 | 2021-08-11 | [\#5336](https://github.com/airbytehq/airbyte/pull/5336) | Destination Postgres: fix \u0000\(NULL\) value processing | From a9f2b2955dc4a745b7920022d1dec27e488b60a8 Mon Sep 17 00:00:00 2001 From: benmoriceau Date: Thu, 18 Jan 2024 00:45:51 +0000 Subject: [PATCH 132/574] Bump Airbyte version from 0.50.43 to 0.50.44 --- .bumpversion.cfg | 2 +- docs/operator-guides/upgrading-airbyte.md | 2 +- gradle.properties | 2 +- run-ab-platform.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9a2f045a1537d..57fbc2cdd4634 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.50.43 +current_version = 0.50.44 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-[a-z]+)? diff --git a/docs/operator-guides/upgrading-airbyte.md b/docs/operator-guides/upgrading-airbyte.md index 6029020327f41..4f73ce7859408 100644 --- a/docs/operator-guides/upgrading-airbyte.md +++ b/docs/operator-guides/upgrading-airbyte.md @@ -128,7 +128,7 @@ If you are upgrading from (i.e. your current version of Airbyte is) Airbyte vers Here's an example of what it might look like with the values filled in. It assumes that the downloaded `airbyte_archive.tar.gz` is in `/tmp`. ```bash - docker run --rm -v /tmp:/config airbyte/migration:0.50.43 --\ + docker run --rm -v /tmp:/config airbyte/migration:0.50.44 --\ --input /config/airbyte_archive.tar.gz\ --output /config/airbyte_archive_migrated.tar.gz ``` diff --git a/gradle.properties b/gradle.properties index 4d7f4ccd22283..9d7fb27ae04cb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION=0.50.43 +VERSION=0.50.44 # NOTE: some of these values are overwritten in CI! # NOTE: if you want to override this for your local machine, set overrides in ~/.gradle/gradle.properties diff --git a/run-ab-platform.sh b/run-ab-platform.sh index ffd721a29b640..2f347c8545d50 100755 --- a/run-ab-platform.sh +++ b/run-ab-platform.sh @@ -1,6 +1,6 @@ #!/bin/bash -VERSION=0.50.43 +VERSION=0.50.44 # Run away from anything even a little scary set -o nounset # -u exit if a variable is not set set -o errexit # -f exit for any command failure" From 1b0ad2403fd4236fb38f88c5a0e29861d8fa212b Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Wed, 17 Jan 2024 17:45:12 -0800 Subject: [PATCH 133/574] source-faker: unique state messages (#34344) --- .../connectors/source-faker/Dockerfile | 2 +- .../connectors/source-faker/metadata.yaml | 2 +- .../source-faker/source_faker/streams.py | 8 ++++---- docs/integrations/sources/faker.md | 18 ++++++++++++------ 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/airbyte-integrations/connectors/source-faker/Dockerfile b/airbyte-integrations/connectors/source-faker/Dockerfile index e880e4f38beee..9db110142dbc6 100644 --- a/airbyte-integrations/connectors/source-faker/Dockerfile +++ b/airbyte-integrations/connectors/source-faker/Dockerfile @@ -34,5 +34,5 @@ COPY source_faker ./source_faker ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] -LABEL io.airbyte.version=5.0.1 +LABEL io.airbyte.version=5.0.2 LABEL io.airbyte.name=airbyte/source-faker diff --git a/airbyte-integrations/connectors/source-faker/metadata.yaml b/airbyte-integrations/connectors/source-faker/metadata.yaml index fdd0575e480be..e228708b816ee 100644 --- a/airbyte-integrations/connectors/source-faker/metadata.yaml +++ b/airbyte-integrations/connectors/source-faker/metadata.yaml @@ -7,7 +7,7 @@ data: connectorSubtype: api connectorType: source definitionId: dfd88b22-b603-4c3d-aad7-3701784586b1 - dockerImageTag: 5.0.1 + dockerImageTag: 5.0.2 dockerRepository: airbyte/source-faker documentationUrl: https://docs.airbyte.com/integrations/sources/faker githubIssueLabel: source-faker diff --git a/airbyte-integrations/connectors/source-faker/source_faker/streams.py b/airbyte-integrations/connectors/source-faker/source_faker/streams.py index ba7d70b7dd2c8..002866ba7c54c 100644 --- a/airbyte-integrations/connectors/source-faker/source_faker/streams.py +++ b/airbyte-integrations/connectors/source-faker/source_faker/streams.py @@ -119,9 +119,9 @@ def read_records(self, **kwargs) -> Iterable[Mapping[str, Any]]: if records_remaining_this_loop == 0: break - self.state = {"seed": self.seed, "updated_at": updated_at} + self.state = {"seed": self.seed, "updated_at": updated_at, "loop_offset": loop_offset} - self.state = {"seed": self.seed, "updated_at": updated_at} + self.state = {"seed": self.seed, "updated_at": updated_at, "loop_offset": loop_offset} class Purchases(Stream, IncrementalMixin): @@ -180,6 +180,6 @@ def read_records(self, **kwargs) -> Iterable[Mapping[str, Any]]: if records_remaining_this_loop == 0: break - self.state = {"seed": self.seed, "updated_at": updated_at} + self.state = {"seed": self.seed, "updated_at": updated_at, "loop_offset": loop_offset} - self.state = {"seed": self.seed, "updated_at": updated_at} + self.state = {"seed": self.seed, "updated_at": updated_at, "loop_offset": loop_offset} diff --git a/docs/integrations/sources/faker.md b/docs/integrations/sources/faker.md index e7d8e34878144..39a58897e1242 100644 --- a/docs/integrations/sources/faker.md +++ b/docs/integrations/sources/faker.md @@ -2,11 +2,13 @@ ## Sync overview -The Sample Data (Faker) source generates sample data using the python [`mimesis`](https://mimesis.name/en/master/) package. +The Sample Data (Faker) source generates sample data using the python +[`mimesis`](https://mimesis.name/en/master/) package. ### Output schema -This source will generate an "e-commerce-like" dataset with users, products, and purchases. Here's what is produced at a Postgres destination connected to this source: +This source will generate an "e-commerce-like" dataset with users, products, and purchases. Here's +what is produced at a Postgres destination connected to this source: ```sql CREATE TABLE "public"."users" ( @@ -84,9 +86,12 @@ CREATE TABLE "public"."purchases" ( | Incremental Sync | Yes | | | Namespaces | No | | -Of note, if you choose `Incremental Sync`, state will be maintained between syncs, and once you hit `count` records, no new records will be added. +Of note, if you choose `Incremental Sync`, state will be maintained between syncs, and once you hit +`count` records, no new records will be added. -You can choose a specific `seed` (integer) as an option for this connector which will guarantee that the same fake records are generated each time. Otherwise, random data will be created on each subsequent sync. +You can choose a specific `seed` (integer) as an option for this connector which will guarantee that +the same fake records are generated each time. Otherwise, random data will be created on each +subsequent sync. ### Requirements @@ -95,8 +100,9 @@ None! ## Changelog | Version | Date | Pull Request | Subject | -|:--------|:-----------|:----------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------| -| 5.0.1 | 2023-01-08 | [34033](https://github.com/airbytehq/airbyte/pull/34033) | Add standard entrypoints for usage with AirbyteLib | +| :------ | :--------- | :-------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------- | +| 5.0.2 | 2024-01-17 | [34344](https://github.com/airbytehq/airbyte/pull/34344) | Ensure unique state messages | +| 5.0.1 | 2023-01-08 | [34033](https://github.com/airbytehq/airbyte/pull/34033) | Add standard entrypoints for usage with AirbyteLib | | 5.0.0 | 2023-08-08 | [29213](https://github.com/airbytehq/airbyte/pull/29213) | Change all `*id` fields and `products.year` to be integer | | 4.0.0 | 2023-07-19 | [28485](https://github.com/airbytehq/airbyte/pull/28485) | Bump to test publication | | 3.0.2 | 2023-07-07 | [27807](https://github.com/airbytehq/airbyte/pull/28060) | Bump to test publication | From 237b14875937e3c779103308b39df4f349183c4e Mon Sep 17 00:00:00 2001 From: Augustin Date: Thu, 18 Jan 2024 08:12:14 +0100 Subject: [PATCH 134/574] airbyte-ci: fix nightly build workflow (#34345) --- .github/workflows/connectors_nightly_build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/connectors_nightly_build.yml b/.github/workflows/connectors_nightly_build.yml index a2724c063d6b7..edea7e253ee34 100644 --- a/.github/workflows/connectors_nightly_build.yml +++ b/.github/workflows/connectors_nightly_build.yml @@ -37,6 +37,7 @@ jobs: test_connectors: name: "Test connectors: ${{ inputs.test-connectors-options || 'nightly build for Certified connectors' }}" timeout-minutes: 720 # 12 hours + needs: get_ci_runner runs-on: ${{ needs.get_ci_runner.outputs.runner_name }} steps: - name: Checkout Airbyte From 0faa69d899c4a5d592ab31c0e572f39892d4a6c2 Mon Sep 17 00:00:00 2001 From: Alexandre Girard Date: Wed, 17 Jan 2024 23:54:02 -0800 Subject: [PATCH 135/574] concurrent cdk: improve resource usage and stop waiting on the main thread (#33669) Co-authored-by: Augustin --- .../concurrent_read_processor.py | 41 ++++---- .../concurrent_source/concurrent_source.py | 7 +- .../concurrent_source/thread_pool_manager.py | 33 +++---- .../sources/concurrent_source/throttler.py | 25 +++++ .../streams/concurrent/partition_enqueuer.py | 8 +- .../streams/concurrent/partition_reader.py | 7 +- .../concurrent/partitions/throttled_queue.py | 41 ++++++++ .../test_concurrent_partition_generator.py | 14 +-- .../test_concurrent_read_processor.py | 94 ++++++++++++++++--- .../concurrent/test_thread_pool_manager.py | 17 +--- .../concurrent/test_throttled_queue.py | 65 +++++++++++++ .../streams/concurrent/test_throttler.py | 13 +++ 12 files changed, 277 insertions(+), 88 deletions(-) create mode 100644 airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/throttler.py create mode 100644 airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/partitions/throttled_queue.py create mode 100644 airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_throttled_queue.py create mode 100644 airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_throttler.py diff --git a/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/concurrent_read_processor.py b/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/concurrent_read_processor.py index d73a524f9625a..acfc0c039694f 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/concurrent_read_processor.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/concurrent_read_processor.py @@ -43,9 +43,9 @@ def __init__( """ self._stream_name_to_instance = {s.name: s for s in stream_instances_to_read_from} self._record_counter = {} - self._streams_to_partitions: Dict[str, Set[Partition]] = {} + self._streams_to_running_partitions: Dict[str, Set[Partition]] = {} for stream in stream_instances_to_read_from: - self._streams_to_partitions[stream.name] = set() + self._streams_to_running_partitions[stream.name] = set() self._record_counter[stream.name] = 0 self._thread_pool_manager = thread_pool_manager self._partition_enqueuer = partition_enqueuer @@ -55,6 +55,7 @@ def __init__( self._slice_logger = slice_logger self._message_repository = message_repository self._partition_reader = partition_reader + self._streams_done: Set[str] = set() def on_partition_generation_completed(self, sentinel: PartitionGenerationCompletedSentinel) -> Iterable[AirbyteMessage]: """ @@ -67,7 +68,8 @@ def on_partition_generation_completed(self, sentinel: PartitionGenerationComplet self._streams_currently_generating_partitions.remove(sentinel.stream.name) ret = [] # It is possible for the stream to already be done if no partitions were generated - if self._is_stream_done(stream_name): + # If the partition generation process was completed and there are no partitions left to process, the stream is done + if self._is_stream_done(stream_name) or len(self._streams_to_running_partitions[stream_name]) == 0: ret.append(self._on_stream_is_done(stream_name)) if self._stream_instances_to_start_partition_generation: ret.append(self.start_next_partition_generator()) @@ -81,7 +83,7 @@ def on_partition(self, partition: Partition) -> None: 3. Submit the partition to the thread pool manager """ stream_name = partition.stream_name() - self._streams_to_partitions[stream_name].add(partition) + self._streams_to_running_partitions[stream_name].add(partition) if self._slice_logger.should_log_slice_message(self._logger): self._message_repository.emit_message(self._slice_logger.create_slice_log_message(partition.to_slice())) self._thread_pool_manager.submit(self._partition_reader.process_partition, partition) @@ -95,8 +97,12 @@ def on_partition_complete_sentinel(self, sentinel: PartitionCompleteSentinel) -> """ partition = sentinel.partition partition.close() - if self._is_stream_done(partition.stream_name()): - yield self._on_stream_is_done(partition.stream_name()) + partitions_running = self._streams_to_running_partitions[partition.stream_name()] + if partition in partitions_running: + partitions_running.remove(partition) + # If all partitions were generated and this was the last one, the stream is done + if partition.stream_name() not in self._streams_currently_generating_partitions and len(partitions_running) == 0: + yield self._on_stream_is_done(partition.stream_name()) yield from self._message_repository.consume_queue() def on_record(self, record: Record) -> Iterable[AirbyteMessage]: @@ -114,11 +120,10 @@ def on_record(self, record: Record) -> Iterable[AirbyteMessage]: message = stream_data_to_airbyte_message(record.stream_name, record.data) stream = self._stream_name_to_instance[record.stream_name] - if self._record_counter[stream.name] == 0: - self._logger.info(f"Marking stream {stream.name} as RUNNING") - yield stream_status_as_airbyte_message(stream.as_airbyte_stream(), AirbyteStreamStatus.RUNNING) - if message.type == MessageType.RECORD: + if self._record_counter[stream.name] == 0: + self._logger.info(f"Marking stream {stream.name} as RUNNING") + yield stream_status_as_airbyte_message(stream.as_airbyte_stream(), AirbyteStreamStatus.RUNNING) self._record_counter[stream.name] += 1 yield message yield from self._message_repository.consume_queue() @@ -161,30 +166,24 @@ def is_done(self) -> bool: 2. There are no more streams to read from 3. All partitions for all streams are closed """ - return ( - not self._streams_currently_generating_partitions - and not self._stream_instances_to_start_partition_generation - and all([all(p.is_closed() for p in partitions) for partitions in self._streams_to_partitions.values()]) - ) + return all([self._is_stream_done(stream_name) for stream_name in self._stream_name_to_instance.keys()]) def _is_stream_done(self, stream_name: str) -> bool: - return ( - all([p.is_closed() for p in self._streams_to_partitions[stream_name]]) - and stream_name not in self._streams_currently_generating_partitions - ) + return stream_name in self._streams_done def _on_stream_is_done(self, stream_name: str) -> AirbyteMessage: self._logger.info(f"Read {self._record_counter[stream_name]} records from {stream_name} stream") self._logger.info(f"Marking stream {stream_name} as STOPPED") stream = self._stream_name_to_instance[stream_name] self._logger.info(f"Finished syncing {stream.name}") + self._streams_done.add(stream_name) return stream_status_as_airbyte_message(stream.as_airbyte_stream(), AirbyteStreamStatus.COMPLETE) def _stop_streams(self) -> Iterable[AirbyteMessage]: self._thread_pool_manager.shutdown() - for stream_name, partitions in self._streams_to_partitions.items(): + for stream_name in self._streams_to_running_partitions.keys(): stream = self._stream_name_to_instance[stream_name] - if not all([p.is_closed() for p in partitions]): + if not self._is_stream_done(stream_name): self._logger.info(f"Marking stream {stream.name} as STOPPED") self._logger.info(f"Finished syncing {stream.name}") yield stream_status_as_airbyte_message(stream.as_airbyte_stream(), AirbyteStreamStatus.INCOMPLETE) diff --git a/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/concurrent_source.py b/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/concurrent_source.py index b5439b2302306..f37b78960a81f 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/concurrent_source.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/concurrent_source.py @@ -16,6 +16,7 @@ from airbyte_cdk.sources.streams.concurrent.partition_reader import PartitionReader from airbyte_cdk.sources.streams.concurrent.partitions.partition import Partition from airbyte_cdk.sources.streams.concurrent.partitions.record import Record +from airbyte_cdk.sources.streams.concurrent.partitions.throttled_queue import ThrottledQueue from airbyte_cdk.sources.streams.concurrent.partitions.types import PartitionCompleteSentinel, QueueItem from airbyte_cdk.sources.utils.slice_logger import DebugSliceLogger, SliceLogger @@ -82,7 +83,7 @@ def read( if not stream_instances_to_read_from: return - queue: Queue[QueueItem] = Queue() + queue: ThrottledQueue = ThrottledQueue(Queue(), self._threadpool.get_throttler(), self._timeout_seconds) concurrent_stream_processor = ConcurrentReadProcessor( stream_instances_to_read_from, PartitionEnqueuer(queue), @@ -112,10 +113,10 @@ def _submit_initial_partition_generators(self, concurrent_stream_processor: Conc def _consume_from_queue( self, - queue: Queue[QueueItem], + queue: ThrottledQueue, concurrent_stream_processor: ConcurrentReadProcessor, ) -> Iterable[AirbyteMessage]: - while airbyte_message_or_record_or_exception := queue.get(block=True, timeout=self._timeout_seconds): + while airbyte_message_or_record_or_exception := queue.get(): yield from self._handle_item( airbyte_message_or_record_or_exception, concurrent_stream_processor, diff --git a/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/thread_pool_manager.py b/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/thread_pool_manager.py index 0c269cf0b3ee3..3c0eec206c54b 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/thread_pool_manager.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/thread_pool_manager.py @@ -2,10 +2,11 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # import logging -import time from concurrent.futures import Future, ThreadPoolExecutor from typing import Any, Callable, List +from airbyte_cdk.sources.concurrent_source.throttler import Throttler + class ThreadPoolManager: """ @@ -19,8 +20,8 @@ def __init__( self, threadpool: ThreadPoolExecutor, logger: logging.Logger, - max_concurrent_tasks: int = DEFAULT_MAX_QUEUE_SIZE, sleep_time: float = DEFAULT_SLEEP_TIME, + max_concurrent_tasks: int = DEFAULT_MAX_QUEUE_SIZE, ): """ :param threadpool: The threadpool to use @@ -31,23 +32,17 @@ def __init__( self._threadpool = threadpool self._logger = logger self._max_concurrent_tasks = max_concurrent_tasks - self._sleep_time = sleep_time self._futures: List[Future[Any]] = [] + self._throttler = Throttler(self._futures, sleep_time, max_concurrent_tasks) + + def get_throttler(self) -> Throttler: + return self._throttler def submit(self, function: Callable[..., Any], *args: Any) -> None: - # Submit a task to the threadpool, waiting if there are too many pending tasks - self._wait_while_too_many_pending_futures(self._futures) + # Submit a task to the threadpool, removing completed tasks if there are too many tasks in self._futures. + self._prune_futures(self._futures) self._futures.append(self._threadpool.submit(function, *args)) - def _wait_while_too_many_pending_futures(self, futures: List[Future[Any]]) -> None: - # Wait until the number of pending tasks is < self._max_concurrent_tasks - while True: - self._prune_futures(futures) - if len(futures) < self._max_concurrent_tasks: - break - self._logger.info("Main thread is sleeping because the task queue is full...") - time.sleep(self._sleep_time) - def _prune_futures(self, futures: List[Future[Any]]) -> None: """ Take a list in input and remove the futures that are completed. If a future has an exception, it'll raise and kill the stream @@ -60,12 +55,14 @@ def _prune_futures(self, futures: List[Future[Any]]) -> None: for index in reversed(range(len(futures))): future = futures[index] - optional_exception = future.exception() - if optional_exception: - exception = RuntimeError(f"Failed reading with error: {optional_exception}") - self._stop_and_raise_exception(exception) if future.done(): + # Only call future.exception() if the future is known to be done because it will block until the future is done. + # See https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Future.exception + optional_exception = future.exception() + if optional_exception: + exception = RuntimeError(f"Failed reading with error: {optional_exception}") + self._stop_and_raise_exception(exception) futures.pop(index) def shutdown(self) -> None: diff --git a/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/throttler.py b/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/throttler.py new file mode 100644 index 0000000000000..5b343caef1d78 --- /dev/null +++ b/airbyte-cdk/python/airbyte_cdk/sources/concurrent_source/throttler.py @@ -0,0 +1,25 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +import time +from concurrent.futures import Future +from typing import Any, List + + +class Throttler: + """ + A throttler that waits until the number of concurrent tasks is below a certain threshold. + """ + + def __init__(self, futures_list: List[Future[Any]], sleep_time: float, max_concurrent_tasks: int): + """ + :param futures_list: The list of futures to monitor + :param sleep_time: How long to sleep if there are too many pending tasks + :param max_concurrent_tasks: The maximum number of tasks that can be pending at the same time + """ + self._futures_list = futures_list + self._sleep_time = sleep_time + self._max_concurrent_tasks = max_concurrent_tasks + + def wait_and_acquire(self) -> None: + while len(self._futures_list) >= self._max_concurrent_tasks: + time.sleep(self._sleep_time) diff --git a/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/partition_enqueuer.py b/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/partition_enqueuer.py index 138bb9cf86b79..342c7ae3eec21 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/partition_enqueuer.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/partition_enqueuer.py @@ -2,11 +2,9 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # -from queue import Queue - from airbyte_cdk.sources.concurrent_source.partition_generation_completed_sentinel import PartitionGenerationCompletedSentinel from airbyte_cdk.sources.streams.concurrent.abstract_stream import AbstractStream -from airbyte_cdk.sources.streams.concurrent.partitions.types import QueueItem +from airbyte_cdk.sources.streams.concurrent.partitions.throttled_queue import ThrottledQueue class PartitionEnqueuer: @@ -14,10 +12,10 @@ class PartitionEnqueuer: Generates partitions from a partition generator and puts them in a queue. """ - def __init__(self, queue: Queue[QueueItem]) -> None: + def __init__(self, queue: ThrottledQueue) -> None: """ :param queue: The queue to put the partitions in. - :param sentinel: The sentinel to put in the queue when all the partitions have been generated. + :param throttler: The throttler to use to throttle the partition generation. """ self._queue = queue diff --git a/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/partition_reader.py b/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/partition_reader.py index 0bc9c35117a6c..28f920326cf43 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/partition_reader.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/partition_reader.py @@ -2,10 +2,9 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # -from queue import Queue - from airbyte_cdk.sources.streams.concurrent.partitions.partition import Partition -from airbyte_cdk.sources.streams.concurrent.partitions.types import PartitionCompleteSentinel, QueueItem +from airbyte_cdk.sources.streams.concurrent.partitions.throttled_queue import ThrottledQueue +from airbyte_cdk.sources.streams.concurrent.partitions.types import PartitionCompleteSentinel class PartitionReader: @@ -13,7 +12,7 @@ class PartitionReader: Generates records from a partition and puts them in a queue. """ - def __init__(self, queue: Queue[QueueItem]) -> None: + def __init__(self, queue: ThrottledQueue) -> None: """ :param queue: The queue to put the records in. """ diff --git a/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/partitions/throttled_queue.py b/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/partitions/throttled_queue.py new file mode 100644 index 0000000000000..27a1757e47f5a --- /dev/null +++ b/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/partitions/throttled_queue.py @@ -0,0 +1,41 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from queue import Queue + +from airbyte_cdk.sources.concurrent_source.throttler import Throttler +from airbyte_cdk.sources.streams.concurrent.partitions.types import QueueItem + + +class ThrottledQueue: + """ + A queue that throttles the number of items that can be added to it. + + We throttle the queue using custom logic instead of relying on the queue's max size + because the main thread can continuously dequeue before submitting a future. + + Since the main thread doesn't wait, it'll be able to remove items from the queue even if the tasks should be throttled, + so the tasks won't wait. + + This class solves this issue by checking if we should throttle the queue before adding an item to it. + An example implementation of a throttler would check if the number of pending futures is greater than a certain threshold. + """ + + def __init__(self, queue: Queue[QueueItem], throttler: Throttler, timeout: float) -> None: + """ + :param queue: The queue to throttle + :param throttler: The throttler to use to throttle the queue + :param timeout: The timeout to use when getting items from the queue + """ + self._queue = queue + self._throttler = throttler + self._timeout = timeout + + def put(self, item: QueueItem) -> None: + self._throttler.wait_and_acquire() + self._queue.put(item) + + def get(self) -> QueueItem: + return self._queue.get(block=True, timeout=self._timeout) + + def empty(self) -> bool: + return self._queue.empty() diff --git a/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_concurrent_partition_generator.py b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_concurrent_partition_generator.py index 397c8a194840d..d41b92fb8afa6 100644 --- a/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_concurrent_partition_generator.py +++ b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_concurrent_partition_generator.py @@ -2,21 +2,21 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # -from queue import Queue -from unittest.mock import Mock +from unittest.mock import Mock, call import pytest from airbyte_cdk.models import SyncMode from airbyte_cdk.sources.concurrent_source.partition_generation_completed_sentinel import PartitionGenerationCompletedSentinel from airbyte_cdk.sources.streams.concurrent.adapters import StreamPartition from airbyte_cdk.sources.streams.concurrent.partition_enqueuer import PartitionEnqueuer +from airbyte_cdk.sources.streams.concurrent.partitions.throttled_queue import ThrottledQueue @pytest.mark.parametrize( "slices", [pytest.param([], id="test_no_partitions"), pytest.param([{"partition": 1}, {"partition": 2}], id="test_two_partitions")] ) def test_partition_generator(slices): - queue = Queue() + queue = Mock(spec=ThrottledQueue) partition_generator = PartitionEnqueuer(queue) stream = Mock() @@ -30,10 +30,4 @@ def test_partition_generator(slices): partition_generator.generate_partitions(stream) - actual_partitions = [] - while partition := queue.get(False): - if isinstance(partition, PartitionGenerationCompletedSentinel): - break - actual_partitions.append(partition) - - assert actual_partitions == partitions + assert queue.put.has_calls([call(p) for p in partitions] + [call(PartitionGenerationCompletedSentinel(stream))]) diff --git a/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_concurrent_read_processor.py b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_concurrent_read_processor.py index a520cc7d9c7e3..e33ce5b4df729 100644 --- a/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_concurrent_read_processor.py +++ b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_concurrent_read_processor.py @@ -40,13 +40,11 @@ def setUp(self): self._thread_pool_manager = Mock(spec=ThreadPoolManager) self._an_open_partition = Mock(spec=Partition) - self._an_open_partition.is_closed.return_value = False self._log_message = Mock(spec=LogMessage) self._an_open_partition.to_slice.return_value = self._log_message self._an_open_partition.stream_name.return_value = _STREAM_NAME self._a_closed_partition = Mock(spec=Partition) - self._a_closed_partition.is_closed.return_value = True self._a_closed_partition.stream_name.return_value = _ANOTHER_STREAM_NAME self._logger = Mock(spec=logging.Logger) @@ -76,6 +74,19 @@ def setUp(self): self._record.stream_name = _STREAM_NAME self._record.data = self._record_data + def test_stream_is_not_done_initially(self): + stream_instances_to_read_from = [self._stream] + handler = ConcurrentReadProcessor( + stream_instances_to_read_from, + self._partition_enqueuer, + self._thread_pool_manager, + self._logger, + self._slice_logger, + self._message_repository, + self._partition_reader, + ) + assert not handler._is_stream_done(self._stream.name) + def test_handle_partition_done_no_other_streams_to_generate_partitions_for(self): stream_instances_to_read_from = [self._stream] @@ -111,7 +122,6 @@ def test_handle_last_stream_partition_done(self): self._partition_reader, ) handler.start_next_partition_generator() - handler.on_partition(self._a_closed_partition) sentinel = PartitionGenerationCompletedSentinel(self._another_stream) messages = handler.on_partition_generation_completed(sentinel) @@ -147,7 +157,7 @@ def test_handle_partition(self): handler.on_partition(self._a_closed_partition) self._thread_pool_manager.submit.assert_called_with(self._partition_reader.process_partition, self._a_closed_partition) - assert self._a_closed_partition in handler._streams_to_partitions[_ANOTHER_STREAM_NAME] + assert self._a_closed_partition in handler._streams_to_running_partitions[_ANOTHER_STREAM_NAME] def test_handle_partition_emits_log_message_if_it_should_be_logged(self): stream_instances_to_read_from = [self._stream] @@ -169,15 +179,16 @@ def test_handle_partition_emits_log_message_if_it_should_be_logged(self): self._thread_pool_manager.submit.assert_called_with(self._partition_reader.process_partition, self._an_open_partition) self._message_repository.emit_message.assert_called_with(self._log_message) - assert self._an_open_partition in handler._streams_to_partitions[_STREAM_NAME] + assert self._an_open_partition in handler._streams_to_running_partitions[_STREAM_NAME] + + @freezegun.freeze_time("2020-01-01T00:00:00") def test_handle_on_partition_complete_sentinel_with_messages_from_repository(self): stream_instances_to_read_from = [self._stream] partition = Mock(spec=Partition) log_message = Mock(spec=LogMessage) partition.to_slice.return_value = log_message partition.stream_name.return_value = _STREAM_NAME - partition.is_closed.return_value = True handler = ConcurrentReadProcessor( stream_instances_to_read_from, @@ -189,6 +200,7 @@ def test_handle_on_partition_complete_sentinel_with_messages_from_repository(sel self._partition_reader, ) handler.start_next_partition_generator() + handler.on_partition(partition) sentinel = PartitionCompleteSentinel(partition) @@ -223,6 +235,7 @@ def test_handle_on_partition_complete_sentinel_yields_status_message_if_the_stre self._partition_reader, ) handler.start_next_partition_generator() + handler.on_partition(self._a_closed_partition) handler.on_partition_generation_completed(PartitionGenerationCompletedSentinel(self._another_stream)) sentinel = PartitionCompleteSentinel(self._a_closed_partition) @@ -254,7 +267,6 @@ def test_handle_on_partition_complete_sentinel_yields_no_status_message_if_the_s log_message = Mock(spec=LogMessage) partition.to_slice.return_value = log_message partition.stream_name.return_value = _STREAM_NAME - partition.is_closed.return_value = True handler = ConcurrentReadProcessor( stream_instances_to_read_from, @@ -282,7 +294,6 @@ def test_on_record_no_status_message_no_repository_messge(self): log_message = Mock(spec=LogMessage) partition.to_slice.return_value = log_message partition.stream_name.return_value = _STREAM_NAME - partition.is_closed.return_value = True self._message_repository.consume_queue.return_value = [] handler = ConcurrentReadProcessor( @@ -319,7 +330,6 @@ def test_on_record_with_repository_messge(self): log_message = Mock(spec=LogMessage) partition.to_slice.return_value = log_message partition.stream_name.return_value = _STREAM_NAME - partition.is_closed.return_value = True slice_logger = Mock(spec=SliceLogger) slice_logger.should_log_slice_message.return_value = True slice_logger.create_slice_log_message.return_value = log_message @@ -370,7 +380,6 @@ def test_on_record_emits_status_message_on_first_record_no_repository_message(se stream_instances_to_read_from = [self._stream] partition = Mock(spec=Partition) partition.stream_name.return_value = _STREAM_NAME - partition.is_closed.return_value = True handler = ConcurrentReadProcessor( stream_instances_to_read_from, @@ -413,7 +422,6 @@ def test_on_record_emits_status_message_on_first_record_with_repository_message( log_message = Mock(spec=LogMessage) partition.to_slice.return_value = log_message partition.stream_name.return_value = _STREAM_NAME - partition.is_closed.return_value = True self._message_repository.consume_queue.return_value = [ AirbyteMessage(type=MessageType.LOG, log=AirbyteLogMessage(level=LogLevel.INFO, message="message emitted from the repository")) ] @@ -474,7 +482,69 @@ def test_on_exception_stops_streams_and_raises_an_exception(self): self._message_repository, self._partition_reader, ) - handler._streams_to_partitions = {_STREAM_NAME: {self._an_open_partition}, _ANOTHER_STREAM_NAME: {self._a_closed_partition}} + + handler.start_next_partition_generator() + + another_stream = Mock(spec=AbstractStream) + another_stream.name = _STREAM_NAME + another_stream.as_airbyte_stream.return_value = AirbyteStream( + name=_ANOTHER_STREAM_NAME, + json_schema={}, + supported_sync_modes=[SyncMode.full_refresh], + ) + + exception = RuntimeError("Something went wrong") + + messages = [] + + with self.assertRaises(RuntimeError): + for m in handler.on_exception(exception): + messages.append(m) + + expected_message = [ + AirbyteMessage( + type=MessageType.TRACE, + trace=AirbyteTraceMessage( + type=TraceType.STREAM_STATUS, + emitted_at=1577836800000.0, + stream_status=AirbyteStreamStatusTraceMessage( + stream_descriptor=StreamDescriptor(name=_STREAM_NAME), status=AirbyteStreamStatus(AirbyteStreamStatus.INCOMPLETE) + ), + ), + ), + AirbyteMessage( + type=MessageType.TRACE, + trace=AirbyteTraceMessage( + type=TraceType.STREAM_STATUS, + emitted_at=1577836800000.0, + stream_status=AirbyteStreamStatusTraceMessage( + stream_descriptor=StreamDescriptor(name=_ANOTHER_STREAM_NAME), status=AirbyteStreamStatus(AirbyteStreamStatus.INCOMPLETE) + ), + ), + ) + ] + + assert messages == expected_message + self._thread_pool_manager.shutdown.assert_called_once() + + @freezegun.freeze_time("2020-01-01T00:00:00") + def test_on_exception_does_not_stop_streams_that_are_already_done(self): + stream_instances_to_read_from = [self._stream, self._another_stream] + + handler = ConcurrentReadProcessor( + stream_instances_to_read_from, + self._partition_enqueuer, + self._thread_pool_manager, + self._logger, + self._slice_logger, + self._message_repository, + self._partition_reader, + ) + + handler.start_next_partition_generator() + handler.on_partition(self._an_open_partition) + handler.on_partition_generation_completed(PartitionGenerationCompletedSentinel(self._stream)) + handler.on_partition_generation_completed(PartitionGenerationCompletedSentinel(self._another_stream)) another_stream = Mock(spec=AbstractStream) another_stream.name = _STREAM_NAME diff --git a/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_thread_pool_manager.py b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_thread_pool_manager.py index 12caaceba2d9d..db950f33f79ce 100644 --- a/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_thread_pool_manager.py +++ b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_thread_pool_manager.py @@ -3,7 +3,7 @@ # from concurrent.futures import Future, ThreadPoolExecutor from unittest import TestCase -from unittest.mock import Mock, patch +from unittest.mock import Mock from airbyte_cdk.sources.concurrent_source.thread_pool_manager import ThreadPoolManager @@ -23,23 +23,10 @@ def test_submit_calls_underlying_thread_pool(self): assert len(self._thread_pool_manager._futures) == 1 - def test_submit_too_many_concurrent_tasks(self): - future = Mock(spec=Future) - future.exception.return_value = None - future.done.side_effect = [False, True] - - with patch("time.sleep") as sleep_mock: - self._thread_pool_manager._futures = [future] - self._thread_pool_manager.submit(self._fn, self._arg) - self._threadpool.submit.assert_called_with(self._fn, self._arg) - sleep_mock.assert_called_with(_SLEEP_TIME) - - assert len(self._thread_pool_manager._futures) == 1 - def test_submit_task_previous_task_failed(self): future = Mock(spec=Future) future.exception.return_value = RuntimeError - future.done.side_effect = [False, True] + future.done.side_effect = [True, True] self._thread_pool_manager._futures = [future] diff --git a/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_throttled_queue.py b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_throttled_queue.py new file mode 100644 index 0000000000000..33e4c9f0a0587 --- /dev/null +++ b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_throttled_queue.py @@ -0,0 +1,65 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from queue import Queue +from unittest.mock import Mock + +import pytest +from _queue import Empty +from airbyte_cdk.sources.concurrent_source.throttler import Throttler +from airbyte_cdk.sources.streams.concurrent.partitions.throttled_queue import ThrottledQueue + +_AN_ITEM = Mock() + + +def test_new_throttled_queue_is_empty(): + queue = Queue() + throttler = Mock(spec=Throttler) + timeout = 100 + throttled_queue = ThrottledQueue(queue, throttler, timeout) + + assert throttled_queue.empty() + + +def test_throttled_queue_is_not_empty_after_putting_an_item(): + queue = Queue() + throttler = Mock(spec=Throttler) + timeout = 100 + throttled_queue = ThrottledQueue(queue, throttler, timeout) + + throttled_queue.put(_AN_ITEM) + + assert not throttled_queue.empty() + + +def test_throttled_queue_get_returns_item_if_any(): + queue = Queue() + throttler = Mock(spec=Throttler) + timeout = 100 + throttled_queue = ThrottledQueue(queue, throttler, timeout) + + throttled_queue.put(_AN_ITEM) + item = throttled_queue.get() + + assert item == _AN_ITEM + assert throttled_queue.empty() + + +def test_throttled_queue_blocks_for_timeout_seconds_if_no_items(): + queue = Mock(spec=Queue) + throttler = Mock(spec=Throttler) + timeout = 100 + throttled_queue = ThrottledQueue(queue, throttler, timeout) + + throttled_queue.get() + + assert queue.get.is_called_once_with(block=True, timeout=timeout) + + +def test_throttled_queue_raises_an_error_if_no_items_after_timeout(): + queue = Queue() + throttler = Mock(spec=Throttler) + timeout = 0.001 + throttled_queue = ThrottledQueue(queue, throttler, timeout) + + with pytest.raises(Empty): + throttled_queue.get() diff --git a/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_throttler.py b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_throttler.py new file mode 100644 index 0000000000000..fbe006771244a --- /dev/null +++ b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_throttler.py @@ -0,0 +1,13 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from unittest.mock import patch + +from airbyte_cdk.sources.concurrent_source.throttler import Throttler + + +@patch('time.sleep', side_effect=lambda _: None) +@patch('airbyte_cdk.sources.concurrent_source.throttler.len', side_effect=[1, 1, 0]) +def test_throttler(sleep_mock, len_mock): + throttler = Throttler([], 0.1, 1) + throttler.wait_and_acquire() + assert sleep_mock.call_count == 3 From ef785c70198c4cc2a56e2f90457f6222ca770eb2 Mon Sep 17 00:00:00 2001 From: girarda Date: Thu, 18 Jan 2024 08:01:19 +0000 Subject: [PATCH 136/574] =?UTF-8?q?=F0=9F=A4=96=20Bump=20patch=20version?= =?UTF-8?q?=20of=20Python=20CDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airbyte-cdk/python/.bumpversion.cfg | 2 +- airbyte-cdk/python/CHANGELOG.md | 3 +++ airbyte-cdk/python/Dockerfile | 2 +- airbyte-cdk/python/setup.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/airbyte-cdk/python/.bumpversion.cfg b/airbyte-cdk/python/.bumpversion.cfg index df8a9afee1000..55b6678f0508c 100644 --- a/airbyte-cdk/python/.bumpversion.cfg +++ b/airbyte-cdk/python/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.58.8 +current_version = 0.58.9 commit = False [bumpversion:file:setup.py] diff --git a/airbyte-cdk/python/CHANGELOG.md b/airbyte-cdk/python/CHANGELOG.md index 223e8b50e29a3..7570b34ceb628 100644 --- a/airbyte-cdk/python/CHANGELOG.md +++ b/airbyte-cdk/python/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 0.58.9 +concurrent-cdk: improve resource usage when reading from substreams + ## 0.58.8 CDK: HttpRequester can accept http_method in str format, which is required by custom low code components diff --git a/airbyte-cdk/python/Dockerfile b/airbyte-cdk/python/Dockerfile index 7494661031ee0..4d6ce8c3684e4 100644 --- a/airbyte-cdk/python/Dockerfile +++ b/airbyte-cdk/python/Dockerfile @@ -32,5 +32,5 @@ ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] # needs to be the same as CDK -LABEL io.airbyte.version=0.58.8 +LABEL io.airbyte.version=0.58.9 LABEL io.airbyte.name=airbyte/source-declarative-manifest diff --git a/airbyte-cdk/python/setup.py b/airbyte-cdk/python/setup.py index a5b870e690510..627bd437c36a5 100644 --- a/airbyte-cdk/python/setup.py +++ b/airbyte-cdk/python/setup.py @@ -36,7 +36,7 @@ name="airbyte-cdk", # The version of the airbyte-cdk package is used at runtime to validate manifests. That validation must be # updated if our semver format changes such as using release candidate versions. - version="0.58.8", + version="0.58.9", description="A framework for writing Airbyte Connectors.", long_description=README, long_description_content_type="text/markdown", From 9b4ae62ee1f9f8faa9597f30e0de6f248380bdf3 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 18 Jan 2024 14:57:49 +0100 Subject: [PATCH 137/574] airbyte-ci: Pass env vars to poetry container in test command (#34288) --- airbyte-ci/connectors/pipelines/README.md | 5 +++++ .../pipelines/airbyte_ci/test/commands.py | 22 +++++++++++++++++++ .../connectors/pipelines/pyproject.toml | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/airbyte-ci/connectors/pipelines/README.md b/airbyte-ci/connectors/pipelines/README.md index 4c8a85289eaa4..ab36fa02335d9 100644 --- a/airbyte-ci/connectors/pipelines/README.md +++ b/airbyte-ci/connectors/pipelines/README.md @@ -528,6 +528,7 @@ This command runs the Python tests for a airbyte-ci poetry package. | Option | Required | Default | Mapped environment variable | Description | | ------------------------- | -------- | ------- | --------------------------- | ------------------------------------------------------------------------------------------- | | `-c/--poetry-run-command` | True | None | | The command to run with `poetry run` | +| `-e/--pass-env-var` | False | None | | Host environment variable that is passed to the container running the poetry command | | `--ci-requirements` | False | | | Output the CI requirements as a JSON payload. It is used to determine the CI runner to use. | #### Examples @@ -536,6 +537,9 @@ You can pass multiple `-c/--poetry-run-command` options to run multiple commands E.G.: running `pytest` and `mypy`: `airbyte-ci test airbyte-ci/connectors/pipelines --poetry-run-command='pytest tests' --poetry-run-command='mypy pipelines'` +E.G.: passing the environment variable `GCP_GSM_CREDENTIALS` environment variable to the container running the poetry command: +`airbyte-ci test airbyte-lib --pass-env-var='GCP_GSM_CREDENTIALS'` + E.G.: running `pytest` on a specific test folder: `airbyte-ci tests airbyte-integrations/bases/connector-acceptance-test --poetry-run-command='pytest tests/unit_tests'` @@ -543,6 +547,7 @@ E.G.: running `pytest` on a specific test folder: | Version | PR | Description | | ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| 3.5.0 | [#33313](https://github.com/airbytehq/airbyte/pull/33313) | Pass extra params after Gradle tasks. | | 3.4.2 | [#34301](https://github.com/airbytehq/airbyte/pull/34301) | Pass extra params after Gradle tasks. | | 3.4.1 | [#34067](https://github.com/airbytehq/airbyte/pull/34067) | Use dagster-cloud 1.5.7 for deploy | | 3.4.0 | [#34276](https://github.com/airbytehq/airbyte/pull/34276) | Introduce `--only-step` option for connector tests. | diff --git a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/test/commands.py b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/test/commands.py index 7bf140211e78f..4dc24ae0a1974 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/test/commands.py +++ b/airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/test/commands.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging +import os from pathlib import Path from typing import TYPE_CHECKING @@ -34,6 +35,13 @@ async def run_poetry_command(container: dagger.Container, command: str) -> Tuple return await container.stdout(), await container.stderr() +def validate_env_vars_exist(_ctx: dict, _param: dict, value: List[str]) -> List[str]: + for var in value: + if var not in os.environ: + raise click.BadParameter(f"Environment variable {var} does not exist.") + return value + + @click.command() @click.argument("poetry_package_path") @click_ci_requirements_option() @@ -44,6 +52,15 @@ async def run_poetry_command(container: dagger.Container, command: str) -> Tuple help="The poetry run command to run.", required=True, ) +@click.option( + "--pass-env-var", + "-e", + "passed_env_vars", + multiple=True, + help="The environment variables to pass to the container.", + required=False, + callback=validate_env_vars_exist, +) @click_merge_args_into_context_obj @pass_pipeline_context @click_ignore_unused_kwargs @@ -112,6 +129,11 @@ async def test(pipeline_context: ClickPipelineContext) -> None: .with_workdir(f"/airbyte/{poetry_package_path}") ) + # register passed env vars as secrets and add them to the container + for var in pipeline_context.params["passed_env_vars"]: + secret = dagger_client.set_secret(var, os.environ[var]) + test_container = test_container.with_secret_variable(var, secret) + soon_command_executions_results = [] async with asyncer.create_task_group() as poetry_commands_task_group: for command in commands_to_run: diff --git a/airbyte-ci/connectors/pipelines/pyproject.toml b/airbyte-ci/connectors/pipelines/pyproject.toml index 79f37a9efd606..31dba6ae56cab 100644 --- a/airbyte-ci/connectors/pipelines/pyproject.toml +++ b/airbyte-ci/connectors/pipelines/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pipelines" -version = "3.4.2" +version = "3.5.0" description = "Packaged maintained by the connector operations team to perform CI for connectors' pipelines" authors = ["Airbyte "] From c939d739f642f90e6ab93f952436e78eea1d07c3 Mon Sep 17 00:00:00 2001 From: Erica D'Souza <93952107+erica-airbyte@users.noreply.github.com> Date: Thu, 18 Jan 2024 08:20:31 -0700 Subject: [PATCH 138/574] Update getting-support.md --- docs/community/getting-support.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/community/getting-support.md b/docs/community/getting-support.md index af2f9453faab2..339bd08399c41 100644 --- a/docs/community/getting-support.md +++ b/docs/community/getting-support.md @@ -22,13 +22,6 @@ If you require personalized support, reach out to our sales team to inquire abou We are driving our community support from our [forum](https://github.com/airbytehq/airbyte/discussions) on GitHub. -### Office Hour - -Airbyte provides a [Daily Office Hour](https://airbyte.com/daily-office-hour) to discuss issues. -It is a 45 minute meeting, the first 20 minutes are reserved to a weekly topic presentation about Airbyte concepts and the others 25 minutes are for general questions. The schedule is: -* Monday, Wednesday and Fridays: 1 PM PST/PDT -* Tuesday and Thursday: 4 PM CEST - ## Airbyte Cloud Support From 4e694c59856bf72ed34928f7212e06cfddb3c56a Mon Sep 17 00:00:00 2001 From: Anton Karpets Date: Thu, 18 Jan 2024 17:50:42 +0200 Subject: [PATCH 139/574] =?UTF-8?q?=F0=9F=90=9BSource=20Google=20Analytics?= =?UTF-8?q?:=20add=20incorrect=20custom=20reports=20config=20handling=20(#?= =?UTF-8?q?34352)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../metadata.yaml | 2 +- .../source.py | 22 ++++++++--- .../source_google_analytics_data_api/utils.py | 1 + .../unit_tests/test_source.py | 37 ++++++++++++++++--- .../sources/google-analytics-data-api.md | 1 + 5 files changed, 52 insertions(+), 11 deletions(-) diff --git a/airbyte-integrations/connectors/source-google-analytics-data-api/metadata.yaml b/airbyte-integrations/connectors/source-google-analytics-data-api/metadata.yaml index 7570e71c94f9a..d91736309fd97 100644 --- a/airbyte-integrations/connectors/source-google-analytics-data-api/metadata.yaml +++ b/airbyte-integrations/connectors/source-google-analytics-data-api/metadata.yaml @@ -12,7 +12,7 @@ data: connectorSubtype: api connectorType: source definitionId: 3cc2eafd-84aa-4dca-93af-322d9dfeec1a - dockerImageTag: 2.2.0 + dockerImageTag: 2.2.1 dockerRepository: airbyte/source-google-analytics-data-api documentationUrl: https://docs.airbyte.com/integrations/sources/google-analytics-data-api githubIssueLabel: source-google-analytics-data-api diff --git a/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/source.py b/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/source.py index 2418186d271b9..d58bb1673ac78 100644 --- a/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/source.py +++ b/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/source.py @@ -22,7 +22,13 @@ from airbyte_cdk.utils import AirbyteTracedException from requests import HTTPError from source_google_analytics_data_api import utils -from source_google_analytics_data_api.utils import DATE_FORMAT, WRONG_DIMENSIONS, WRONG_JSON_SYNTAX, WRONG_METRICS +from source_google_analytics_data_api.utils import ( + DATE_FORMAT, + WRONG_CUSTOM_REPORT_CONFIG, + WRONG_DIMENSIONS, + WRONG_JSON_SYNTAX, + WRONG_METRICS, +) from .api_quota import GoogleAnalyticsApiQuota from .utils import ( @@ -37,8 +43,8 @@ transform_json, ) -# set the quota handler globaly since limitations are the same for all streams -# the initial values should be saved once and tracked for each stream, inclusivelly. +# set the quota handler globally since limitations are the same for all streams +# the initial values should be saved once and tracked for each stream, inclusively. GoogleAnalyticsQuotaHandler: GoogleAnalyticsApiQuota = GoogleAnalyticsApiQuota() LOOKBACK_WINDOW = datetime.timedelta(days=2) @@ -520,8 +526,14 @@ def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> report_stream = self.instantiate_report_class(report, False, _config, page_size=100) # check if custom_report dimensions + metrics can be combined and report generated - stream_slice = next(report_stream.stream_slices(sync_mode=SyncMode.full_refresh)) - next(report_stream.read_records(sync_mode=SyncMode.full_refresh, stream_slice=stream_slice), None) + try: + stream_slice = next(report_stream.stream_slices(sync_mode=SyncMode.full_refresh)) + next(report_stream.read_records(sync_mode=SyncMode.full_refresh, stream_slice=stream_slice), None) + except HTTPError as e: + error_response = "" + if e.response.status_code == HTTPStatus.BAD_REQUEST: + error_response = e.response.json().get("error", {}).get("message", "") + return False, WRONG_CUSTOM_REPORT_CONFIG.format(report=report["name"], error_response=error_response) return True, None diff --git a/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/utils.py b/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/utils.py index f40cdf08da64b..5a77a16f9a32f 100644 --- a/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/utils.py +++ b/airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/utils.py @@ -71,6 +71,7 @@ WRONG_METRICS = "The custom report {report_name} entered contains invalid metrics: {fields}. Validate your custom query with the GA 4 Query Explorer (https://ga-dev-tools.google/ga4/query-explorer/)." WRONG_PIVOTS = "The custom report {report_name} entered contains invalid pivots: {fields}. Ensure the pivot follow the syntax described in the docs (https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/Pivot)." API_LIMIT_PER_HOUR = "Your API key has reached its limit for the hour. Wait until the quota refreshes in an hour to retry." +WRONG_CUSTOM_REPORT_CONFIG = "Please check configuration for custom report {report}. {error_response}" def datetime_to_secs(dt: datetime.datetime) -> int: diff --git a/airbyte-integrations/connectors/source-google-analytics-data-api/unit_tests/test_source.py b/airbyte-integrations/connectors/source-google-analytics-data-api/unit_tests/test_source.py index 8d8460cacf50f..631b2a1d8683f 100644 --- a/airbyte-integrations/connectors/source-google-analytics-data-api/unit_tests/test_source.py +++ b/airbyte-integrations/connectors/source-google-analytics-data-api/unit_tests/test_source.py @@ -107,12 +107,39 @@ def test_check_failure(requests_mock, config_gen): @pytest.mark.parametrize( - "status_code", - [ - (403), - (401), - ], + ("status_code", "expected_message"), + ( + (403, "Please check configuration for custom report cohort_report. "), + (400, "Please check configuration for custom report cohort_report. Granularity in the cohortsRange is required."), + ), ) +def test_check_incorrect_custom_reports_config(requests_mock, config_gen, status_code, expected_message): + requests_mock.register_uri( + "POST", "https://oauth2.googleapis.com/token", json={"access_token": "access_token", "expires_in": 3600, "token_type": "Bearer"} + ) + requests_mock.register_uri( + "GET", + "https://analyticsdata.googleapis.com/v1beta/properties/108176369/metadata", + json={ + "dimensions": [{"apiName": "date"}, {"apiName": "country"}, {"apiName": "language"}, {"apiName": "browser"}], + "metrics": [{"apiName": "totalUsers"}, {"apiName": "screenPageViews"}, {"apiName": "sessions"}], + }, + ) + requests_mock.register_uri( + "POST", + "https://analyticsdata.googleapis.com/v1beta/properties/108176369:runReport", + status_code=status_code, + json={"error": {"message": "Granularity in the cohortsRange is required."}}, + ) + config = {"custom_reports_array": '[{"name": "cohort_report", "dimensions": ["date"], "metrics": ["totalUsers"]}]'} + source = SourceGoogleAnalyticsDataApi() + logger = MagicMock() + status, message = source.check_connection(logger, config_gen(**config)) + assert status is False + assert message == expected_message + + +@pytest.mark.parametrize("status_code", (403, 401)) def test_missing_metadata(requests_mock, status_code): # required for MetadataDescriptor $instance input class TestConfig: diff --git a/docs/integrations/sources/google-analytics-data-api.md b/docs/integrations/sources/google-analytics-data-api.md index 5cac246e905a0..17b18cc3a4604 100644 --- a/docs/integrations/sources/google-analytics-data-api.md +++ b/docs/integrations/sources/google-analytics-data-api.md @@ -264,6 +264,7 @@ The Google Analytics connector is subject to Google Analytics Data API quotas. P | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:--------------------------------------------------------------------------------| +| 2.2.1 | 2024-01-18 | [34352](https://github.com/airbytehq/airbyte/pull/34352) | Add incorrect custom reports config handling | | 2.2.0 | 2024-01-10 | [34176](https://github.com/airbytehq/airbyte/pull/34176) | Add a report option keepEmptyRows | | 2.1.1 | 2024-01-08 | [34018](https://github.com/airbytehq/airbyte/pull/34018) | prepare for airbyte-lib | | 2.1.0 | 2023-12-28 | [33802](https://github.com/airbytehq/airbyte/pull/33802) | Add `CohortSpec` to custom report in specification | From 5f351870108d990ed1fd63a8b1e8248bb8914ee5 Mon Sep 17 00:00:00 2001 From: Augustin Date: Thu, 18 Jan 2024 17:16:27 +0100 Subject: [PATCH 140/574] airbyte-ci: upgrade to dagger 0.9.6 (#34321) --- airbyte-ci/connectors/base_images/poetry.lock | 142 ++++++---- .../connectors/base_images/pyproject.toml | 4 +- airbyte-ci/connectors/pipelines/README.md | 3 +- .../pipelines/models/ci_requirements.py | 1 + airbyte-ci/connectors/pipelines/poetry.lock | 181 ++++++------ .../connectors/pipelines/pyproject.toml | 4 +- .../connector-acceptance-test/CHANGELOG.md | 5 +- .../connector-acceptance-test/poetry.lock | 258 +++++++++--------- .../connector-acceptance-test/pyproject.toml | 4 +- 9 files changed, 334 insertions(+), 268 deletions(-) diff --git a/airbyte-ci/connectors/base_images/poetry.lock b/airbyte-ci/connectors/base_images/poetry.lock index ca32fc4675fbf..44a8b475dca2a 100644 --- a/airbyte-ci/connectors/base_images/poetry.lock +++ b/airbyte-ci/connectors/base_images/poetry.lock @@ -514,13 +514,13 @@ test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.0)" [[package]] name = "dagger-io" -version = "0.9.5" +version = "0.9.6" description = "A client package for running Dagger pipelines in Python." optional = false python-versions = ">=3.10" files = [ - {file = "dagger_io-0.9.5-py3-none-any.whl", hash = "sha256:e7f075997af3eaef742bab9ad7c862c92c0965029679805ae12ab7a613d97fd1"}, - {file = "dagger_io-0.9.5.tar.gz", hash = "sha256:90593bb6a419f64d3c468adbbffbb907a1dd14eed9b852b65ff60a14da0b37a8"}, + {file = "dagger_io-0.9.6-py3-none-any.whl", hash = "sha256:e2f1e4bbc252071a314fa5b0bad11a910433a9ee043972b716f6fcc5f9fc8236"}, + {file = "dagger_io-0.9.6.tar.gz", hash = "sha256:147b5a33c44d17f602a4121679893655e91308beb8c46a466afed39cf40f789b"}, ] [package.dependencies] @@ -551,6 +551,21 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] +[[package]] +name = "editor" +version = "1.6.5" +description = "🖋 Open the default text editor 🖋" +optional = false +python-versions = ">=3.8" +files = [ + {file = "editor-1.6.5-py3-none-any.whl", hash = "sha256:53c26dd78333b50b8cdcf67748956afa75fabcb5bb25e96a00515504f58e49a8"}, + {file = "editor-1.6.5.tar.gz", hash = "sha256:5a8ad611d2a05de34994df3781605e26e63492f82f04c2e93abdd330eed6fa8d"}, +] + +[package.dependencies] +runs = "*" +xmod = "*" + [[package]] name = "exceptiongroup" version = "1.2.0" @@ -581,20 +596,20 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.40" +version = "3.1.41" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.40-py3-none-any.whl", hash = "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a"}, - {file = "GitPython-3.1.40.tar.gz", hash = "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4"}, + {file = "GitPython-3.1.41-py3-none-any.whl", hash = "sha256:c36b6634d069b3f719610175020a9aed919421c87552185b085e04fbbdb10b7c"}, + {file = "GitPython-3.1.41.tar.gz", hash = "sha256:ed66e624884f76df22c8e16066d567aaa5a37d5b5fa19db2c6df6f7156db9048"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-sugar"] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "sumtypes"] [[package]] name = "google-api-core" @@ -620,13 +635,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-auth" -version = "2.26.0" +version = "2.26.2" description = "Google Authentication Library" optional = false python-versions = ">=3.7" files = [ - {file = "google-auth-2.26.0.tar.gz", hash = "sha256:5d8bf0a5143baa45368c3d08bf157babda468db1c5dd987cd2c824b788524196"}, - {file = "google_auth-2.26.0-py2.py3-none-any.whl", hash = "sha256:11f56129d30902cc9f4b93ed0c84ef7323f7328e6520eab1716740f3171afe35"}, + {file = "google-auth-2.26.2.tar.gz", hash = "sha256:97327dbbf58cccb58fc5a1712bba403ae76668e64814eb30f7316f7e27126b81"}, + {file = "google_auth-2.26.2-py2.py3-none-any.whl", hash = "sha256:3f445c8ce9b61ed6459aad86d8ccdba4a9afed841b2d1451a11ef4db08957424"}, ] [package.dependencies] @@ -798,29 +813,31 @@ grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] [[package]] name = "gql" -version = "3.4.1" +version = "3.5.0" description = "GraphQL client for Python" optional = false python-versions = "*" files = [ - {file = "gql-3.4.1-py2.py3-none-any.whl", hash = "sha256:315624ca0f4d571ef149d455033ebd35e45c1a13f18a059596aeddcea99135cf"}, - {file = "gql-3.4.1.tar.gz", hash = "sha256:11dc5d8715a827f2c2899593439a4f36449db4f0eafa5b1ea63948f8a2f8c545"}, + {file = "gql-3.5.0-py2.py3-none-any.whl", hash = "sha256:70dda5694a5b194a8441f077aa5fb70cc94e4ec08016117523f013680901ecb7"}, + {file = "gql-3.5.0.tar.gz", hash = "sha256:ccb9c5db543682b28f577069950488218ed65d4ac70bb03b6929aaadaf636de9"}, ] [package.dependencies] +anyio = ">=3.0,<5" backoff = ">=1.11.1,<3.0" graphql-core = ">=3.2,<3.3" yarl = ">=1.6,<2.0" [package.extras] -aiohttp = ["aiohttp (>=3.7.1,<3.9.0)"] -all = ["aiohttp (>=3.7.1,<3.9.0)", "botocore (>=1.21,<2)", "requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26,<2)", "websockets (>=10,<11)", "websockets (>=9,<10)"] +aiohttp = ["aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)"] +all = ["aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "botocore (>=1.21,<2)", "httpx (>=0.23.1,<1)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "websockets (>=10,<12)"] botocore = ["botocore (>=1.21,<2)"] -dev = ["aiofiles", "aiohttp (>=3.7.1,<3.9.0)", "black (==22.3.0)", "botocore (>=1.21,<2)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "isort (==4.3.21)", "mock (==4.0.2)", "mypy (==0.910)", "parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "sphinx (>=3.0.0,<4)", "sphinx-argparse (==0.2.5)", "sphinx-rtd-theme (>=0.4,<1)", "types-aiofiles", "types-mock", "types-requests", "urllib3 (>=1.26,<2)", "vcrpy (==4.0.2)", "websockets (>=10,<11)", "websockets (>=9,<10)"] -requests = ["requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26,<2)"] -test = ["aiofiles", "aiohttp (>=3.7.1,<3.9.0)", "botocore (>=1.21,<2)", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26,<2)", "vcrpy (==4.0.2)", "websockets (>=10,<11)", "websockets (>=9,<10)"] -test-no-transport = ["aiofiles", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "vcrpy (==4.0.2)"] -websockets = ["websockets (>=10,<11)", "websockets (>=9,<10)"] +dev = ["aiofiles", "aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "black (==22.3.0)", "botocore (>=1.21,<2)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "httpx (>=0.23.1,<1)", "isort (==4.3.21)", "mock (==4.0.2)", "mypy (==0.910)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "sphinx (>=5.3.0,<6)", "sphinx-argparse (==0.2.5)", "sphinx-rtd-theme (>=0.4,<1)", "types-aiofiles", "types-mock", "types-requests", "vcrpy (==4.4.0)", "websockets (>=10,<12)"] +httpx = ["httpx (>=0.23.1,<1)"] +requests = ["requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)"] +test = ["aiofiles", "aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "botocore (>=1.21,<2)", "httpx (>=0.23.1,<1)", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "vcrpy (==4.4.0)", "websockets (>=10,<12)"] +test-no-transport = ["aiofiles", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "vcrpy (==4.4.0)"] +websockets = ["websockets (>=10,<12)"] [[package]] name = "graphql-core" @@ -913,29 +930,29 @@ files = [ [[package]] name = "inquirer" -version = "3.1.4" +version = "3.2.1" description = "Collection of common interactive command line user interfaces, based on Inquirer.js" optional = false -python-versions = ">=3.8" +python-versions = ">=3.8.1" files = [ - {file = "inquirer-3.1.4-py3-none-any.whl", hash = "sha256:8ca28834b6c6f69e0bf19cab2e2bea2c465312bb74bd6317b88a46458163a051"}, - {file = "inquirer-3.1.4.tar.gz", hash = "sha256:958dbd5978f173630756a6ed6243acf931e750416eb7a6ed3a0ff13af0fdfcb5"}, + {file = "inquirer-3.2.1-py3-none-any.whl", hash = "sha256:e1a0a001b499633ca69d2ea64da712b449939e8fad8fa47caebc92b0ee212df4"}, + {file = "inquirer-3.2.1.tar.gz", hash = "sha256:d5ff9bb8cd07bd3f076eabad8ae338280886e93998ff10461975b768e3854fbc"}, ] [package.dependencies] blessed = ">=1.19.0" -python-editor = ">=1.0.4" +editor = ">=1.6.0" readchar = ">=3.0.6" [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.3" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [package.dependencies] @@ -1359,22 +1376,22 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "protobuf" -version = "4.25.1" +version = "4.25.2" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "protobuf-4.25.1-cp310-abi3-win32.whl", hash = "sha256:193f50a6ab78a970c9b4f148e7c750cfde64f59815e86f686c22e26b4fe01ce7"}, - {file = "protobuf-4.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:3497c1af9f2526962f09329fd61a36566305e6c72da2590ae0d7d1322818843b"}, - {file = "protobuf-4.25.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:0bf384e75b92c42830c0a679b0cd4d6e2b36ae0cf3dbb1e1dfdda48a244f4bcd"}, - {file = "protobuf-4.25.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:0f881b589ff449bf0b931a711926e9ddaad3b35089cc039ce1af50b21a4ae8cb"}, - {file = "protobuf-4.25.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:ca37bf6a6d0046272c152eea90d2e4ef34593aaa32e8873fc14c16440f22d4b7"}, - {file = "protobuf-4.25.1-cp38-cp38-win32.whl", hash = "sha256:abc0525ae2689a8000837729eef7883b9391cd6aa7950249dcf5a4ede230d5dd"}, - {file = "protobuf-4.25.1-cp38-cp38-win_amd64.whl", hash = "sha256:1484f9e692091450e7edf418c939e15bfc8fc68856e36ce399aed6889dae8bb0"}, - {file = "protobuf-4.25.1-cp39-cp39-win32.whl", hash = "sha256:8bdbeaddaac52d15c6dce38c71b03038ef7772b977847eb6d374fc86636fa510"}, - {file = "protobuf-4.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:becc576b7e6b553d22cbdf418686ee4daa443d7217999125c045ad56322dda10"}, - {file = "protobuf-4.25.1-py3-none-any.whl", hash = "sha256:a19731d5e83ae4737bb2a089605e636077ac001d18781b3cf489b9546c7c80d6"}, - {file = "protobuf-4.25.1.tar.gz", hash = "sha256:57d65074b4f5baa4ab5da1605c02be90ac20c8b40fb137d6a8df9f416b0d0ce2"}, + {file = "protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6"}, + {file = "protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9"}, + {file = "protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d"}, + {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62"}, + {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020"}, + {file = "protobuf-4.25.2-cp38-cp38-win32.whl", hash = "sha256:33a1aeef4b1927431d1be780e87b641e322b88d654203a9e9d93f218ee359e61"}, + {file = "protobuf-4.25.2-cp38-cp38-win_amd64.whl", hash = "sha256:47f3de503fe7c1245f6f03bea7e8d3ec11c6c4a2ea9ef910e3221c8a15516d62"}, + {file = "protobuf-4.25.2-cp39-cp39-win32.whl", hash = "sha256:5e5c933b4c30a988b52e0b7c02641760a5ba046edc5e43d3b94a74c9fc57c1b3"}, + {file = "protobuf-4.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:d66a769b8d687df9024f2985d5137a337f957a0916cf5464d1513eee96a63ff0"}, + {file = "protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830"}, + {file = "protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e"}, ] [[package]] @@ -1644,18 +1661,6 @@ files = [ [package.dependencies] six = ">=1.5" -[[package]] -name = "python-editor" -version = "1.0.4" -description = "Programmatically open an editor, capture the result." -optional = false -python-versions = "*" -files = [ - {file = "python-editor-1.0.4.tar.gz", hash = "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b"}, - {file = "python_editor-1.0.4-py2-none-any.whl", hash = "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"}, - {file = "python_editor-1.0.4-py3-none-any.whl", hash = "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d"}, -] - [[package]] name = "pytz" version = "2023.3.post1" @@ -1793,6 +1798,20 @@ files = [ [package.dependencies] pyasn1 = ">=0.1.3" +[[package]] +name = "runs" +version = "1.2.0" +description = "🏃 Run a block of text as a subprocess 🏃" +optional = false +python-versions = ">=3.8" +files = [ + {file = "runs-1.2.0-py3-none-any.whl", hash = "sha256:ec6fe3b24dfa20c5c4e5c4806d3b35bb880aad0e787a8610913c665c5a7cc07c"}, + {file = "runs-1.2.0.tar.gz", hash = "sha256:8804271011b7a2eeb0d77c3e3f556e5ce5f602fa0dd2a31ed0c1222893be69b7"}, +] + +[package.dependencies] +xmod = "*" + [[package]] name = "semver" version = "3.0.2" @@ -1940,13 +1959,13 @@ toml = "*" [[package]] name = "wcwidth" -version = "0.2.12" +version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" files = [ - {file = "wcwidth-0.2.12-py2.py3-none-any.whl", hash = "sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"}, - {file = "wcwidth-0.2.12.tar.gz", hash = "sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02"}, + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] [[package]] @@ -2028,6 +2047,17 @@ files = [ {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] +[[package]] +name = "xmod" +version = "1.8.1" +description = "🌱 Turn any object into a module 🌱" +optional = false +python-versions = ">=3.8" +files = [ + {file = "xmod-1.8.1-py3-none-any.whl", hash = "sha256:a24e9458a4853489042522bdca9e50ee2eac5ab75c809a91150a8a7f40670d48"}, + {file = "xmod-1.8.1.tar.gz", hash = "sha256:38c76486b9d672c546d57d8035df0beb7f4a9b088bc3fb2de5431ae821444377"}, +] + [[package]] name = "yarl" version = "1.9.4" @@ -2134,4 +2164,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c582f0ceda4586ff7be1c1ba91a6f0eef7f9103da4483e0d164cba9685a41416" +content-hash = "e6f67b753371bdbe515e2326b68d32e46a492722b13a8b32a2636fe1e0c39028" diff --git a/airbyte-ci/connectors/base_images/pyproject.toml b/airbyte-ci/connectors/base_images/pyproject.toml index 439c74c0f2c63..206408e58c2d8 100644 --- a/airbyte-ci/connectors/base_images/pyproject.toml +++ b/airbyte-ci/connectors/base_images/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "airbyte-connectors-base-images" -version = "1.0.0" +version = "1.0.1" description = "This package is used to generate and publish the base images for Airbyte Connectors." authors = ["Augustin Lafanechere "] readme = "README.md" @@ -8,7 +8,7 @@ packages = [{include = "base_images"}] include = ["generated"] [tool.poetry.dependencies] python = "^3.10" -dagger-io = "==0.9.5" +dagger-io = "==0.9.6" gitpython = "^3.1.35" rich = "^13.5.2" semver = "^3.0.1" diff --git a/airbyte-ci/connectors/pipelines/README.md b/airbyte-ci/connectors/pipelines/README.md index ab36fa02335d9..c979b0e8c0f95 100644 --- a/airbyte-ci/connectors/pipelines/README.md +++ b/airbyte-ci/connectors/pipelines/README.md @@ -547,9 +547,10 @@ E.G.: running `pytest` on a specific test folder: | Version | PR | Description | | ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| 3.5.1 | [#34321](https://github.com/airbytehq/airbyte/pull/34321) | Upgrade to Dagger 0.9.6 . | | 3.5.0 | [#33313](https://github.com/airbytehq/airbyte/pull/33313) | Pass extra params after Gradle tasks. | | 3.4.2 | [#34301](https://github.com/airbytehq/airbyte/pull/34301) | Pass extra params after Gradle tasks. | -| 3.4.1 | [#34067](https://github.com/airbytehq/airbyte/pull/34067) | Use dagster-cloud 1.5.7 for deploy | +| 3.4.1 | [#34067](https://github.com/airbytehq/airbyte/pull/34067) | Use dagster-cloud 1.5.7 for deploy | | 3.4.0 | [#34276](https://github.com/airbytehq/airbyte/pull/34276) | Introduce `--only-step` option for connector tests. | | 3.3.0 | [#34218](https://github.com/airbytehq/airbyte/pull/34218) | Introduce `--ci-requirements` option for client defined CI runners. | | 3.2.0 | [#34050](https://github.com/airbytehq/airbyte/pull/34050) | Connector test steps can take extra parameters | diff --git a/airbyte-ci/connectors/pipelines/pipelines/models/ci_requirements.py b/airbyte-ci/connectors/pipelines/pipelines/models/ci_requirements.py index 7eb8a9157b573..fce2dce671241 100644 --- a/airbyte-ci/connectors/pipelines/pipelines/models/ci_requirements.py +++ b/airbyte-ci/connectors/pipelines/pipelines/models/ci_requirements.py @@ -7,6 +7,7 @@ INFRA_SUPPORTED_DAGGER_VERSIONS = { "0.6.4", "0.9.5", + "0.9.6", } diff --git a/airbyte-ci/connectors/pipelines/poetry.lock b/airbyte-ci/connectors/pipelines/poetry.lock index a1e1a4468abe6..e97b0e920b728 100644 --- a/airbyte-ci/connectors/pipelines/poetry.lock +++ b/airbyte-ci/connectors/pipelines/poetry.lock @@ -2,7 +2,7 @@ [[package]] name = "airbyte-connectors-base-images" -version = "0.1.2" +version = "1.0.1" description = "This package is used to generate and publish the base images for Airbyte Connectors." optional = false python-versions = "^3.10" @@ -11,7 +11,7 @@ develop = true [package.dependencies] connector-ops = {path = "../connector_ops", develop = true} -dagger-io = "==0.9.5" +dagger-io = "==0.9.6" gitpython = "^3.1.35" inquirer = "^3.1.3" jinja2 = "^3.1.2" @@ -24,13 +24,13 @@ url = "../base_images" [[package]] name = "airbyte-protocol-models" -version = "0.5.2" +version = "0.5.3" description = "Declares the Airbyte Protocol." optional = false python-versions = ">=3.8" files = [ - {file = "airbyte_protocol_models-0.5.2-py3-none-any.whl", hash = "sha256:73ca1064bb2cd6d2ae525a69f3e3375c759323d7caf7574d87d934d8b5e58c13"}, - {file = "airbyte_protocol_models-0.5.2.tar.gz", hash = "sha256:c33ab77e1c3a04276c3525c7223843c203dc452ac3ff805155c6870c57bb270f"}, + {file = "airbyte_protocol_models-0.5.3-py3-none-any.whl", hash = "sha256:a913f1e86d5b2ae17d19e0135339e55fc25bb93bfc3f7ab38592677f29b56c57"}, + {file = "airbyte_protocol_models-0.5.3.tar.gz", hash = "sha256:a71bc0e98e0722d5cbd3122c40a59a7f9cbc91b6c934db7e768a57c40546f54b"}, ] [package.dependencies] @@ -81,16 +81,17 @@ trio = ["trio (<0.22)"] [[package]] name = "asyncclick" -version = "8.1.3.4" +version = "8.1.7.1" description = "Composable command line interface toolkit, async version" optional = false python-versions = ">=3.7" files = [ - {file = "asyncclick-8.1.3.4-py3-none-any.whl", hash = "sha256:f8db604e37dabd43922d58f857817b1dfd8f88695b75c4cc1afe7ff1cc238a7b"}, - {file = "asyncclick-8.1.3.4.tar.gz", hash = "sha256:81d98cbf6c8813f9cd5599f586d56cfc532e9e6441391974d10827abb90fe833"}, + {file = "asyncclick-8.1.7.1-py3-none-any.whl", hash = "sha256:e0fea5f0223ac45cfc26153cc80a58cc65fc077ac8de79be49248c918e8c3422"}, + {file = "asyncclick-8.1.7.1.tar.gz", hash = "sha256:a47b61258a689212cf9463fbf3b4cc52d05bfd03185f6ead2315fc03fd17ef75"}, ] [package.dependencies] +anyio = "*" colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] @@ -588,13 +589,13 @@ test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.0)" [[package]] name = "dagger-io" -version = "0.9.5" +version = "0.9.6" description = "A client package for running Dagger pipelines in Python." optional = false python-versions = ">=3.10" files = [ - {file = "dagger_io-0.9.5-py3-none-any.whl", hash = "sha256:e7f075997af3eaef742bab9ad7c862c92c0965029679805ae12ab7a613d97fd1"}, - {file = "dagger_io-0.9.5.tar.gz", hash = "sha256:90593bb6a419f64d3c468adbbffbb907a1dd14eed9b852b65ff60a14da0b37a8"}, + {file = "dagger_io-0.9.6-py3-none-any.whl", hash = "sha256:e2f1e4bbc252071a314fa5b0bad11a910433a9ee043972b716f6fcc5f9fc8236"}, + {file = "dagger_io-0.9.6.tar.gz", hash = "sha256:147b5a33c44d17f602a4121679893655e91308beb8c46a466afed39cf40f789b"}, ] [package.dependencies] @@ -646,6 +647,21 @@ websocket-client = ">=0.32.0" [package.extras] ssh = ["paramiko (>=2.4.3)"] +[[package]] +name = "editor" +version = "1.6.5" +description = "🖋 Open the default text editor 🖋" +optional = false +python-versions = ">=3.8" +files = [ + {file = "editor-1.6.5-py3-none-any.whl", hash = "sha256:53c26dd78333b50b8cdcf67748956afa75fabcb5bb25e96a00515504f58e49a8"}, + {file = "editor-1.6.5.tar.gz", hash = "sha256:5a8ad611d2a05de34994df3781605e26e63492f82f04c2e93abdd330eed6fa8d"}, +] + +[package.dependencies] +runs = "*" +xmod = "*" + [[package]] name = "exceptiongroup" version = "1.2.0" @@ -690,20 +706,20 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.40" +version = "3.1.41" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.40-py3-none-any.whl", hash = "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a"}, - {file = "GitPython-3.1.40.tar.gz", hash = "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4"}, + {file = "GitPython-3.1.41-py3-none-any.whl", hash = "sha256:c36b6634d069b3f719610175020a9aed919421c87552185b085e04fbbdb10b7c"}, + {file = "GitPython-3.1.41.tar.gz", hash = "sha256:ed66e624884f76df22c8e16066d567aaa5a37d5b5fa19db2c6df6f7156db9048"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-sugar"] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "sumtypes"] [[package]] name = "google-api-core" @@ -729,13 +745,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-auth" -version = "2.26.1" +version = "2.26.2" description = "Google Authentication Library" optional = false python-versions = ">=3.7" files = [ - {file = "google-auth-2.26.1.tar.gz", hash = "sha256:54385acca5c0fbdda510cd8585ba6f3fcb06eeecf8a6ecca39d3ee148b092590"}, - {file = "google_auth-2.26.1-py2.py3-none-any.whl", hash = "sha256:2c8b55e3e564f298122a02ab7b97458ccfcc5617840beb5d0ac757ada92c9780"}, + {file = "google-auth-2.26.2.tar.gz", hash = "sha256:97327dbbf58cccb58fc5a1712bba403ae76668e64814eb30f7316f7e27126b81"}, + {file = "google_auth-2.26.2-py2.py3-none-any.whl", hash = "sha256:3f445c8ce9b61ed6459aad86d8ccdba4a9afed841b2d1451a11ef4db08957424"}, ] [package.dependencies] @@ -1024,29 +1040,29 @@ files = [ [[package]] name = "inquirer" -version = "3.1.4" +version = "3.2.1" description = "Collection of common interactive command line user interfaces, based on Inquirer.js" optional = false -python-versions = ">=3.8" +python-versions = ">=3.8.1" files = [ - {file = "inquirer-3.1.4-py3-none-any.whl", hash = "sha256:8ca28834b6c6f69e0bf19cab2e2bea2c465312bb74bd6317b88a46458163a051"}, - {file = "inquirer-3.1.4.tar.gz", hash = "sha256:958dbd5978f173630756a6ed6243acf931e750416eb7a6ed3a0ff13af0fdfcb5"}, + {file = "inquirer-3.2.1-py3-none-any.whl", hash = "sha256:e1a0a001b499633ca69d2ea64da712b449939e8fad8fa47caebc92b0ee212df4"}, + {file = "inquirer-3.2.1.tar.gz", hash = "sha256:d5ff9bb8cd07bd3f076eabad8ae338280886e93998ff10461975b768e3854fbc"}, ] [package.dependencies] blessed = ">=1.19.0" -python-editor = ">=1.0.4" +editor = ">=1.6.0" readchar = ">=3.0.6" [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.3" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [package.dependencies] @@ -1542,22 +1558,22 @@ poetry-plugin = ["poetry (>=1.0,<2.0)"] [[package]] name = "protobuf" -version = "4.25.1" +version = "4.25.2" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "protobuf-4.25.1-cp310-abi3-win32.whl", hash = "sha256:193f50a6ab78a970c9b4f148e7c750cfde64f59815e86f686c22e26b4fe01ce7"}, - {file = "protobuf-4.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:3497c1af9f2526962f09329fd61a36566305e6c72da2590ae0d7d1322818843b"}, - {file = "protobuf-4.25.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:0bf384e75b92c42830c0a679b0cd4d6e2b36ae0cf3dbb1e1dfdda48a244f4bcd"}, - {file = "protobuf-4.25.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:0f881b589ff449bf0b931a711926e9ddaad3b35089cc039ce1af50b21a4ae8cb"}, - {file = "protobuf-4.25.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:ca37bf6a6d0046272c152eea90d2e4ef34593aaa32e8873fc14c16440f22d4b7"}, - {file = "protobuf-4.25.1-cp38-cp38-win32.whl", hash = "sha256:abc0525ae2689a8000837729eef7883b9391cd6aa7950249dcf5a4ede230d5dd"}, - {file = "protobuf-4.25.1-cp38-cp38-win_amd64.whl", hash = "sha256:1484f9e692091450e7edf418c939e15bfc8fc68856e36ce399aed6889dae8bb0"}, - {file = "protobuf-4.25.1-cp39-cp39-win32.whl", hash = "sha256:8bdbeaddaac52d15c6dce38c71b03038ef7772b977847eb6d374fc86636fa510"}, - {file = "protobuf-4.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:becc576b7e6b553d22cbdf418686ee4daa443d7217999125c045ad56322dda10"}, - {file = "protobuf-4.25.1-py3-none-any.whl", hash = "sha256:a19731d5e83ae4737bb2a089605e636077ac001d18781b3cf489b9546c7c80d6"}, - {file = "protobuf-4.25.1.tar.gz", hash = "sha256:57d65074b4f5baa4ab5da1605c02be90ac20c8b40fb137d6a8df9f416b0d0ce2"}, + {file = "protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6"}, + {file = "protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9"}, + {file = "protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d"}, + {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62"}, + {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020"}, + {file = "protobuf-4.25.2-cp38-cp38-win32.whl", hash = "sha256:33a1aeef4b1927431d1be780e87b641e322b88d654203a9e9d93f218ee359e61"}, + {file = "protobuf-4.25.2-cp38-cp38-win_amd64.whl", hash = "sha256:47f3de503fe7c1245f6f03bea7e8d3ec11c6c4a2ea9ef910e3221c8a15516d62"}, + {file = "protobuf-4.25.2-cp39-cp39-win32.whl", hash = "sha256:5e5c933b4c30a988b52e0b7c02641760a5ba046edc5e43d3b94a74c9fc57c1b3"}, + {file = "protobuf-4.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:d66a769b8d687df9024f2985d5137a337f957a0916cf5464d1513eee96a63ff0"}, + {file = "protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830"}, + {file = "protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e"}, ] [[package]] @@ -1914,18 +1930,6 @@ files = [ [package.dependencies] six = ">=1.5" -[[package]] -name = "python-editor" -version = "1.0.4" -description = "Programmatically open an editor, capture the result." -optional = false -python-versions = "*" -files = [ - {file = "python-editor-1.0.4.tar.gz", hash = "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b"}, - {file = "python_editor-1.0.4-py2-none-any.whl", hash = "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"}, - {file = "python_editor-1.0.4-py3-none-any.whl", hash = "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d"}, -] - [[package]] name = "pytz" version = "2023.3.post1" @@ -2099,30 +2103,44 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruff" -version = "0.1.11" +version = "0.1.13" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a7f772696b4cdc0a3b2e527fc3c7ccc41cdcb98f5c80fdd4f2b8c50eb1458196"}, - {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:934832f6ed9b34a7d5feea58972635c2039c7a3b434fe5ba2ce015064cb6e955"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea0d3e950e394c4b332bcdd112aa566010a9f9c95814844a7468325290aabfd9"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bd4025b9c5b429a48280785a2b71d479798a69f5c2919e7d274c5f4b32c3607"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ad00662305dcb1e987f5ec214d31f7d6a062cae3e74c1cbccef15afd96611d"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4b077ce83f47dd6bea1991af08b140e8b8339f0ba8cb9b7a484c30ebab18a23f"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a88efecec23c37b11076fe676e15c6cdb1271a38f2b415e381e87fe4517f18"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b25093dad3b055667730a9b491129c42d45e11cdb7043b702e97125bcec48a1"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231d8fb11b2cc7c0366a326a66dafc6ad449d7fcdbc268497ee47e1334f66f77"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:09c415716884950080921dd6237767e52e227e397e2008e2bed410117679975b"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f58948c6d212a6b8d41cd59e349751018797ce1727f961c2fa755ad6208ba45"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:190a566c8f766c37074d99640cd9ca3da11d8deae2deae7c9505e68a4a30f740"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6464289bd67b2344d2a5d9158d5eb81025258f169e69a46b741b396ffb0cda95"}, - {file = "ruff-0.1.11-py3-none-win32.whl", hash = "sha256:9b8f397902f92bc2e70fb6bebfa2139008dc72ae5177e66c383fa5426cb0bf2c"}, - {file = "ruff-0.1.11-py3-none-win_amd64.whl", hash = "sha256:eb85ee287b11f901037a6683b2374bb0ec82928c5cbc984f575d0437979c521a"}, - {file = "ruff-0.1.11-py3-none-win_arm64.whl", hash = "sha256:97ce4d752f964ba559c7023a86e5f8e97f026d511e48013987623915431c7ea9"}, - {file = "ruff-0.1.11.tar.gz", hash = "sha256:f9d4d88cb6eeb4dfe20f9f0519bd2eaba8119bde87c3d5065c541dbae2b5a2cb"}, + {file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e3fd36e0d48aeac672aa850045e784673449ce619afc12823ea7868fcc41d8ba"}, + {file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9fb6b3b86450d4ec6a6732f9f60c4406061b6851c4b29f944f8c9d91c3611c7a"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b13ba5d7156daaf3fd08b6b993360a96060500aca7e307d95ecbc5bb47a69296"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ebb40442f7b531e136d334ef0851412410061e65d61ca8ce90d894a094feb22"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226b517f42d59a543d6383cfe03cccf0091e3e0ed1b856c6824be03d2a75d3b6"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f0312ba1061e9b8c724e9a702d3c8621e3c6e6c2c9bd862550ab2951ac75c16"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f59bcf5217c661254bd6bc42d65a6fd1a8b80c48763cb5c2293295babd945dd"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6894b00495e00c27b6ba61af1fc666f17de6140345e5ef27dd6e08fb987259d"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1600942485c6e66119da294c6294856b5c86fd6df591ce293e4a4cc8e72989"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ee3febce7863e231a467f90e681d3d89210b900d49ce88723ce052c8761be8c7"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dcaab50e278ff497ee4d1fe69b29ca0a9a47cd954bb17963628fa417933c6eb1"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f57de973de4edef3ad3044d6a50c02ad9fc2dff0d88587f25f1a48e3f72edf5e"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a36fa90eb12208272a858475ec43ac811ac37e91ef868759770b71bdabe27b6"}, + {file = "ruff-0.1.13-py3-none-win32.whl", hash = "sha256:a623349a505ff768dad6bd57087e2461be8db58305ebd5577bd0e98631f9ae69"}, + {file = "ruff-0.1.13-py3-none-win_amd64.whl", hash = "sha256:f988746e3c3982bea7f824c8fa318ce7f538c4dfefec99cd09c8770bd33e6539"}, + {file = "ruff-0.1.13-py3-none-win_arm64.whl", hash = "sha256:6bbbc3042075871ec17f28864808540a26f0f79a4478c357d3e3d2284e832998"}, + {file = "ruff-0.1.13.tar.gz", hash = "sha256:e261f1baed6291f434ffb1d5c6bd8051d1c2a26958072d38dfbec39b3dda7352"}, +] + +[[package]] +name = "runs" +version = "1.2.0" +description = "🏃 Run a block of text as a subprocess 🏃" +optional = false +python-versions = ">=3.8" +files = [ + {file = "runs-1.2.0-py3-none-any.whl", hash = "sha256:ec6fe3b24dfa20c5c4e5c4806d3b35bb880aad0e787a8610913c665c5a7cc07c"}, + {file = "runs-1.2.0.tar.gz", hash = "sha256:8804271011b7a2eeb0d77c3e3f556e5ce5f602fa0dd2a31ed0c1222893be69b7"}, ] +[package.dependencies] +xmod = "*" + [[package]] name = "segment-analytics-python" version = "2.2.3" @@ -2156,13 +2174,13 @@ files = [ [[package]] name = "sentry-sdk" -version = "1.39.1" +version = "1.39.2" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = "*" files = [ - {file = "sentry-sdk-1.39.1.tar.gz", hash = "sha256:320a55cdf9da9097a0bead239c35b7e61f53660ef9878861824fd6d9b2eaf3b5"}, - {file = "sentry_sdk-1.39.1-py2.py3-none-any.whl", hash = "sha256:81b5b9ffdd1a374e9eb0c053b5d2012155db9cbe76393a8585677b753bd5fdc1"}, + {file = "sentry-sdk-1.39.2.tar.gz", hash = "sha256:24c83b0b41c887d33328a9166f5950dc37ad58f01c9f2fbff6b87a6f1094170c"}, + {file = "sentry_sdk-1.39.2-py2.py3-none-any.whl", hash = "sha256:acaf597b30258fc7663063b291aa99e58f3096e91fe1e6634f4b79f9c1943e8e"}, ] [package.dependencies] @@ -2188,7 +2206,7 @@ huey = ["huey (>=2)"] loguru = ["loguru (>=0.5)"] opentelemetry = ["opentelemetry-distro (>=0.35b0)"] opentelemetry-experimental = ["opentelemetry-distro (>=0.40b0,<1.0)", "opentelemetry-instrumentation-aiohttp-client (>=0.40b0,<1.0)", "opentelemetry-instrumentation-django (>=0.40b0,<1.0)", "opentelemetry-instrumentation-fastapi (>=0.40b0,<1.0)", "opentelemetry-instrumentation-flask (>=0.40b0,<1.0)", "opentelemetry-instrumentation-requests (>=0.40b0,<1.0)", "opentelemetry-instrumentation-sqlite3 (>=0.40b0,<1.0)", "opentelemetry-instrumentation-urllib (>=0.40b0,<1.0)"] -pure-eval = ["asttokens", "executing", "pure-eval"] +pure-eval = ["asttokens", "executing", "pure_eval"] pymongo = ["pymongo (>=3.1)"] pyspark = ["pyspark (>=2.4.4)"] quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] @@ -2321,13 +2339,13 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "wcwidth" -version = "0.2.12" +version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" files = [ - {file = "wcwidth-0.2.12-py2.py3-none-any.whl", hash = "sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"}, - {file = "wcwidth-0.2.12.tar.gz", hash = "sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02"}, + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] [[package]] @@ -2425,6 +2443,17 @@ files = [ {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] +[[package]] +name = "xmod" +version = "1.8.1" +description = "🌱 Turn any object into a module 🌱" +optional = false +python-versions = ">=3.8" +files = [ + {file = "xmod-1.8.1-py3-none-any.whl", hash = "sha256:a24e9458a4853489042522bdca9e50ee2eac5ab75c809a91150a8a7f40670d48"}, + {file = "xmod-1.8.1.tar.gz", hash = "sha256:38c76486b9d672c546d57d8035df0beb7f4a9b088bc3fb2de5431ae821444377"}, +] + [[package]] name = "yarl" version = "1.9.4" @@ -2531,4 +2560,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "~3.10" -content-hash = "c24e2d2a6c1288b7fe7efa1c0ca598dfef8942bb437986512bbe17bcf1408799" +content-hash = "0c7f7c9e18637d2cf9402f22c71502916cd4a1938111dd78eb7874f2c061c1fe" diff --git a/airbyte-ci/connectors/pipelines/pyproject.toml b/airbyte-ci/connectors/pipelines/pyproject.toml index 31dba6ae56cab..ce6db7b3942e9 100644 --- a/airbyte-ci/connectors/pipelines/pyproject.toml +++ b/airbyte-ci/connectors/pipelines/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pipelines" -version = "3.5.0" +version = "3.5.1" description = "Packaged maintained by the connector operations team to perform CI for connectors' pipelines" authors = ["Airbyte "] [tool.poetry.dependencies] python = "~3.10" -dagger-io = "==0.9.5" +dagger-io = "==0.9.6" asyncer = "^0.0.2" anyio = "^3.4.1" more-itertools = "^8.11.0" diff --git a/airbyte-integrations/bases/connector-acceptance-test/CHANGELOG.md b/airbyte-integrations/bases/connector-acceptance-test/CHANGELOG.md index 4eea85a592a25..407a1754e01c3 100644 --- a/airbyte-integrations/bases/connector-acceptance-test/CHANGELOG.md +++ b/airbyte-integrations/bases/connector-acceptance-test/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog -## 2.3.0 +## 3.0.1 +Upgrade to Dagger 0.9.6 + +## 3.0.0 Upgrade to Dagger 0.9.5 ## 2.2.0 diff --git a/airbyte-integrations/bases/connector-acceptance-test/poetry.lock b/airbyte-integrations/bases/connector-acceptance-test/poetry.lock index 46e2fdc46d492..fff2f1561b59f 100644 --- a/airbyte-integrations/bases/connector-acceptance-test/poetry.lock +++ b/airbyte-integrations/bases/connector-acceptance-test/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "airbyte-protocol-models" -version = "0.5.2" +version = "0.5.3" description = "Declares the Airbyte Protocol." optional = false python-versions = ">=3.8" files = [ - {file = "airbyte_protocol_models-0.5.2-py3-none-any.whl", hash = "sha256:73ca1064bb2cd6d2ae525a69f3e3375c759323d7caf7574d87d934d8b5e58c13"}, - {file = "airbyte_protocol_models-0.5.2.tar.gz", hash = "sha256:c33ab77e1c3a04276c3525c7223843c203dc452ac3ff805155c6870c57bb270f"}, + {file = "airbyte_protocol_models-0.5.3-py3-none-any.whl", hash = "sha256:a913f1e86d5b2ae17d19e0135339e55fc25bb93bfc3f7ab38592677f29b56c57"}, + {file = "airbyte_protocol_models-0.5.3.tar.gz", hash = "sha256:a71bc0e98e0722d5cbd3122c40a59a7f9cbc91b6c934db7e768a57c40546f54b"}, ] [package.dependencies] @@ -308,13 +308,13 @@ toml = ["tomli"] [[package]] name = "dagger-io" -version = "0.9.5" +version = "0.9.6" description = "A client package for running Dagger pipelines in Python." optional = false python-versions = ">=3.10" files = [ - {file = "dagger_io-0.9.5-py3-none-any.whl", hash = "sha256:e7f075997af3eaef742bab9ad7c862c92c0965029679805ae12ab7a613d97fd1"}, - {file = "dagger_io-0.9.5.tar.gz", hash = "sha256:90593bb6a419f64d3c468adbbffbb907a1dd14eed9b852b65ff60a14da0b37a8"}, + {file = "dagger_io-0.9.6-py3-none-any.whl", hash = "sha256:e2f1e4bbc252071a314fa5b0bad11a910433a9ee043972b716f6fcc5f9fc8236"}, + {file = "dagger_io-0.9.6.tar.gz", hash = "sha256:147b5a33c44d17f602a4121679893655e91308beb8c46a466afed39cf40f789b"}, ] [package.dependencies] @@ -422,29 +422,31 @@ pyrepl = ">=0.8.2" [[package]] name = "gql" -version = "3.4.1" +version = "3.5.0" description = "GraphQL client for Python" optional = false python-versions = "*" files = [ - {file = "gql-3.4.1-py2.py3-none-any.whl", hash = "sha256:315624ca0f4d571ef149d455033ebd35e45c1a13f18a059596aeddcea99135cf"}, - {file = "gql-3.4.1.tar.gz", hash = "sha256:11dc5d8715a827f2c2899593439a4f36449db4f0eafa5b1ea63948f8a2f8c545"}, + {file = "gql-3.5.0-py2.py3-none-any.whl", hash = "sha256:70dda5694a5b194a8441f077aa5fb70cc94e4ec08016117523f013680901ecb7"}, + {file = "gql-3.5.0.tar.gz", hash = "sha256:ccb9c5db543682b28f577069950488218ed65d4ac70bb03b6929aaadaf636de9"}, ] [package.dependencies] +anyio = ">=3.0,<5" backoff = ">=1.11.1,<3.0" graphql-core = ">=3.2,<3.3" yarl = ">=1.6,<2.0" [package.extras] -aiohttp = ["aiohttp (>=3.7.1,<3.9.0)"] -all = ["aiohttp (>=3.7.1,<3.9.0)", "botocore (>=1.21,<2)", "requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26,<2)", "websockets (>=10,<11)", "websockets (>=9,<10)"] +aiohttp = ["aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)"] +all = ["aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "botocore (>=1.21,<2)", "httpx (>=0.23.1,<1)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "websockets (>=10,<12)"] botocore = ["botocore (>=1.21,<2)"] -dev = ["aiofiles", "aiohttp (>=3.7.1,<3.9.0)", "black (==22.3.0)", "botocore (>=1.21,<2)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "isort (==4.3.21)", "mock (==4.0.2)", "mypy (==0.910)", "parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "sphinx (>=3.0.0,<4)", "sphinx-argparse (==0.2.5)", "sphinx-rtd-theme (>=0.4,<1)", "types-aiofiles", "types-mock", "types-requests", "urllib3 (>=1.26,<2)", "vcrpy (==4.0.2)", "websockets (>=10,<11)", "websockets (>=9,<10)"] -requests = ["requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26,<2)"] -test = ["aiofiles", "aiohttp (>=3.7.1,<3.9.0)", "botocore (>=1.21,<2)", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26,<2)", "vcrpy (==4.0.2)", "websockets (>=10,<11)", "websockets (>=9,<10)"] -test-no-transport = ["aiofiles", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "vcrpy (==4.0.2)"] -websockets = ["websockets (>=10,<11)", "websockets (>=9,<10)"] +dev = ["aiofiles", "aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "black (==22.3.0)", "botocore (>=1.21,<2)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "httpx (>=0.23.1,<1)", "isort (==4.3.21)", "mock (==4.0.2)", "mypy (==0.910)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "sphinx (>=5.3.0,<6)", "sphinx-argparse (==0.2.5)", "sphinx-rtd-theme (>=0.4,<1)", "types-aiofiles", "types-mock", "types-requests", "vcrpy (==4.4.0)", "websockets (>=10,<12)"] +httpx = ["httpx (>=0.23.1,<1)"] +requests = ["requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)"] +test = ["aiofiles", "aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "botocore (>=1.21,<2)", "httpx (>=0.23.1,<1)", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "vcrpy (==4.4.0)", "websockets (>=10,<12)"] +test-no-transport = ["aiofiles", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "vcrpy (==4.4.0)"] +websockets = ["websockets (>=10,<12)"] [[package]] name = "graphql-core" @@ -515,13 +517,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "hypothesis" -version = "6.92.2" +version = "6.96.0" description = "A library for property-based testing" optional = false python-versions = ">=3.8" files = [ - {file = "hypothesis-6.92.2-py3-none-any.whl", hash = "sha256:d335044492acb03fa1fdb4edacb81cca2e578049fc7306345bc0e8947fef15a9"}, - {file = "hypothesis-6.92.2.tar.gz", hash = "sha256:841f89a486c43bdab55698de8929bd2635639ec20bf6ce98ccd75622d7ee6d41"}, + {file = "hypothesis-6.96.0-py3-none-any.whl", hash = "sha256:ec8e0348844e1a9368aeaf85dbea1d247f93f5f865fdf65801bc578b4608cc08"}, + {file = "hypothesis-6.96.0.tar.gz", hash = "sha256:fec50dcbc54ec5884a4199d723543ba9408bbab940cc3ab849a92fe1fab97625"}, ] [package.dependencies] @@ -530,7 +532,7 @@ exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2023.3)"] +all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2023.4)"] cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] codemods = ["libcst (>=0.3.16)"] dateutil = ["python-dateutil (>=1.4)"] @@ -543,7 +545,7 @@ pandas = ["pandas (>=1.1)"] pytest = ["pytest (>=4.6)"] pytz = ["pytz (>=2014.1)"] redis = ["redis (>=3.0.0)"] -zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2023.3)"] +zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2023.4)"] [[package]] name = "hypothesis-jsonschema" @@ -616,13 +618,13 @@ files = [ [[package]] name = "jsonschema" -version = "4.20.0" +version = "4.21.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" files = [ - {file = "jsonschema-4.20.0-py3-none-any.whl", hash = "sha256:ed6231f0429ecf966f5bc8dfef245998220549cbbcf140f913b7464c52c3b6b3"}, - {file = "jsonschema-4.20.0.tar.gz", hash = "sha256:4f614fd46d8d61258610998997743ec5492a648b33cf478c1ddc23ed4598a5fa"}, + {file = "jsonschema-4.21.0-py3-none-any.whl", hash = "sha256:70a09719d375c0a2874571b363c8a24be7df8071b80c9aa76bc4551e7297c63c"}, + {file = "jsonschema-4.21.0.tar.gz", hash = "sha256:3ba18e27f7491ea4a1b22edce00fb820eec968d397feb3f9cb61d5894bb38167"}, ] [package.dependencies] @@ -1260,13 +1262,13 @@ files = [ [[package]] name = "referencing" -version = "0.32.0" +version = "0.32.1" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" files = [ - {file = "referencing-0.32.0-py3-none-any.whl", hash = "sha256:bdcd3efb936f82ff86f993093f6da7435c7de69a3b3a5a06678a6050184bee99"}, - {file = "referencing-0.32.0.tar.gz", hash = "sha256:689e64fe121843dcfd57b71933318ef1f91188ffb45367332700a86ac8fd6161"}, + {file = "referencing-0.32.1-py3-none-any.whl", hash = "sha256:7e4dc12271d8e15612bfe35792f5ea1c40970dadf8624602e33db2758f7ee554"}, + {file = "referencing-0.32.1.tar.gz", hash = "sha256:3c57da0513e9563eb7e203ebe9bb3a1b509b042016433bd1e45a2853466c3dd3"}, ] [package.dependencies] @@ -1333,110 +1335,110 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.16.2" +version = "0.17.1" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" files = [ - {file = "rpds_py-0.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:509b617ac787cd1149600e731db9274ebbef094503ca25158e6f23edaba1ca8f"}, - {file = "rpds_py-0.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:413b9c17388bbd0d87a329d8e30c1a4c6e44e2bb25457f43725a8e6fe4161e9e"}, - {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2946b120718eba9af2b4dd103affc1164a87b9e9ebff8c3e4c05d7b7a7e274e2"}, - {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:35ae5ece284cf36464eb160880018cf6088a9ac5ddc72292a6092b6ef3f4da53"}, - {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc6a7620ba7639a3db6213da61312cb4aa9ac0ca6e00dc1cbbdc21c2aa6eb57"}, - {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8cb6fe8ecdfffa0e711a75c931fb39f4ba382b4b3ccedeca43f18693864fe850"}, - {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dace7b26a13353e24613417ce2239491b40a6ad44e5776a18eaff7733488b44"}, - {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bdbc5fcb04a7309074de6b67fa9bc4b418ab3fc435fec1f2779a0eced688d04"}, - {file = "rpds_py-0.16.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f42e25c016927e2a6b1ce748112c3ab134261fc2ddc867e92d02006103e1b1b7"}, - {file = "rpds_py-0.16.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eab36eae3f3e8e24b05748ec9acc66286662f5d25c52ad70cadab544e034536b"}, - {file = "rpds_py-0.16.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0474df4ade9a3b4af96c3d36eb81856cb9462e4c6657d4caecfd840d2a13f3c9"}, - {file = "rpds_py-0.16.2-cp310-none-win32.whl", hash = "sha256:84c5a4d1f9dd7e2d2c44097fb09fffe728629bad31eb56caf97719e55575aa82"}, - {file = "rpds_py-0.16.2-cp310-none-win_amd64.whl", hash = "sha256:2bd82db36cd70b3628c0c57d81d2438e8dd4b7b32a6a9f25f24ab0e657cb6c4e"}, - {file = "rpds_py-0.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:adc0c3d6fc6ae35fee3e4917628983f6ce630d513cbaad575b4517d47e81b4bb"}, - {file = "rpds_py-0.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ec23fcad480e77ede06cf4127a25fc440f7489922e17fc058f426b5256ee0edb"}, - {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07aab64e2808c3ebac2a44f67e9dc0543812b715126dfd6fe4264df527556cb6"}, - {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a4ebb8b20bd09c5ce7884c8f0388801100f5e75e7f733b1b6613c713371feefc"}, - {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3d7e2ea25d3517c6d7e5a1cc3702cffa6bd18d9ef8d08d9af6717fc1c700eed"}, - {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f28ac0e8e7242d140f99402a903a2c596ab71550272ae9247ad78f9a932b5698"}, - {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19f00f57fdd38db4bb5ad09f9ead1b535332dbf624200e9029a45f1f35527ebb"}, - {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3da5a4c56953bdbf6d04447c3410309616c54433146ccdb4a277b9cb499bc10e"}, - {file = "rpds_py-0.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec2e1cf025b2c0f48ec17ff3e642661da7ee332d326f2e6619366ce8e221f018"}, - {file = "rpds_py-0.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e0441fb4fdd39a230477b2ca9be90868af64425bfe7b122b57e61e45737a653b"}, - {file = "rpds_py-0.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9f0350ef2fba5f34eb0c9000ea328e51b9572b403d2f7f3b19f24085f6f598e8"}, - {file = "rpds_py-0.16.2-cp311-none-win32.whl", hash = "sha256:5a80e2f83391ad0808b4646732af2a7b67550b98f0cae056cb3b40622a83dbb3"}, - {file = "rpds_py-0.16.2-cp311-none-win_amd64.whl", hash = "sha256:e04e56b4ca7a770593633556e8e9e46579d66ec2ada846b401252a2bdcf70a6d"}, - {file = "rpds_py-0.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:5e6caa3809e50690bd92fa490f5c38caa86082c8c3315aa438bce43786d5e90d"}, - {file = "rpds_py-0.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e53b9b25cac9065328901713a7e9e3b12e4f57ef4280b370fbbf6fef2052eef"}, - {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af27423662f32d7501a00c5e7342f7dbd1e4a718aea7a239781357d15d437133"}, - {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d4dd5fb16eb3825742bad8339d454054261ab59fed2fbac84e1d84d5aae7ba"}, - {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e061de3b745fe611e23cd7318aec2c8b0e4153939c25c9202a5811ca911fd733"}, - {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b811d182ad17ea294f2ec63c0621e7be92a1141e1012383461872cead87468f"}, - {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5552f328eaef1a75ff129d4d0c437bf44e43f9436d3996e8eab623ea0f5fcf73"}, - {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dcbe1f8dd179e4d69b70b1f1d9bb6fd1e7e1bdc9c9aad345cdeb332e29d40748"}, - {file = "rpds_py-0.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8aad80645a011abae487d356e0ceb359f4938dfb6f7bcc410027ed7ae4f7bb8b"}, - {file = "rpds_py-0.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6f5549d6ed1da9bfe3631ca9483ae906f21410be2445b73443fa9f017601c6f"}, - {file = "rpds_py-0.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d452817e0d9c749c431a1121d56a777bd7099b720b3d1c820f1725cb40928f58"}, - {file = "rpds_py-0.16.2-cp312-none-win32.whl", hash = "sha256:888a97002e986eca10d8546e3c8b97da1d47ad8b69726dcfeb3e56348ebb28a3"}, - {file = "rpds_py-0.16.2-cp312-none-win_amd64.whl", hash = "sha256:d8dda2a806dfa4a9b795950c4f5cc56d6d6159f7d68080aedaff3bdc9b5032f5"}, - {file = "rpds_py-0.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:071980663c273bf3d388fe5c794c547e6f35ba3335477072c713a3176bf14a60"}, - {file = "rpds_py-0.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:726ac36e8a3bb8daef2fd482534cabc5e17334052447008405daca7ca04a3108"}, - {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9e557db6a177470316c82f023e5d571811c9a4422b5ea084c85da9aa3c035fc"}, - {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90123853fc8b1747f80b0d354be3d122b4365a93e50fc3aacc9fb4c2488845d6"}, - {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a61f659665a39a4d17d699ab3593d7116d66e1e2e3f03ef3fb8f484e91908808"}, - {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc97f0640e91d7776530f06e6836c546c1c752a52de158720c4224c9e8053cad"}, - {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a54e99a2b9693a37ebf245937fd6e9228b4cbd64b9cc961e1f3391ec6c7391"}, - {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd4b677d929cf1f6bac07ad76e0f2d5de367e6373351c01a9c0a39f6b21b4a8b"}, - {file = "rpds_py-0.16.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5ef00873303d678aaf8b0627e111fd434925ca01c657dbb2641410f1cdaef261"}, - {file = "rpds_py-0.16.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:349cb40897fd529ca15317c22c0eab67f5ac5178b5bd2c6adc86172045210acc"}, - {file = "rpds_py-0.16.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2ddef620e70eaffebed5932ce754d539c0930f676aae6212f8e16cd9743dd365"}, - {file = "rpds_py-0.16.2-cp38-none-win32.whl", hash = "sha256:882ce6e25e585949c3d9f9abd29202367175e0aab3aba0c58c9abbb37d4982ff"}, - {file = "rpds_py-0.16.2-cp38-none-win_amd64.whl", hash = "sha256:f4bd4578e44f26997e9e56c96dedc5f1af43cc9d16c4daa29c771a00b2a26851"}, - {file = "rpds_py-0.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:69ac7ea9897ec201ce68b48582f3eb34a3f9924488a5432a93f177bf76a82a7e"}, - {file = "rpds_py-0.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a9880b4656efe36ccad41edc66789e191e5ee19a1ea8811e0aed6f69851a82f4"}, - {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee94cb58c0ba2c62ee108c2b7c9131b2c66a29e82746e8fa3aa1a1effbd3dcf1"}, - {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24f7a2eb3866a9e91f4599851e0c8d39878a470044875c49bd528d2b9b88361c"}, - {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca57468da2d9a660bcf8961637c85f2fbb2aa64d9bc3f9484e30c3f9f67b1dd7"}, - {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccd4e400309e1f34a5095bf9249d371f0fd60f8a3a5c4a791cad7b99ce1fd38d"}, - {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80443fe2f7b3ea3934c5d75fb0e04a5dbb4a8e943e5ff2de0dec059202b70a8b"}, - {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d6a9f052e72d493efd92a77f861e45bab2f6be63e37fa8ecf0c6fd1a58fedb0"}, - {file = "rpds_py-0.16.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:35953f4f2b3216421af86fd236b7c0c65935936a94ea83ddbd4904ba60757773"}, - {file = "rpds_py-0.16.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:981d135c7cdaf6cd8eadae1c950de43b976de8f09d8e800feed307140d3d6d00"}, - {file = "rpds_py-0.16.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d0dd7ed2f16df2e129496e7fbe59a34bc2d7fc8db443a606644d069eb69cbd45"}, - {file = "rpds_py-0.16.2-cp39-none-win32.whl", hash = "sha256:703d95c75a72e902544fda08e965885525e297578317989fd15a6ce58414b41d"}, - {file = "rpds_py-0.16.2-cp39-none-win_amd64.whl", hash = "sha256:e93ec1b300acf89730cf27975ef574396bc04edecc358e9bd116fb387a123239"}, - {file = "rpds_py-0.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:44627b6ca7308680a70766454db5249105fa6344853af6762eaad4158a2feebe"}, - {file = "rpds_py-0.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3f91df8e6dbb7360e176d1affd5fb0246d2b88d16aa5ebc7db94fd66b68b61da"}, - {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d904c5693e08bad240f16d79305edba78276be87061c872a4a15e2c301fa2c0"}, - {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:290a81cfbe4673285cdf140ec5cd1658ffbf63ab359f2b352ebe172e7cfa5bf0"}, - {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b634c5ec0103c5cbebc24ebac4872b045cccb9456fc59efdcf6fe39775365bd2"}, - {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a297a4d08cc67c7466c873c78039d87840fb50d05473db0ec1b7b03d179bf322"}, - {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2e75e17bd0bb66ee34a707da677e47c14ee51ccef78ed6a263a4cc965a072a1"}, - {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f1b9d9260e06ea017feb7172976ab261e011c1dc2f8883c7c274f6b2aabfe01a"}, - {file = "rpds_py-0.16.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:162d7cd9cd311c1b0ff1c55a024b8f38bd8aad1876b648821da08adc40e95734"}, - {file = "rpds_py-0.16.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:9b32f742ce5b57201305f19c2ef7a184b52f6f9ba6871cc042c2a61f0d6b49b8"}, - {file = "rpds_py-0.16.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac08472f41ea77cd6a5dae36ae7d4ed3951d6602833af87532b556c1b4601d63"}, - {file = "rpds_py-0.16.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:495a14b72bbe217f2695dcd9b5ab14d4f8066a00f5d209ed94f0aca307f85f6e"}, - {file = "rpds_py-0.16.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:8d6b6937ae9eac6d6c0ca3c42774d89fa311f55adff3970fb364b34abde6ed3d"}, - {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a61226465bda9283686db8f17d02569a98e4b13c637be5a26d44aa1f1e361c2"}, - {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5cf6af100ffb5c195beec11ffaa8cf8523057f123afa2944e6571d54da84cdc9"}, - {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6df15846ee3fb2e6397fe25d7ca6624af9f89587f3f259d177b556fed6bebe2c"}, - {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1be2f033df1b8be8c3167ba3c29d5dca425592ee31e35eac52050623afba5772"}, - {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96f957d6ab25a78b9e7fc9749d754b98eac825a112b4e666525ce89afcbd9ed5"}, - {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:088396c7c70e59872f67462fcac3ecbded5233385797021976a09ebd55961dfe"}, - {file = "rpds_py-0.16.2-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4c46ad6356e1561f2a54f08367d1d2e70a0a1bb2db2282d2c1972c1d38eafc3b"}, - {file = "rpds_py-0.16.2-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:47713dc4fce213f5c74ca8a1f6a59b622fc1b90868deb8e8e4d993e421b4b39d"}, - {file = "rpds_py-0.16.2-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f811771019f063bbd0aa7bb72c8a934bc13ebacb4672d712fc1639cfd314cccc"}, - {file = "rpds_py-0.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f19afcfc0dd0dca35694df441e9b0f95bc231b512f51bded3c3d8ca32153ec19"}, - {file = "rpds_py-0.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a4b682c5775d6a3d21e314c10124599976809455ee67020e8e72df1769b87bc3"}, - {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c647ca87fc0ebe808a41de912e9a1bfef9acb85257e5d63691364ac16b81c1f0"}, - {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:302bd4983bbd47063e452c38be66153760112f6d3635c7eeefc094299fa400a9"}, - {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf721ede3eb7b829e4a9b8142bd55db0bdc82902720548a703f7e601ee13bdc3"}, - {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:358dafc89ce3894c7f486c615ba914609f38277ef67f566abc4c854d23b997fa"}, - {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cad0f59ee3dc35526039f4bc23642d52d5f6616b5f687d846bfc6d0d6d486db0"}, - {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cffa76b385dfe1e38527662a302b19ffb0e7f5cf7dd5e89186d2c94a22dd9d0c"}, - {file = "rpds_py-0.16.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:83640a5d7cd3bff694747d50436b8b541b5b9b9782b0c8c1688931d6ee1a1f2d"}, - {file = "rpds_py-0.16.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:ed99b4f7179d2111702020fd7d156e88acd533f5a7d3971353e568b6051d5c97"}, - {file = "rpds_py-0.16.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4022b9dc620e14f30201a8a73898a873c8e910cb642bcd2f3411123bc527f6ac"}, - {file = "rpds_py-0.16.2.tar.gz", hash = "sha256:781ef8bfc091b19960fc0142a23aedadafa826bc32b433fdfe6fd7f964d7ef44"}, + {file = "rpds_py-0.17.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4128980a14ed805e1b91a7ed551250282a8ddf8201a4e9f8f5b7e6225f54170d"}, + {file = "rpds_py-0.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ff1dcb8e8bc2261a088821b2595ef031c91d499a0c1b031c152d43fe0a6ecec8"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d65e6b4f1443048eb7e833c2accb4fa7ee67cc7d54f31b4f0555b474758bee55"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a71169d505af63bb4d20d23a8fbd4c6ce272e7bce6cc31f617152aa784436f29"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:436474f17733c7dca0fbf096d36ae65277e8645039df12a0fa52445ca494729d"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10162fe3f5f47c37ebf6d8ff5a2368508fe22007e3077bf25b9c7d803454d921"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:720215373a280f78a1814becb1312d4e4d1077b1202a56d2b0815e95ccb99ce9"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70fcc6c2906cfa5c6a552ba7ae2ce64b6c32f437d8f3f8eea49925b278a61453"}, + {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91e5a8200e65aaac342a791272c564dffcf1281abd635d304d6c4e6b495f29dc"}, + {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:99f567dae93e10be2daaa896e07513dd4bf9c2ecf0576e0533ac36ba3b1d5394"}, + {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24e4900a6643f87058a27320f81336d527ccfe503984528edde4bb660c8c8d59"}, + {file = "rpds_py-0.17.1-cp310-none-win32.whl", hash = "sha256:0bfb09bf41fe7c51413f563373e5f537eaa653d7adc4830399d4e9bdc199959d"}, + {file = "rpds_py-0.17.1-cp310-none-win_amd64.whl", hash = "sha256:20de7b7179e2031a04042e85dc463a93a82bc177eeba5ddd13ff746325558aa6"}, + {file = "rpds_py-0.17.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:65dcf105c1943cba45d19207ef51b8bc46d232a381e94dd38719d52d3980015b"}, + {file = "rpds_py-0.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:01f58a7306b64e0a4fe042047dd2b7d411ee82e54240284bab63e325762c1147"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:071bc28c589b86bc6351a339114fb7a029f5cddbaca34103aa573eba7b482382"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae35e8e6801c5ab071b992cb2da958eee76340e6926ec693b5ff7d6381441745"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149c5cd24f729e3567b56e1795f74577aa3126c14c11e457bec1b1c90d212e38"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e796051f2070f47230c745d0a77a91088fbee2cc0502e9b796b9c6471983718c"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e820ee1004327609b28db8307acc27f5f2e9a0b185b2064c5f23e815f248f8"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1957a2ab607f9added64478a6982742eb29f109d89d065fa44e01691a20fc20a"}, + {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8587fd64c2a91c33cdc39d0cebdaf30e79491cc029a37fcd458ba863f8815383"}, + {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4dc889a9d8a34758d0fcc9ac86adb97bab3fb7f0c4d29794357eb147536483fd"}, + {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2953937f83820376b5979318840f3ee47477d94c17b940fe31d9458d79ae7eea"}, + {file = "rpds_py-0.17.1-cp311-none-win32.whl", hash = "sha256:1bfcad3109c1e5ba3cbe2f421614e70439f72897515a96c462ea657261b96518"}, + {file = "rpds_py-0.17.1-cp311-none-win_amd64.whl", hash = "sha256:99da0a4686ada4ed0f778120a0ea8d066de1a0a92ab0d13ae68492a437db78bf"}, + {file = "rpds_py-0.17.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1dc29db3900cb1bb40353772417800f29c3d078dbc8024fd64655a04ee3c4bdf"}, + {file = "rpds_py-0.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82ada4a8ed9e82e443fcef87e22a3eed3654dd3adf6e3b3a0deb70f03e86142a"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d36b2b59e8cc6e576f8f7b671e32f2ff43153f0ad6d0201250a7c07f25d570e"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3677fcca7fb728c86a78660c7fb1b07b69b281964673f486ae72860e13f512ad"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:516fb8c77805159e97a689e2f1c80655c7658f5af601c34ffdb916605598cda2"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df3b6f45ba4515632c5064e35ca7f31d51d13d1479673185ba8f9fefbbed58b9"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a967dd6afda7715d911c25a6ba1517975acd8d1092b2f326718725461a3d33f9"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dbbb95e6fc91ea3102505d111b327004d1c4ce98d56a4a02e82cd451f9f57140"}, + {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02866e060219514940342a1f84303a1ef7a1dad0ac311792fbbe19b521b489d2"}, + {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2528ff96d09f12e638695f3a2e0c609c7b84c6df7c5ae9bfeb9252b6fa686253"}, + {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd345a13ce06e94c753dab52f8e71e5252aec1e4f8022d24d56decd31e1b9b23"}, + {file = "rpds_py-0.17.1-cp312-none-win32.whl", hash = "sha256:2a792b2e1d3038daa83fa474d559acfd6dc1e3650ee93b2662ddc17dbff20ad1"}, + {file = "rpds_py-0.17.1-cp312-none-win_amd64.whl", hash = "sha256:292f7344a3301802e7c25c53792fae7d1593cb0e50964e7bcdcc5cf533d634e3"}, + {file = "rpds_py-0.17.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:8ffe53e1d8ef2520ebcf0c9fec15bb721da59e8ef283b6ff3079613b1e30513d"}, + {file = "rpds_py-0.17.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4341bd7579611cf50e7b20bb8c2e23512a3dc79de987a1f411cb458ab670eb90"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4eb548daf4836e3b2c662033bfbfc551db58d30fd8fe660314f86bf8510b93"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b686f25377f9c006acbac63f61614416a6317133ab7fafe5de5f7dc8a06d42eb"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e21b76075c01d65d0f0f34302b5a7457d95721d5e0667aea65e5bb3ab415c25"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b86b21b348f7e5485fae740d845c65a880f5d1eda1e063bc59bef92d1f7d0c55"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f175e95a197f6a4059b50757a3dca33b32b61691bdbd22c29e8a8d21d3914cae"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1701fc54460ae2e5efc1dd6350eafd7a760f516df8dbe51d4a1c79d69472fbd4"}, + {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9051e3d2af8f55b42061603e29e744724cb5f65b128a491446cc029b3e2ea896"}, + {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:7450dbd659fed6dd41d1a7d47ed767e893ba402af8ae664c157c255ec6067fde"}, + {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5a024fa96d541fd7edaa0e9d904601c6445e95a729a2900c5aec6555fe921ed6"}, + {file = "rpds_py-0.17.1-cp38-none-win32.whl", hash = "sha256:da1ead63368c04a9bded7904757dfcae01eba0e0f9bc41d3d7f57ebf1c04015a"}, + {file = "rpds_py-0.17.1-cp38-none-win_amd64.whl", hash = "sha256:841320e1841bb53fada91c9725e766bb25009cfd4144e92298db296fb6c894fb"}, + {file = "rpds_py-0.17.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:f6c43b6f97209e370124baf2bf40bb1e8edc25311a158867eb1c3a5d449ebc7a"}, + {file = "rpds_py-0.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7d63ec01fe7c76c2dbb7e972fece45acbb8836e72682bde138e7e039906e2c"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81038ff87a4e04c22e1d81f947c6ac46f122e0c80460b9006e6517c4d842a6ec"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:810685321f4a304b2b55577c915bece4c4a06dfe38f6e62d9cc1d6ca8ee86b99"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25f071737dae674ca8937a73d0f43f5a52e92c2d178330b4c0bb6ab05586ffa6"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa5bfb13f1e89151ade0eb812f7b0d7a4d643406caaad65ce1cbabe0a66d695f"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfe07308b311a8293a0d5ef4e61411c5c20f682db6b5e73de6c7c8824272c256"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a000133a90eea274a6f28adc3084643263b1e7c1a5a66eb0a0a7a36aa757ed74"}, + {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d0e8a6434a3fbf77d11448c9c25b2f25244226cfbec1a5159947cac5b8c5fa4"}, + {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efa767c220d94aa4ac3a6dd3aeb986e9f229eaf5bce92d8b1b3018d06bed3772"}, + {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:dbc56680ecf585a384fbd93cd42bc82668b77cb525343170a2d86dafaed2a84b"}, + {file = "rpds_py-0.17.1-cp39-none-win32.whl", hash = "sha256:270987bc22e7e5a962b1094953ae901395e8c1e1e83ad016c5cfcfff75a15a3f"}, + {file = "rpds_py-0.17.1-cp39-none-win_amd64.whl", hash = "sha256:2a7b2f2f56a16a6d62e55354dd329d929560442bd92e87397b7a9586a32e3e76"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a3264e3e858de4fc601741498215835ff324ff2482fd4e4af61b46512dd7fc83"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f2f3b28b40fddcb6c1f1f6c88c6f3769cd933fa493ceb79da45968a21dccc920"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9584f8f52010295a4a417221861df9bea4c72d9632562b6e59b3c7b87a1522b7"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c64602e8be701c6cfe42064b71c84ce62ce66ddc6422c15463fd8127db3d8066"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:060f412230d5f19fc8c8b75f315931b408d8ebf56aec33ef4168d1b9e54200b1"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9412abdf0ba70faa6e2ee6c0cc62a8defb772e78860cef419865917d86c7342"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9737bdaa0ad33d34c0efc718741abaafce62fadae72c8b251df9b0c823c63b22"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9f0e4dc0f17dcea4ab9d13ac5c666b6b5337042b4d8f27e01b70fae41dd65c57"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1db228102ab9d1ff4c64148c96320d0be7044fa28bd865a9ce628ce98da5973d"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8bbd8e56f3ba25a7d0cf980fc42b34028848a53a0e36c9918550e0280b9d0b6"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:be22ae34d68544df293152b7e50895ba70d2a833ad9566932d750d3625918b82"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bf046179d011e6114daf12a534d874958b039342b347348a78b7cdf0dd9d6041"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a746a6d49665058a5896000e8d9d2f1a6acba8a03b389c1e4c06e11e0b7f40d"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0b8bf5b8db49d8fd40f54772a1dcf262e8be0ad2ab0206b5a2ec109c176c0a4"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7f4cb1f173385e8a39c29510dd11a78bf44e360fb75610594973f5ea141028b"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7fbd70cb8b54fe745301921b0816c08b6d917593429dfc437fd024b5ba713c58"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bdf1303df671179eaf2cb41e8515a07fc78d9d00f111eadbe3e14262f59c3d0"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad059a4bd14c45776600d223ec194e77db6c20255578bb5bcdd7c18fd169361"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3664d126d3388a887db44c2e293f87d500c4184ec43d5d14d2d2babdb4c64cad"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:698ea95a60c8b16b58be9d854c9f993c639f5c214cf9ba782eca53a8789d6b19"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:c3d2010656999b63e628a3c694f23020322b4178c450dc478558a2b6ef3cb9bb"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:938eab7323a736533f015e6069a7d53ef2dcc841e4e533b782c2bfb9fb12d84b"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e626b365293a2142a62b9a614e1f8e331b28f3ca57b9f05ebbf4cf2a0f0bdc5"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:380e0df2e9d5d5d339803cfc6d183a5442ad7ab3c63c2a0982e8c824566c5ccc"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b760a56e080a826c2e5af09002c1a037382ed21d03134eb6294812dda268c811"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5576ee2f3a309d2bb403ec292d5958ce03953b0e57a11d224c1f134feaf8c40f"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3c3461ebb4c4f1bbc70b15d20b565759f97a5aaf13af811fcefc892e9197ba"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:637b802f3f069a64436d432117a7e58fab414b4e27a7e81049817ae94de45d8d"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffee088ea9b593cc6160518ba9bd319b5475e5f3e578e4552d63818773c6f56a"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ac732390d529d8469b831949c78085b034bff67f584559340008d0f6041a049"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:93432e747fb07fa567ad9cc7aaadd6e29710e515aabf939dfbed8046041346c6"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b7d9ca34542099b4e185b3c2a2b2eda2e318a7dbde0b0d83357a6d4421b5296"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:0387ce69ba06e43df54e43968090f3626e231e4bc9150e4c3246947567695f68"}, + {file = "rpds_py-0.17.1.tar.gz", hash = "sha256:0210b2668f24c078307260bf88bdac9d6f1093635df5123789bfee4d8d7fc8e7"}, ] [[package]] @@ -1685,4 +1687,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "5161593461b5642520d71b1185cf981761d6117a89546e5c449b92ecb2eed050" +content-hash = "1c468b66c56cfccd5e5bff7d9c69f01c729d828132a8a56a7089447f5da0f534" diff --git a/airbyte-integrations/bases/connector-acceptance-test/pyproject.toml b/airbyte-integrations/bases/connector-acceptance-test/pyproject.toml index faa897d7ad075..12f3060287b32 100644 --- a/airbyte-integrations/bases/connector-acceptance-test/pyproject.toml +++ b/airbyte-integrations/bases/connector-acceptance-test/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "connector-acceptance-test" -version = "3.0.0" +version = "3.0.1" description = "Contains acceptance tests for connectors." authors = ["Airbyte "] license = "MIT" @@ -13,7 +13,7 @@ homepage = "https://github.com/airbytehq/airbyte" [tool.poetry.dependencies] python = "^3.10" airbyte-protocol-models = "<1.0.0" -dagger-io = "==0.9.5" +dagger-io = "==0.9.6" PyYAML = "~=6.0" icdiff = "~=1.9" inflection = "~=0.5" From e3e58cc0639db45719b29256983ed0cebe7e16db Mon Sep 17 00:00:00 2001 From: Catherine Noll Date: Thu, 18 Jan 2024 11:35:40 -0500 Subject: [PATCH 141/574] Concurrent CDK: fix state message ordering (#34131) --- .../sources/streams/concurrent/cursor.py | 37 +- .../abstract_stream_state_converter.py | 18 +- .../datetime_stream_state_converter.py | 66 +-- .../scenarios/stream_facade_builder.py | 4 +- .../sources/streams/concurrent/test_cursor.py | 23 +- .../test_datetime_state_converter.py | 434 +++++++----------- 6 files changed, 259 insertions(+), 323 deletions(-) diff --git a/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/cursor.py b/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/cursor.py index 42137e48a46f5..282498db17835 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/cursor.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/cursor.py @@ -3,7 +3,8 @@ # import functools from abc import ABC, abstractmethod -from typing import Any, List, Mapping, Optional, Protocol, Tuple +from datetime import datetime +from typing import Any, List, Mapping, MutableMapping, Optional, Protocol, Tuple from airbyte_cdk.sources.connector_state_manager import ConnectorStateManager from airbyte_cdk.sources.message import MessageRepository @@ -36,6 +37,11 @@ def extract_value(self, record: Record) -> Comparable: class Cursor(ABC): + @property + @abstractmethod + def state(self) -> MutableMapping[str, Any]: + ... + @abstractmethod def observe(self, record: Record) -> None: """ @@ -52,6 +58,10 @@ def close_partition(self, partition: Partition) -> None: class NoopCursor(Cursor): + @property + def state(self) -> MutableMapping[str, Any]: + return {} + def observe(self, record: Record) -> None: pass @@ -73,6 +83,7 @@ def __init__( connector_state_converter: AbstractStreamStateConverter, cursor_field: CursorField, slice_boundary_fields: Optional[Tuple[str, str]], + start: Optional[Any], ) -> None: self._stream_name = stream_name self._stream_namespace = stream_namespace @@ -82,9 +93,19 @@ def __init__( self._cursor_field = cursor_field # To see some example where the slice boundaries might not be defined, check https://github.com/airbytehq/airbyte/blob/1ce84d6396e446e1ac2377362446e3fb94509461/airbyte-integrations/connectors/source-stripe/source_stripe/streams.py#L363-L379 self._slice_boundary_fields = slice_boundary_fields if slice_boundary_fields else tuple() + self._start = start self._most_recent_record: Optional[Record] = None self._has_closed_at_least_one_slice = False - self.state = stream_state + self.start, self._concurrent_state = self._get_concurrent_state(stream_state) + + @property + def state(self) -> MutableMapping[str, Any]: + return self._concurrent_state + + def _get_concurrent_state(self, state: MutableMapping[str, Any]) -> Tuple[datetime, MutableMapping[str, Any]]: + if self._connector_state_converter.is_state_message_compatible(state): + return self._start or self._connector_state_converter.zero_value, self._connector_state_converter.deserialize(state) + return self._connector_state_converter.convert_from_sequential_state(self._cursor_field, state, self._start) def observe(self, record: Record) -> None: if self._slice_boundary_fields: @@ -102,7 +123,7 @@ def _extract_cursor_value(self, record: Record) -> Any: def close_partition(self, partition: Partition) -> None: slice_count_before = len(self.state.get("slices", [])) self._add_slice_to_state(partition) - if slice_count_before < len(self.state["slices"]): + if slice_count_before < len(self.state["slices"]): # only emit if at least one slice has been processed self._merge_partitions() self._emit_state_message() self._has_closed_at_least_one_slice = True @@ -110,7 +131,9 @@ def close_partition(self, partition: Partition) -> None: def _add_slice_to_state(self, partition: Partition) -> None: if self._slice_boundary_fields: if "slices" not in self.state: - self.state["slices"] = [] + raise RuntimeError( + f"The state for stream {self._stream_name} should have at least one slice to delineate the sync start time, but no slices are present. This is unexpected. Please contact Support." + ) self.state["slices"].append( { "start": self._extract_from_slice(partition, self._slice_boundary_fields[self._START_BOUNDARY]), @@ -126,10 +149,8 @@ def _add_slice_to_state(self, partition: Partition) -> None: self.state["slices"].append( { - # TODO: if we migrate stored state to the concurrent state format, we may want this to be the config start date - # instead of zero_value. - "start": self._connector_state_converter.zero_value, - "end": self._extract_cursor_value(self._most_recent_record), + self._connector_state_converter.START_KEY: self.start, + self._connector_state_converter.END_KEY: self._extract_cursor_value(self._most_recent_record), } ) diff --git a/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/state_converters/abstract_stream_state_converter.py b/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/state_converters/abstract_stream_state_converter.py index dbf10bb2355ac..843f477ddb160 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/state_converters/abstract_stream_state_converter.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/state_converters/abstract_stream_state_converter.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from enum import Enum -from typing import TYPE_CHECKING, Any, List, MutableMapping, Optional +from typing import TYPE_CHECKING, Any, List, MutableMapping, Tuple if TYPE_CHECKING: from airbyte_cdk.sources.streams.concurrent.cursor import CursorField @@ -18,15 +18,6 @@ class AbstractStreamStateConverter(ABC): START_KEY = "start" END_KEY = "end" - def get_concurrent_stream_state( - self, cursor_field: Optional["CursorField"], state: MutableMapping[str, Any] - ) -> Optional[MutableMapping[str, Any]]: - if not cursor_field: - return None - if self.is_state_message_compatible(state): - return self.deserialize(state) - return self.convert_from_sequential_state(cursor_field, state) - @abstractmethod def deserialize(self, state: MutableMapping[str, Any]) -> MutableMapping[str, Any]: """ @@ -40,8 +31,11 @@ def is_state_message_compatible(state: MutableMapping[str, Any]) -> bool: @abstractmethod def convert_from_sequential_state( - self, cursor_field: "CursorField", stream_state: MutableMapping[str, Any] - ) -> MutableMapping[str, Any]: + self, + cursor_field: "CursorField", + stream_state: MutableMapping[str, Any], + start: Any, + ) -> Tuple[Any, MutableMapping[str, Any]]: """ Convert the state message to the format required by the ConcurrentCursor. diff --git a/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/state_converters/datetime_stream_state_converter.py b/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/state_converters/datetime_stream_state_converter.py index 0a8527d44ddb0..83f8a44b23db2 100644 --- a/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/state_converters/datetime_stream_state_converter.py +++ b/airbyte-cdk/python/airbyte_cdk/sources/streams/concurrent/state_converters/datetime_stream_state_converter.py @@ -4,7 +4,7 @@ from abc import abstractmethod from datetime import datetime, timedelta -from typing import Any, List, MutableMapping, Optional +from typing import Any, List, MutableMapping, Optional, Tuple import pendulum from airbyte_cdk.sources.streams.concurrent.cursor import CursorField @@ -16,9 +16,6 @@ class DateTimeStreamStateConverter(AbstractStreamStateConverter): - START_KEY = "start" - END_KEY = "end" - @property @abstractmethod def _zero_value(self) -> Any: @@ -62,7 +59,7 @@ def merge_intervals(self, intervals: List[MutableMapping[str, datetime]]) -> Lis for interval in sorted_intervals[1:]: last_end_time = merged_intervals[-1][self.END_KEY] current_start_time = interval[self.START_KEY] - if self.compare_intervals(last_end_time, current_start_time): + if self._compare_intervals(last_end_time, current_start_time): merged_end_time = max(last_end_time, interval[self.END_KEY]) merged_intervals[-1][self.END_KEY] = merged_end_time else: @@ -70,10 +67,12 @@ def merge_intervals(self, intervals: List[MutableMapping[str, datetime]]) -> Lis return merged_intervals - def compare_intervals(self, end_time: Any, start_time: Any) -> bool: + def _compare_intervals(self, end_time: Any, start_time: Any) -> bool: return bool(self.increment(end_time) >= start_time) - def convert_from_sequential_state(self, cursor_field: CursorField, stream_state: MutableMapping[str, Any]) -> MutableMapping[str, Any]: + def convert_from_sequential_state( + self, cursor_field: CursorField, stream_state: MutableMapping[str, Any], start: datetime + ) -> Tuple[datetime, MutableMapping[str, Any]]: """ Convert the state message to the format required by the ConcurrentCursor. @@ -82,28 +81,35 @@ def convert_from_sequential_state(self, cursor_field: CursorField, stream_state: "state_type": ConcurrencyCompatibleStateType.date_range.value, "metadata": { … }, "slices": [ - {starts: 0, end: "2021-01-18T21:18:20.000+00:00", finished_processing: true}] + {"start": "2021-01-18T21:18:20.000+00:00", "end": "2021-01-18T21:18:20.000+00:00"}, + ] } """ + sync_start = self._get_sync_start(cursor_field, stream_state, start) if self.is_state_message_compatible(stream_state): - return stream_state - if cursor_field.cursor_field_key in stream_state: - slices = [ - { - # TODO: if we migrate stored state to the concurrent state format, we may want this to be the config start date - # instead of `zero_value` - self.START_KEY: self.zero_value, - self.END_KEY: self.parse_timestamp(stream_state[cursor_field.cursor_field_key]), - }, - ] - else: - slices = [] - return { + return sync_start, stream_state + + # Create a slice to represent the records synced during prior syncs. + # The start and end are the same to avoid confusion as to whether the records for this slice + # were actually synced + slices = [{self.START_KEY: sync_start, self.END_KEY: sync_start}] + + return sync_start, { "state_type": ConcurrencyCompatibleStateType.date_range.value, "slices": slices, "legacy": stream_state, } + def _get_sync_start(self, cursor_field: CursorField, stream_state: MutableMapping[str, Any], start: Optional[Any]) -> datetime: + sync_start = self.parse_timestamp(start) if start is not None else self.zero_value + prev_sync_low_water_mark = ( + self.parse_timestamp(stream_state[cursor_field.cursor_field_key]) if cursor_field.cursor_field_key in stream_state else None + ) + if prev_sync_low_water_mark and prev_sync_low_water_mark >= sync_start: + return prev_sync_low_water_mark + else: + return sync_start + def convert_to_sequential_state(self, cursor_field: CursorField, stream_state: MutableMapping[str, Any]) -> MutableMapping[str, Any]: """ Convert the state message from the concurrency-compatible format to the stream's original format. @@ -113,10 +119,9 @@ def convert_to_sequential_state(self, cursor_field: CursorField, stream_state: M """ if self.is_state_message_compatible(stream_state): legacy_state = stream_state.get("legacy", {}) - if slices := stream_state.pop("slices", None): - latest_complete_time = self._get_latest_complete_time(slices) - if latest_complete_time: - legacy_state.update({cursor_field.cursor_field_key: self.output_format(latest_complete_time)}) + latest_complete_time = self._get_latest_complete_time(stream_state.get("slices", [])) + if latest_complete_time is not None: + legacy_state.update({cursor_field.cursor_field_key: self.output_format(latest_complete_time)}) return legacy_state or {} else: return stream_state @@ -125,11 +130,12 @@ def _get_latest_complete_time(self, slices: List[MutableMapping[str, Any]]) -> O """ Get the latest time before which all records have been processed. """ - if slices: - first_interval = self.merge_intervals(slices)[0][self.END_KEY] - return first_interval - else: - return None + if not slices: + raise RuntimeError("Expected at least one slice but there were none. This is unexpected; please contact Support.") + + merged_intervals = self.merge_intervals(slices) + first_interval = merged_intervals[0] + return first_interval[self.END_KEY] class EpochValueConcurrentStreamStateConverter(DateTimeStreamStateConverter): diff --git a/airbyte-cdk/python/unit_tests/sources/streams/concurrent/scenarios/stream_facade_builder.py b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/scenarios/stream_facade_builder.py index 716eb5508eafa..30ec297b0b4f0 100644 --- a/airbyte-cdk/python/unit_tests/sources/streams/concurrent/scenarios/stream_facade_builder.py +++ b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/scenarios/stream_facade_builder.py @@ -52,8 +52,7 @@ def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> def streams(self, config: Mapping[str, Any]) -> List[Stream]: state_manager = ConnectorStateManager(stream_instance_map={s.name: s for s in self._streams}, state=self._state) state_converter = StreamFacadeConcurrentConnectorStateConverter() - stream_states = [state_converter.get_concurrent_stream_state(self._cursor_field, state_manager.get_stream_state(stream.name, stream.namespace)) - for stream in self._streams] + stream_states = [state_manager.get_stream_state(stream.name, stream.namespace) for stream in self._streams] return [ StreamFacade.create_from_stream( stream, @@ -69,6 +68,7 @@ def streams(self, config: Mapping[str, Any]) -> List[Stream]: state_converter, self._cursor_field, self._cursor_boundaries, + None, ) if self._cursor_field else NoopCursor(), diff --git a/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_cursor.py b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_cursor.py index 2de0f3d6c28e2..dd1246fbc2a8f 100644 --- a/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_cursor.py +++ b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_cursor.py @@ -45,33 +45,50 @@ def _cursor_with_slice_boundary_fields(self) -> ConcurrentCursor: return ConcurrentCursor( _A_STREAM_NAME, _A_STREAM_NAMESPACE, - self._state_converter.get_concurrent_stream_state(CursorField(_A_CURSOR_FIELD_KEY), {}), + {}, self._message_repository, self._state_manager, self._state_converter, CursorField(_A_CURSOR_FIELD_KEY), _SLICE_BOUNDARY_FIELDS, + None, ) def _cursor_without_slice_boundary_fields(self) -> ConcurrentCursor: return ConcurrentCursor( _A_STREAM_NAME, _A_STREAM_NAMESPACE, - self._state_converter.get_concurrent_stream_state(CursorField(_A_CURSOR_FIELD_KEY), {}), + {}, self._message_repository, self._state_manager, self._state_converter, CursorField(_A_CURSOR_FIELD_KEY), None, + None, ) def test_given_boundary_fields_when_close_partition_then_emit_state(self) -> None: - self._cursor_with_slice_boundary_fields().close_partition( + cursor = self._cursor_with_slice_boundary_fields() + cursor.close_partition( _partition( {_LOWER_SLICE_BOUNDARY_FIELD: 12, _UPPER_SLICE_BOUNDARY_FIELD: 30}, ) ) + self._message_repository.emit_message.assert_called_once_with(self._state_manager.create_state_message.return_value) + self._state_manager.update_state_for_stream.assert_called_once_with( + _A_STREAM_NAME, + _A_STREAM_NAMESPACE, + {_A_CURSOR_FIELD_KEY: 0}, # State message is updated to the legacy format before being emitted + ) + + def test_given_boundary_fields_when_close_partition_then_emit_updated_state(self) -> None: + self._cursor_with_slice_boundary_fields().close_partition( + _partition( + {_LOWER_SLICE_BOUNDARY_FIELD: 0, _UPPER_SLICE_BOUNDARY_FIELD: 30}, + ) + ) + self._message_repository.emit_message.assert_called_once_with(self._state_manager.create_state_message.return_value) self._state_manager.update_state_for_stream.assert_called_once_with( _A_STREAM_NAME, diff --git a/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_datetime_state_converter.py b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_datetime_state_converter.py index 7feb54c19a8e5..b516afaeef622 100644 --- a/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_datetime_state_converter.py +++ b/airbyte-cdk/python/unit_tests/sources/streams/concurrent/test_datetime_state_converter.py @@ -5,16 +5,6 @@ from datetime import datetime, timezone import pytest -from airbyte_cdk.models import ( - AirbyteStateBlob, - AirbyteStateMessage, - AirbyteStateType, - AirbyteStream, - AirbyteStreamState, - StreamDescriptor, - SyncMode, -) -from airbyte_cdk.sources.connector_state_manager import ConnectorStateManager from airbyte_cdk.sources.streams.concurrent.cursor import CursorField from airbyte_cdk.sources.streams.concurrent.state_converters.abstract_stream_state_converter import ConcurrencyCompatibleStateType from airbyte_cdk.sources.streams.concurrent.state_converters.datetime_stream_state_converter import ( @@ -23,115 +13,12 @@ ) -@pytest.mark.parametrize( - "converter, stream, input_state, expected_output_state", - [ - pytest.param( - EpochValueConcurrentStreamStateConverter(), - AirbyteStream(name="stream1", json_schema={}, supported_sync_modes=[SyncMode.incremental]), - [], - {'legacy': {}, 'slices': [], 'state_type': 'date-range'}, - id="no-input-state-epoch", - ), - pytest.param( - EpochValueConcurrentStreamStateConverter(), - AirbyteStream(name="stream1", json_schema={}, supported_sync_modes=[SyncMode.incremental]), - [ - AirbyteStateMessage( - type=AirbyteStateType.STREAM, - stream=AirbyteStreamState( - stream_descriptor=StreamDescriptor(name="stream1", namespace=None), - stream_state=AirbyteStateBlob.parse_obj({"created_at": 1703020837}), - ), - ), - ], - { - "legacy": {"created_at": 1703020837}, - "slices": [{"end": datetime(2023, 12, 19, 21, 20, 37, tzinfo=timezone.utc), - "start": datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)}], - "state_type": ConcurrencyCompatibleStateType.date_range.value, - }, - id="incompatible-input-state-epoch", - ), - pytest.param( - EpochValueConcurrentStreamStateConverter(), - AirbyteStream(name="stream1", json_schema={}, supported_sync_modes=[SyncMode.incremental]), - [ - AirbyteStateMessage( - type=AirbyteStateType.STREAM, - stream=AirbyteStreamState( - stream_descriptor=StreamDescriptor(name="stream1", namespace=None), - stream_state=AirbyteStateBlob.parse_obj( - { - "created_at": 1703020837, - "state_type": ConcurrencyCompatibleStateType.date_range.value, - }, - ), - ), - ), - ], - {"created_at": 1703020837, "state_type": ConcurrencyCompatibleStateType.date_range.value}, - id="compatible-input-state-epoch", - ), - pytest.param( - IsoMillisConcurrentStreamStateConverter(), - AirbyteStream(name="stream1", json_schema={}, supported_sync_modes=[SyncMode.incremental]), - [], - {'legacy': {}, 'slices': [], 'state_type': 'date-range'}, - id="no-input-state-isomillis", - ), - pytest.param( - IsoMillisConcurrentStreamStateConverter(), - AirbyteStream(name="stream1", json_schema={}, supported_sync_modes=[SyncMode.incremental]), - [ - AirbyteStateMessage( - type=AirbyteStateType.STREAM, - stream=AirbyteStreamState( - stream_descriptor=StreamDescriptor(name="stream1", namespace=None), - stream_state=AirbyteStateBlob.parse_obj({"created_at": "2021-01-18T21:18:20.000Z"}), - ), - ), - ], - { - "legacy": {"created_at": "2021-01-18T21:18:20.000Z"}, - "slices": [{"end": datetime(2021, 1, 18, 21, 18, 20, tzinfo=timezone.utc), - "start": datetime(1, 1, 1, 0, 0, 0, tzinfo=timezone.utc)}], - "state_type": ConcurrencyCompatibleStateType.date_range.value}, - id="incompatible-input-state-isomillis", - ), - pytest.param( - IsoMillisConcurrentStreamStateConverter(), - AirbyteStream(name="stream1", json_schema={}, supported_sync_modes=[SyncMode.incremental]), - [ - AirbyteStateMessage( - type=AirbyteStateType.STREAM, - stream=AirbyteStreamState( - stream_descriptor=StreamDescriptor(name="stream1", namespace=None), - stream_state=AirbyteStateBlob.parse_obj( - { - "created_at": "2021-01-18T21:18:20.000Z", - "state_type": ConcurrencyCompatibleStateType.date_range.value, - }, - ), - ), - ), - ], - {"created_at": "2021-01-18T21:18:20.000Z", "state_type": ConcurrencyCompatibleStateType.date_range.value}, - id="compatible-input-state-isomillis", - ), - ], -) -def test_concurrent_connector_state_manager_get_stream_state(converter, stream, input_state, expected_output_state): - state_manager = ConnectorStateManager({"stream1": stream}, input_state) - assert converter.get_concurrent_stream_state(CursorField("created_at"), state_manager.get_stream_state("stream1", None)) == expected_output_state - - @pytest.mark.parametrize( "converter, input_state, is_compatible", [ pytest.param( EpochValueConcurrentStreamStateConverter(), - {'state_type': 'date-range'}, + {"state_type": "date-range"}, True, id="no-input-state-is-compatible-epoch", ), @@ -163,7 +50,7 @@ def test_concurrent_connector_state_manager_get_stream_state(converter, stream, ), pytest.param( IsoMillisConcurrentStreamStateConverter(), - {'state_type': 'date-range'}, + {"state_type": "date-range"}, True, id="no-input-state-is-compatible-isomillis", ), @@ -200,22 +87,106 @@ def test_concurrent_stream_state_converter_is_state_message_compatible(converter @pytest.mark.parametrize( - "converter, stream, sequential_state, expected_output_state", + "converter,start,state,expected_start", + [ + pytest.param( + EpochValueConcurrentStreamStateConverter(), + None, + {}, + EpochValueConcurrentStreamStateConverter().zero_value, + id="epoch-converter-no-state-no-start-start-is-zero-value" + ), + pytest.param( + EpochValueConcurrentStreamStateConverter(), + 1617030403, + {}, + datetime(2021, 3, 29, 15, 6, 43, tzinfo=timezone.utc), + id="epoch-converter-no-state-with-start-start-is-start" + ), + pytest.param( + EpochValueConcurrentStreamStateConverter(), + None, + {"created_at": 1617030404}, + datetime(2021, 3, 29, 15, 6, 44, tzinfo=timezone.utc), + id="epoch-converter-state-without-start-start-is-from-state" + ), + pytest.param( + EpochValueConcurrentStreamStateConverter(), + 1617030404, + {"created_at": 1617030403}, + datetime(2021, 3, 29, 15, 6, 44, tzinfo=timezone.utc), + id="epoch-converter-state-before-start-start-is-start" + ), + pytest.param( + EpochValueConcurrentStreamStateConverter(), + 1617030403, + {"created_at": 1617030404}, + datetime(2021, 3, 29, 15, 6, 44, tzinfo=timezone.utc), + id="epoch-converter-state-after-start-start-is-from-state" + ), + pytest.param( + IsoMillisConcurrentStreamStateConverter(), + None, + {}, + IsoMillisConcurrentStreamStateConverter().zero_value, + id="isomillis-converter-no-state-no-start-start-is-zero-value" + ), + pytest.param( + IsoMillisConcurrentStreamStateConverter(), + "2021-08-22T05:03:27.000Z", + {}, + datetime(2021, 8, 22, 5, 3, 27, tzinfo=timezone.utc), + id="isomillis-converter-no-state-with-start-start-is-start" + ), + pytest.param( + IsoMillisConcurrentStreamStateConverter(), + None, + {"created_at": "2021-08-22T05:03:27.000Z"}, + datetime(2021, 8, 22, 5, 3, 27, tzinfo=timezone.utc), + id="isomillis-converter-state-without-start-start-is-from-state" + ), + pytest.param( + IsoMillisConcurrentStreamStateConverter(), + "2022-08-22T05:03:27.000Z", + {"created_at": "2021-08-22T05:03:27.000Z"}, + datetime(2022, 8, 22, 5, 3, 27, tzinfo=timezone.utc), + id="isomillis-converter-state-before-start-start-is-start" + ), + pytest.param( + IsoMillisConcurrentStreamStateConverter(), + "2022-08-22T05:03:27.000Z", + {"created_at": "2023-08-22T05:03:27.000Z"}, + datetime(2023, 8, 22, 5, 3, 27, tzinfo=timezone.utc), + id="isomillis-converter-state-after-start-start-is-from-state" + ), + ] +) +def test_get_sync_start(converter, start, state, expected_start): + assert converter._get_sync_start(CursorField("created_at"), state, start) == expected_start + + +@pytest.mark.parametrize( + "converter, start, sequential_state, expected_output_state", [ pytest.param( EpochValueConcurrentStreamStateConverter(), - AirbyteStream(name="stream1", json_schema={}, supported_sync_modes=[SyncMode.incremental]), + 0, {}, - {'legacy': {}, 'slices': [], 'state_type': 'date-range'}, + { + "legacy": {}, + "slices": [{"start": EpochValueConcurrentStreamStateConverter().zero_value, + "end": EpochValueConcurrentStreamStateConverter().zero_value}], + "state_type": "date-range", + }, id="empty-input-state-epoch", ), pytest.param( EpochValueConcurrentStreamStateConverter(), - AirbyteStream(name="stream1", json_schema={}, supported_sync_modes=[SyncMode.incremental]), + 1617030403, {"created": 1617030403}, { "state_type": "date-range", - "slices": [{"start": datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "slices": [{"start": datetime(2021, 3, 29, 15, 6, 43, tzinfo=timezone.utc), "end": datetime(2021, 3, 29, 15, 6, 43, tzinfo=timezone.utc)}], "legacy": {"created": 1617030403}, }, @@ -223,18 +194,11 @@ def test_concurrent_stream_state_converter_is_state_message_compatible(converter ), pytest.param( IsoMillisConcurrentStreamStateConverter(), - AirbyteStream(name="stream1", json_schema={}, supported_sync_modes=[SyncMode.incremental]), - {}, - {'legacy': {}, 'slices': [], 'state_type': 'date-range'}, - id="empty-input-state-isomillis", - ), - pytest.param( - IsoMillisConcurrentStreamStateConverter(), - AirbyteStream(name="stream1", json_schema={}, supported_sync_modes=[SyncMode.incremental]), + "2020-01-01T00:00:00.000Z", {"created": "2021-08-22T05:03:27.000Z"}, { "state_type": "date-range", - "slices": [{"start": datetime(1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "slices": [{"start": datetime(2021, 8, 22, 5, 3, 27, tzinfo=timezone.utc), "end": datetime(2021, 8, 22, 5, 3, 27, tzinfo=timezone.utc)}], "legacy": {"created": "2021-08-22T05:03:27.000Z"}, }, @@ -242,186 +206,120 @@ def test_concurrent_stream_state_converter_is_state_message_compatible(converter ), ], ) -def test_convert_from_sequential_state(converter, stream, sequential_state, expected_output_state): +def test_convert_from_sequential_state(converter, start, sequential_state, expected_output_state): comparison_format = "%Y-%m-%dT%H:%M:%S.%f" if expected_output_state["slices"]: - conversion = converter.convert_from_sequential_state(CursorField("created"), sequential_state) + _, conversion = converter.convert_from_sequential_state(CursorField("created"), sequential_state, start) assert conversion["state_type"] == expected_output_state["state_type"] assert conversion["legacy"] == expected_output_state["legacy"] for actual, expected in zip(conversion["slices"], expected_output_state["slices"]): assert actual["start"].strftime(comparison_format) == expected["start"].strftime(comparison_format) assert actual["end"].strftime(comparison_format) == expected["end"].strftime(comparison_format) else: - assert converter.convert_from_sequential_state(CursorField("created"), sequential_state) == expected_output_state + _, conversion = converter.convert_from_sequential_state(CursorField("created"), sequential_state, start) + assert conversion == expected_output_state @pytest.mark.parametrize( - "converter, stream, concurrent_state, expected_output_state", + "converter, concurrent_state, expected_output_state", [ pytest.param( EpochValueConcurrentStreamStateConverter(), - AirbyteStream(name="stream1", json_schema={}, supported_sync_modes=[SyncMode.incremental]), - {"state_type": ConcurrencyCompatibleStateType.date_range.value}, - {}, - id="empty-input-state-epoch", + { + "state_type": "date-range", + "slices": [{"start": datetime(1970, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + "end": datetime(2021, 3, 29, 15, 6, 43, tzinfo=timezone.utc)}], + }, + {"created": 1617030403}, + id="epoch-single-slice", ), pytest.param( EpochValueConcurrentStreamStateConverter(), - AirbyteStream(name="stream1", json_schema={}, supported_sync_modes=[SyncMode.incremental]), { "state_type": "date-range", - "slices": [{"start": datetime(1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), - "end": datetime(2021, 3, 29, 15, 6, 43, tzinfo=timezone.utc)}]}, + "slices": [{"start": datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "end": datetime(2021, 3, 29, 15, 6, 43, tzinfo=timezone.utc)}, + {"start": datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "end": datetime(2022, 3, 29, 15, 6, 43, tzinfo=timezone.utc)}], + }, + {"created": 1648566403}, + id="epoch-overlapping-slices", + ), + pytest.param( + EpochValueConcurrentStreamStateConverter(), + { + "state_type": "date-range", + "slices": [{"start": datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "end": datetime(2021, 3, 29, 15, 6, 43, tzinfo=timezone.utc)}, + {"start": datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "end": datetime(2023, 3, 29, 15, 6, 43, tzinfo=timezone.utc)}], + }, {"created": 1617030403}, - id="with-input-state-epoch", + id="epoch-multiple-slices", ), pytest.param( IsoMillisConcurrentStreamStateConverter(), - AirbyteStream(name="stream1", json_schema={}, supported_sync_modes=[SyncMode.incremental]), - {"state_type": ConcurrencyCompatibleStateType.date_range.value}, - {}, - id="empty-input-state-isomillis", + { + "state_type": "date-range", + "slices": [{"start": datetime(1970, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + "end": datetime(2021, 3, 29, 15, 6, 43, tzinfo=timezone.utc)}], + }, + {"created": "2021-03-29T15:06:43.000Z"}, + id="isomillis-single-slice", ), pytest.param( IsoMillisConcurrentStreamStateConverter(), - AirbyteStream(name="stream1", json_schema={}, supported_sync_modes=[SyncMode.incremental]), { "state_type": "date-range", - "slices": [{"start": datetime(1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), - "end": datetime(2021, 8, 22, 5, 3, 27, tzinfo=timezone.utc)}]}, - {"created": "2021-08-22T05:03:27.000Z"}, - id="with-input-state-isomillis", + "slices": [{"start": datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "end": datetime(2021, 3, 29, 15, 6, 43, tzinfo=timezone.utc)}, + {"start": datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "end": datetime(2022, 3, 29, 15, 6, 43, tzinfo=timezone.utc)}], + }, + {"created": "2022-03-29T15:06:43.000Z"}, + id="isomillis-overlapping-slices", + ), + pytest.param( + IsoMillisConcurrentStreamStateConverter(), + { + "state_type": "date-range", + "slices": [{"start": datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "end": datetime(2021, 3, 29, 15, 6, 43, tzinfo=timezone.utc)}, + {"start": datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "end": datetime(2023, 3, 29, 15, 6, 43, tzinfo=timezone.utc)}], + }, + {"created": "2021-03-29T15:06:43.000Z"}, + id="isomillis-multiple-slices", ), ], ) -def test_convert_to_sequential_state(converter, stream, concurrent_state, expected_output_state): +def test_convert_to_sequential_state(converter, concurrent_state, expected_output_state): assert converter.convert_to_sequential_state(CursorField("created"), concurrent_state) == expected_output_state @pytest.mark.parametrize( - "converter, input_intervals, expected_merged_intervals", + "converter, concurrent_state, expected_output_state", [ pytest.param( EpochValueConcurrentStreamStateConverter(), - [], - [], - id="no-intervals-epoch", - ), - pytest.param( - EpochValueConcurrentStreamStateConverter(), - [{"start": 0, "end": 1}], - [{"start": 0, "end": 1}], - id="single-interval-epoch", - ), - pytest.param( - EpochValueConcurrentStreamStateConverter(), - [{"start": 0, "end": 1}, {"start": 0, "end": 1}], - [{"start": 0, "end": 1}], - id="duplicate-intervals-epoch", - ), - pytest.param( - EpochValueConcurrentStreamStateConverter(), - [{"start": 0, "end": 1}, {"start": 0, "end": 2}], - [{"start": 0, "end": 2}], - id="overlapping-intervals-epoch", - ), - pytest.param( - EpochValueConcurrentStreamStateConverter(), - [{"start": 0, "end": 3}, {"start": 1, "end": 2}], - [{"start": 0, "end": 3}], - id="enclosed-intervals-epoch", - ), - pytest.param( - EpochValueConcurrentStreamStateConverter(), - [{"start": 1, "end": 2}, {"start": 0, "end": 1}], - [{"start": 0, "end": 2}], - id="unordered-intervals-epoch", - ), - pytest.param( - EpochValueConcurrentStreamStateConverter(), - [{"start": 0, "end": 1}, {"start": 2, "end": 3}], - [{"start": 0, "end": 3}], - id="adjacent-intervals-epoch", - ), - pytest.param( - EpochValueConcurrentStreamStateConverter(), - [{"start": 3, "end": 4}, {"start": 0, "end": 1}], - [{"start": 0, "end": 1}, {"start": 3, "end": 4}], - id="nonoverlapping-intervals-epoch", - ), - pytest.param( - EpochValueConcurrentStreamStateConverter(), - [{"start": 0, "end": 1}, {"start": 2, "end": 3}, {"start": 10, "end": 11}, {"start": 1, "end": 4}], - [{"start": 0, "end": 4}, {"start": 10, "end": 11}], - id="overlapping-and-nonoverlapping-intervals-epoch", - ), - pytest.param( - IsoMillisConcurrentStreamStateConverter(), - [], - [], - id="no-intervals-isomillis", - ), - pytest.param( - IsoMillisConcurrentStreamStateConverter(), - [{"start": "2021-08-22T05:03:27.000Z", "end": "2022-08-22T05:03:27.000Z"}], - [{"start": "2021-08-22T05:03:27.000Z", "end": "2022-08-22T05:03:27.000Z"}], - id="single-interval-isomillis", - ), - pytest.param( - IsoMillisConcurrentStreamStateConverter(), - [{"start": "2021-08-22T05:03:27.000Z", "end": "2022-08-22T05:03:27.000Z"}, - {"start": "2021-08-22T05:03:27.000Z", "end": "2022-08-22T05:03:27.000Z"}], - [{"start": "2021-08-22T05:03:27.000Z", "end": "2022-08-22T05:03:27.000Z"}], - id="duplicate-intervals-isomillis", - ), - pytest.param( - IsoMillisConcurrentStreamStateConverter(), - [{"start": "2021-08-22T05:03:27.000Z", "end": "2023-08-22T05:03:27.000Z"}, - {"start": "2021-08-22T05:03:27.000Z", "end": "2022-08-22T05:03:27.000Z"}], - [{"start": "2021-08-22T05:03:27.000Z", "end": "2023-08-22T05:03:27.000Z"}], - id="overlapping-intervals-isomillis", - ), - pytest.param( - IsoMillisConcurrentStreamStateConverter(), - [{"start": "2021-08-22T05:03:27.000Z", "end": "2024-08-22T05:03:27.000Z"}, - {"start": "2022-08-22T05:03:27.000Z", "end": "2023-08-22T05:03:27.000Z"}], - [{"start": "2021-08-22T05:03:27.000Z", "end": "2024-08-22T05:03:27.000Z"}], - id="enclosed-intervals-isomillis", - ), - pytest.param( - IsoMillisConcurrentStreamStateConverter(), - [{"start": "2023-08-22T05:03:27.000Z", "end": "2024-08-22T05:03:27.000Z"}, - {"start": "2021-08-22T05:03:27.000Z", "end": "2022-08-22T05:03:27.000Z"}], - [{"start": 0, "end": 2}], - id="unordered-intervals-isomillis", - ), - pytest.param( - IsoMillisConcurrentStreamStateConverter(), - [{"start": "2021-08-22T05:03:27.000Z", "end": "2022-08-22T05:03:27.000Z"}, - {"start": "2022-08-22T05:03:27.001Z", "end": "2023-08-22T05:03:27.000Z"}], - [{"start": "2021-08-22T05:03:27.000Z", "end": "2023-08-22T05:03:27.000Z"}], - id="adjacent-intervals-isomillis", - ), - pytest.param( - IsoMillisConcurrentStreamStateConverter(), - [{"start": "2023-08-22T05:03:27.000Z", "end": "2024-08-22T05:03:27.000Z"}, - {"start": "2021-08-22T05:03:27.000Z", "end": "2022-08-22T05:03:27.000Z"}], - [{"start": "2021-08-22T05:03:27.000Z", "end": "2022-08-22T05:03:27.000Z"}, - {"start": "2023-08-22T05:03:27.000Z", "end": "2024-08-22T05:03:27.000Z"}], - id="nonoverlapping-intervals-isomillis", + { + "state_type": ConcurrencyCompatibleStateType.date_range.value, + "start": EpochValueConcurrentStreamStateConverter().zero_value, + }, + {"created": 0}, + id="empty-slices-epoch", ), pytest.param( IsoMillisConcurrentStreamStateConverter(), - [{"start": "2021-08-22T05:03:27.000Z", "end": "2022-08-22T05:03:27.000Z"}, - {"start": "2022-08-22T05:03:27.001Z", "end": "2023-08-22T05:03:27.000Z"}, - {"start": "2027-08-22T05:03:27.000Z", "end": "2028-08-22T05:03:27.000Z"}, - {"start": "2022-08-22T05:03:27.000Z", "end": "2025-08-22T05:03:27.000Z"}], - [{"start": "2021-08-22T05:03:27.000Z", "end": "2025-08-22T05:03:27.000Z"}, - {"start": "2027-08-22T05:03:27.000Z", "end": "2028-08-22T05:03:27.000Z"}], - id="overlapping-and-nonoverlapping-intervals-isomillis", + { + "state_type": ConcurrencyCompatibleStateType.date_range.value, + "start": datetime(2021, 8, 22, 5, 3, 27, tzinfo=timezone.utc), + }, + {"created": "2021-08-22T05:03:27.000Z"}, + id="empty-slices-isomillis", ), ], ) -def test_merge_intervals(converter, input_intervals, expected_merged_intervals): - parsed_intervals = [{"start": converter.parse_timestamp(i["start"]), "end": converter.parse_timestamp(i["end"])} for i in input_intervals] - return converter.merge_intervals(parsed_intervals) == expected_merged_intervals +def test_convert_to_sequential_state_no_slices_returns_legacy_state(converter, concurrent_state, expected_output_state): + with pytest.raises(RuntimeError): + converter.convert_to_sequential_state(CursorField("created"), concurrent_state) From 1d6e628ba13b1ac6c66a7bdc85947c0a813b78cf Mon Sep 17 00:00:00 2001 From: clnoll Date: Thu, 18 Jan 2024 16:47:14 +0000 Subject: [PATCH 142/574] =?UTF-8?q?=F0=9F=A4=96=20Bump=20minor=20version?= =?UTF-8?q?=20of=20Python=20CDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airbyte-cdk/python/.bumpversion.cfg | 2 +- airbyte-cdk/python/CHANGELOG.md | 3 +++ airbyte-cdk/python/Dockerfile | 2 +- airbyte-cdk/python/setup.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/airbyte-cdk/python/.bumpversion.cfg b/airbyte-cdk/python/.bumpversion.cfg index 55b6678f0508c..641822eeb211e 100644 --- a/airbyte-cdk/python/.bumpversion.cfg +++ b/airbyte-cdk/python/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.58.9 +current_version = 0.59.0 commit = False [bumpversion:file:setup.py] diff --git a/airbyte-cdk/python/CHANGELOG.md b/airbyte-cdk/python/CHANGELOG.md index 7570b34ceb628..a02b1f1151427 100644 --- a/airbyte-cdk/python/CHANGELOG.md +++ b/airbyte-cdk/python/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 0.59.0 +Fix state message handling when running concurrent syncs + ## 0.58.9 concurrent-cdk: improve resource usage when reading from substreams diff --git a/airbyte-cdk/python/Dockerfile b/airbyte-cdk/python/Dockerfile index 4d6ce8c3684e4..f095029dbacec 100644 --- a/airbyte-cdk/python/Dockerfile +++ b/airbyte-cdk/python/Dockerfile @@ -32,5 +32,5 @@ ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] # needs to be the same as CDK -LABEL io.airbyte.version=0.58.9 +LABEL io.airbyte.version=0.59.0 LABEL io.airbyte.name=airbyte/source-declarative-manifest diff --git a/airbyte-cdk/python/setup.py b/airbyte-cdk/python/setup.py index 627bd437c36a5..483305c344693 100644 --- a/airbyte-cdk/python/setup.py +++ b/airbyte-cdk/python/setup.py @@ -36,7 +36,7 @@ name="airbyte-cdk", # The version of the airbyte-cdk package is used at runtime to validate manifests. That validation must be # updated if our semver format changes such as using release candidate versions. - version="0.58.9", + version="0.59.0", description="A framework for writing Airbyte Connectors.", long_description=README, long_description_content_type="text/markdown", From 14c61991c671d6022b93225207c8ca9c4666f880 Mon Sep 17 00:00:00 2001 From: Natalie Kwong <38087517+nataliekwong@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:15:26 -0800 Subject: [PATCH 143/574] [Docs] Update Getting Started (#34237) Co-authored-by: Tim Roes --- .../getting-started-destination-page.png | Bin 136895 -> 0 bytes .../getting-started-source-page.png | Bin 161204 -> 0 bytes .../getting-started-connection-config.png | Bin 69878 -> 0 bytes .../getting-started-connection-streams.png | Bin 41673 -> 0 bytes .../getting-started-connection-success.png | Bin 60403 -> 0 bytes .../assets/connection-job-history.png | Bin 0 -> 165704 bytes .../assets/connection-status-page.png | Bin 0 -> 127481 bytes .../configuring-connections.md | 17 +++++---- .../manage-connection-state.md | 2 +- .../manage-data-residency.md | 14 +++---- .../manage-schema-changes.md | 2 +- .../review-connection-status.md | 2 + .../review-sync-history.md | 4 +- docs/operator-guides/browsing-output-logs.md | 2 +- docs/operator-guides/reset.md | 2 +- .../using-airbyte/core-concepts/namespaces.md | 24 +++++------- .../core-concepts/sync-schedules.md | 26 ++++++++----- .../getting-started/add-a-destination.md | 34 +++++++++++++---- .../getting-started/add-a-source.md | 15 ++++---- .../getting-started-connection-complete.png | Bin 0 -> 144567 bytes ...tting-started-connection-configuration.png | Bin 0 -> 185254 bytes .../getting-started-destination-list.png | Bin .../assets/getting-started-faker-source.png | Bin 0 -> 281905 bytes ...ting-started-google-sheets-destination.png | Bin 0 -> 294005 bytes .../assets}/getting-started-source-list.png | Bin .../getting-started-stream-selection.png | Bin 0 -> 168483 bytes docs/using-airbyte/getting-started/readme.md | 22 ++++++++--- .../getting-started/set-up-a-connection.md | 35 ++++++++++++------ docusaurus/sidebars.js | 18 +++++++-- docusaurus/src/components/Arcade.jsx | 7 ++++ docusaurus/src/theme/MDXComponents/index.js | 2 + 31 files changed, 150 insertions(+), 78 deletions(-) delete mode 100644 docs/.gitbook/assets/add-a-destination/getting-started-destination-page.png delete mode 100644 docs/.gitbook/assets/add-a-source/getting-started-source-page.png delete mode 100644 docs/.gitbook/assets/set-up-a-connection/getting-started-connection-config.png delete mode 100644 docs/.gitbook/assets/set-up-a-connection/getting-started-connection-streams.png delete mode 100644 docs/.gitbook/assets/set-up-a-connection/getting-started-connection-success.png create mode 100644 docs/cloud/managing-airbyte-cloud/assets/connection-job-history.png create mode 100644 docs/cloud/managing-airbyte-cloud/assets/connection-status-page.png create mode 100644 docs/using-airbyte/getting-started/assets/getting-started-connection-complete.png create mode 100644 docs/using-airbyte/getting-started/assets/getting-started-connection-configuration.png rename docs/{.gitbook/assets/add-a-destination => using-airbyte/getting-started/assets}/getting-started-destination-list.png (100%) create mode 100644 docs/using-airbyte/getting-started/assets/getting-started-faker-source.png create mode 100644 docs/using-airbyte/getting-started/assets/getting-started-google-sheets-destination.png rename docs/{.gitbook/assets/add-a-source => using-airbyte/getting-started/assets}/getting-started-source-list.png (100%) create mode 100644 docs/using-airbyte/getting-started/assets/getting-started-stream-selection.png create mode 100644 docusaurus/src/components/Arcade.jsx diff --git a/docs/.gitbook/assets/add-a-destination/getting-started-destination-page.png b/docs/.gitbook/assets/add-a-destination/getting-started-destination-page.png deleted file mode 100644 index 16c15dadfe813d20156c1ecd322309d4e0a97bf4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 136895 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≶`X`J?_ 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

Bp=SZ26RemN4Mpe?#>ehNqEir0C*02Y- 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≦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 diff --git a/docs/.gitbook/assets/set-up-a-connection/getting-started-connection-config.png b/docs/.gitbook/assets/set-up-a-connection/getting-started-connection-config.png deleted file mode 100644 index 2f800ab71b843472f7291346acc3f7994bb7a854..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69878 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�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 diff --git a/docs/.gitbook/assets/set-up-a-connection/getting-started-connection-success.png b/docs/.gitbook/assets/set-up-a-connection/getting-started-connection-success.png deleted file mode 100644 index 7bf01c37f1f6d103e164ef5b1f23de4255999395..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60403 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�hJq=E@R4T&R{-otiEB=uRI`5Jg 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%0Dq(^*fTD0PI3 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 z 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

iQ{6?fi&>Qog91PC~G60YDWzEgd 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

kR0Y}ZO}%y2>_2PdGHSGl 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 aNuyfEp4KV0NgV+Hu3x=%B~S5Q!2bY0!E!YK literal 0 HcmV?d00001 diff --git a/docs/using-airbyte/getting-started/assets/getting-started-connection-configuration.png b/docs/using-airbyte/getting-started/assets/getting-started-connection-configuration.png new file mode 100644 index 0000000000000000000000000000000000000000..92921edd1dc47c2435f4f6446135dea9b43352eb GIT binary patch literal 185254 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úF(Y@gKNNOb7HP%^CAdZ%ThXB`!ul09SjaP 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

g-d30%JF~Wt4?T=jcE7?Z@(yx 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
  • B(a$~s|4ImGTRx`^I8)& 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 L*`0KgxI5urNjUFy literal 0 HcmV?d00001 diff --git a/docs/.gitbook/assets/add-a-destination/getting-started-destination-list.png b/docs/using-airbyte/getting-started/assets/getting-started-destination-list.png similarity index 100% rename from docs/.gitbook/assets/add-a-destination/getting-started-destination-list.png rename to docs/using-airbyte/getting-started/assets/getting-started-destination-list.png diff --git a/docs/using-airbyte/getting-started/assets/getting-started-faker-source.png b/docs/using-airbyte/getting-started/assets/getting-started-faker-source.png new file mode 100644 index 0000000000000000000000000000000000000000..ed8b9db12effd34de3803d9c8edc1e9db4310878 GIT binary patch literal 281905 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%0DF)Qt$ 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

    }yry1p|5LX*Vz&n7MOcw_=iwPKDYI(RBGVn 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 Uz?bAKIPgbS@|i?|n4$mw0f}wowg3PC literal 0 HcmV?d00001 diff --git a/docs/using-airbyte/getting-started/assets/getting-started-google-sheets-destination.png b/docs/using-airbyte/getting-started/assets/getting-started-google-sheets-destination.png new file mode 100644 index 0000000000000000000000000000000000000000..03c6a2aade971682679ff6be870df2a502eec307 GIT binary patch literal 294005 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/.gitbook/assets/add-a-source/getting-started-source-list.png b/docs/using-airbyte/getting-started/assets/getting-started-source-list.png similarity index 100% rename from docs/.gitbook/assets/add-a-source/getting-started-source-list.png rename to docs/using-airbyte/getting-started/assets/getting-started-source-list.png diff --git a/docs/using-airbyte/getting-started/assets/getting-started-stream-selection.png b/docs/using-airbyte/getting-started/assets/getting-started-stream-selection.png new file mode 100644 index 0000000000000000000000000000000000000000..fc7cc81d0ddc1983282e97bb74429a3ab84842f5 GIT binary patch literal 168483 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 literal 0 HcmV?d00001 diff --git a/docs/using-airbyte/getting-started/readme.md b/docs/using-airbyte/getting-started/readme.md index 62715935cf1b9..72d422d7b1b57 100644 --- a/docs/using-airbyte/getting-started/readme.md +++ b/docs/using-airbyte/getting-started/readme.md @@ -6,7 +6,11 @@ products: all Getting started with Airbyte takes only a few steps! This page guides you through the initial steps to get started and you'll learn how to setup your first connection on the following pages. -You have two options to run Airbyte: Use **Airbyte Cloud** (recommended) or **self-host Airbyte** in your infrastructure. +You have two options to run Airbyte: Use **Airbyte Cloud** (recommended) or **self-manage Airbyte** in your infrastructure. + +:::tip +If you are have already deployed Airbyte or signed up for Airbyte Cloud, jump ahead to [set up a source](./add-a-source.md). +::: ## Sign Up for Airbyte Cloud @@ -16,13 +20,14 @@ Airbyte Cloud offers a 14-day free trial that begins after your first successful To start setting up a data pipeline, see how to [set up a source](./add-a-source.md). -:::info -Depending on your data residency, you may need to [allowlist IP addresses](/operating-airbyte/security.md#network-security-1) to enable access to Airbyte. -::: -## Deploy Airbyte (Open Source) +## Deploy Airbyte (Self-Managed) + +When self-managing Airbyte, your data never leaves your premises. Get started immediately by deploying locally using Docker. -To use Airbyte Open Source, you can use on the following options to deploy it on your infrastructure. +### Self-Managed Community (Open Source) + +With Airbyte Self-Managed Community (Open Source), you can use one of the following options in your infrastructure: - [Local Deployment](/deploying-airbyte/local-deployment.md) (recommended when trying out Airbyte) - [On Aws](/deploying-airbyte/on-aws-ec2.md) @@ -34,3 +39,8 @@ To use Airbyte Open Source, you can use on the following options to deploy it on - [On Restack](/deploying-airbyte/on-restack.md) - [On Plural](/deploying-airbyte/on-plural.md) - [On AWS ECS](/deploying-airbyte/on-aws-ecs.md) (Spoiler alert: it doesn't work) + +### Self-Managed Enterprise +Airbyte Self-Managed Enterprise is the best way to run Airbyte yourself. You get all 300+ pre-built connectors, data never leaves your environment, and Airbyte becomes self-serve in your organization with new tools to manage multiple users, and multiple teams using Airbyte all in one place. + +To start with Self-Managed Enterprrise, navigate to our [Enterprise setup guide](/enterprise-setup/README.md). diff --git a/docs/using-airbyte/getting-started/set-up-a-connection.md b/docs/using-airbyte/getting-started/set-up-a-connection.md index d2c7780823bbb..c27c77c16f974 100644 --- a/docs/using-airbyte/getting-started/set-up-a-connection.md +++ b/docs/using-airbyte/getting-started/set-up-a-connection.md @@ -2,17 +2,20 @@ products: all --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + # Set up a Connection Now that you've learned how to set up your first [source](./add-a-source) and [destination](./add-a-destination), it's time to finish the job by creating your very first connection! -On the left side of your main Airbyte dashboard, select **Connections**. You will be prompted to choose which source and destination to use for this connection. As an example, we'll use the **Google Sheets** source and **Local JSON** destination. +On the left side of your main Airbyte dashboard, select **Connections**. You will be prompted to choose which source and destination to use for this connection. For this example, we'll use the **Google Sheets** source and the destination you previously set up, either **Local JSON** or **Google Sheets**. ## Configure the connection Once you've chosen your source and destination, you'll be able to configure the connection. You can refer to [this page](/cloud/managing-airbyte-cloud/configuring-connections.md) for more information on each available configuration. For this demo, we'll simply set the **Replication frequency** to a 24 hour interval and leave the other fields at their default values. -![Connection config](../../.gitbook/assets/set-up-a-connection/getting-started-connection-config.png) +![Connection config](./assets/getting-started-connection-configuration.png) :::note By default, data will sync to the default defined in the destination. To ensure your data is synced to the correct place, see our examples for [Destination Namespace](/using-airbyte/core-concepts/namespaces.md) @@ -20,9 +23,9 @@ By default, data will sync to the default defined in the destination. To ensure Next, you can toggle which streams you want to replicate, as well as setting up the desired sync mode for each stream. For more information on the nature of each sync mode supported by Airbyte, see [this page](/using-airbyte/core-concepts/sync-modes). -Our test data consists of a single stream cleverly named `Test Data`, which we've enabled and set to `Full Refresh - Overwrite` sync mode. +Our test data consists of three streams, which we've enabled and set to `Incremental - Append + Deduped` sync mode. -![Stream config](../../.gitbook/assets/set-up-a-connection/getting-started-connection-streams.png) +![Stream config](./assets/getting-started-stream-selection.png) Click **Set up connection** to complete your first connection. Your first sync is about to begin! @@ -30,34 +33,44 @@ Click **Set up connection** to complete your first connection. Your first sync i Once you've finished setting up the connection, you will be automatically redirected to a connection overview containing all the tools you need to keep track of your connection. -![Connection dashboard](../../.gitbook/assets/set-up-a-connection/getting-started-connection-success.png) +![Connection dashboard](./assets/getting-started-connection-complete.png) Here's a basic overview of the tabs and their use: 1. The **Status** tab shows you an overview of your connector's sync health. 2. The **Job History** tab allows you to check the logs for each sync. If you encounter any errors or unexpected behaviors during a sync, checking the logs is always a good first step to finding the cause and solution. 3. The **Replication** tab allows you to modify the configurations you chose during the connection setup. +4. The **Transformation** tab allows you to set up a custom post-sync transformations using dbt. 4. The **Settings** tab contains additional settings, and the option to delete the connection if you no longer wish to use it. ### Check the data from your first sync Once the first sync has completed, you can verify the sync has completed by checking the data in your destination. -If you followed along and created your own connection using a `Local JSON` destination, you can use this command to check the file's contents to make sure the replication worked as intended (be sure to replace YOUR_PATH with the path you chose in your destination setup, and YOUR_STREAM_NAME with the name of an actual stream you replicated): + + + If you followed along and created your own connection using a **Google Sheets** destination, you will now see three tabs created in your Google Sheet, `products`, `users`, and `purchases`. + + + + If you followed along and created your own connection using a `Local JSON` destination, you can use this command to check the file's contents to make sure the replication worked as intended (be sure to replace YOUR_PATH with the path you chose in your destination setup, and YOUR_STREAM_NAME with the name of an actual stream you replicated): -```bash -cat /tmp/airbyte_local/YOUR_PATH/_airbyte_raw_YOUR_STREAM_NAME.jsonl -``` + ```bash + cat /tmp/airbyte_local/YOUR_PATH/_airbyte_raw_YOUR_STREAM_NAME.jsonl + ``` -You should see a list of JSON objects, each containing a unique `airbyte_ab_id`, an `emitted_at` timestamp, and `airbyte_data` containing the extracted record. + You should see a list of JSON objects, each containing a unique `airbyte_ab_id`, an `emitted_at` timestamp, and `airbyte_data` containing the extracted record. :::tip If you are using Airbyte on Windows with WSL2 and Docker, refer to [this guide](/integrations/locating-files-local-destination.md) to locate the replicated folder and file. ::: + + + ## What's next? -Congratulations on successfully setting up your first connection using Airbyte Open Source! We hope that this will be just the first step on your journey with us. We support a large, ever-growing [catalog of sources and destinations](/integrations/), and you can even [contribute your own](/connector-development/). +Congratulations on successfully setting up your first connection using Airbyte! We hope that this will be just the first step on your journey with us. We support a large, ever-growing [catalog of sources and destinations](/integrations/), and you can even [contribute your own](/connector-development/). If you have any questions at all, please reach out to us on [Slack](https://slack.airbyte.io/). If you would like to see a missing feature or connector added, please create an issue on our [Github](https://github.com/airbytehq/airbyte). Our community's participation is invaluable in helping us grow and improve every day, and we always welcome your feedback. diff --git a/docusaurus/sidebars.js b/docusaurus/sidebars.js index e6d5340725fee..30c0e69d0d926 100644 --- a/docusaurus/sidebars.js +++ b/docusaurus/sidebars.js @@ -441,11 +441,18 @@ module.exports = { "using-airbyte/core-concepts/sync-modes/full-refresh-overwrite", ], }, - "using-airbyte/core-concepts/typing-deduping", - "using-airbyte/core-concepts/basic-normalization", + { + type: "category", + label: "Typing and Deduping", + link: { + type: "doc", + id: "using-airbyte/core-concepts/typing-deduping" + }, + items: [ + "using-airbyte/core-concepts/basic-normalization" + ], + }, "cloud/managing-airbyte-cloud/manage-schema-changes", - "cloud/managing-airbyte-cloud/manage-data-residency", - "cloud/managing-airbyte-cloud/manage-connection-state", { type: "category", label: "Transformations", @@ -466,16 +473,19 @@ module.exports = { "cloud/managing-airbyte-cloud/review-sync-history", "operator-guides/browsing-output-logs", "operator-guides/reset", + "cloud/managing-airbyte-cloud/manage-connection-state", ], }, { type: "category", label: "Workspace Management", items: [ + "cloud/managing-airbyte-cloud/manage-data-residency", "using-airbyte/workspaces", "cloud/managing-airbyte-cloud/manage-airbyte-cloud-notifications", "cloud/managing-airbyte-cloud/manage-credits", "operator-guides/using-custom-connectors", + ] }, sectionHeader("Managing Airbyte"), diff --git a/docusaurus/src/components/Arcade.jsx b/docusaurus/src/components/Arcade.jsx new file mode 100644 index 0000000000000..a2f3d2d646ef0 --- /dev/null +++ b/docusaurus/src/components/Arcade.jsx @@ -0,0 +1,7 @@ +export const Arcade = (props) => { + return ( +

    +
  • )|xgCQl2g{ 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 ex^`mmByxMe0pk0~sH%P7pS Custom format for more details | @@ -58,13 +56,17 @@ When replicating multiple sources into the same destination, you may create tabl For example, a Github source can be replicated into a `github` schema. However, you may have multiple connections writing from different GitHub repositories \(common in multi-tenant scenarios\). :::tip -To keep the same table names, Airbyte recommends writing the connections to unique namespaces to avoid mixing data from the different GitHub repositories. +To write more than 1 table with the same name to your destination, Airbyte recommends writing the connections to unique namespaces to avoid mixing data from the different GitHub repositories. ::: You can enter plain text (most common) or additionally add a dynamic parameter `${SOURCE_NAMESPACE}`, which uses the namespace provided by the source if available. ### Examples +:::info +If the Source does not support namespaces, the data will be replicated into the Destination's default namespace. If the Destination does not support namespaces, any preference set in the connection is ignored. +::: + The following table summarises how this works. In this example, we're looking at the replication configuration between a Postgres Source and Snowflake Destination \(with settings of schema = "my\_schema"\): | Namespace Configuration | Source Namespace | Source Table Name | Destination Namespace | Destination Table Name | @@ -78,21 +80,15 @@ The following table summarises how this works. In this example, we're looking at | Custom format = `"my\_${SOURCE\_NAMESPACE}\_schema"` | public | my\_table | my\_public\_schema | my\_table | | Custom format = " " | public | my\_table | my\_schema | my\_table | -## Syncing Details - -If the Source does not support namespaces, the data will be replicated into the Destination's default namespace. For databases, the default namespace is the schema provided in the destination configuration. - -If the Destination does not support namespaces, any preference set in the connection is ignored. - ## Using Namespaces with Basic Normalization -As part of the connections sync settings, it is possible to configure the namespace used by: 1. destination connectors: to store the `_airbyte_raw_*` tables. 2. basic normalization: to store the final normalized tables. +As part of the connection settings, it is possible to configure the namespace used by: 1. destination connectors: to store the `_airbyte_raw_*` tables. 2. basic normalization: to store the final normalized tables. -:::info When basic normalization is enabled, this is the location that both your normalized and raw data will get written to. Your raw data will show up with the prefix `_airbyte_raw_` in the namespace you define. If you don't enable basic normalization, you will only receive the raw tables. -:::note +:::note Note custom transformation outputs are not affected by the namespace settings from Airbyte: It is up to the configuration of the custom dbt project, and how it is written to handle its [custom schemas](https://docs.getdbt.com/docs/building-a-dbt-project/building-models/using-custom-schemas). The default target schema for dbt in this case, will always be the destination namespace. +::: ## Requirements diff --git a/docs/using-airbyte/core-concepts/sync-schedules.md b/docs/using-airbyte/core-concepts/sync-schedules.md index 358a127b27155..c4514d9413968 100644 --- a/docs/using-airbyte/core-concepts/sync-schedules.md +++ b/docs/using-airbyte/core-concepts/sync-schedules.md @@ -14,7 +14,11 @@ For each connection, you can select between three options that allow a sync to r * Only one sync per connection can run at a time. * If a sync is scheduled to run before the previous sync finishes, the scheduled sync will start after the completion of the previous sync. -* Syncs can run at most every 60 minutes. Reach out to [Sales](https://airbyte.com/company/talk-to-sales) if you require replication more frequently than once per hour. +* Syncs can run at most every 60 minutes in Airbyte Cloud. Reach out to [Sales](https://airbyte.com/company/talk-to-sales) if you require replication more frequently than once per hour. + +:::note +For Scheduled or cron scheduled syncs, Airbyte guarantees syncs will initiate with a schedule accuracy of +/- 30 minutes. +::: ## Scheduled syncs When a scheduled connection is first created, a sync is executed immediately after creation. After that, a sync is run once the time since the last sync \(whether it was triggered manually or due to a schedule\) has exceeded the schedule interval. For example: @@ -27,17 +31,21 @@ When a scheduled connection is first created, a sync is executed immediately aft - **October 3rd, 5:01pm:** It has been more than 24 hours since the last sync, so a sync is run ## Cron Scheduling -If you prefer more flexibility in scheduling your sync, you can also use CRON scheduling to set a precise time of day or month. +If you prefer more precision in scheduling your sync, you can also use CRON scheduling to set a specific time of day or month. -Airbyte uses the CRON scheduler from [Quartz](http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html). We recommend reading their [documentation](http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html) to learn more about how to +Airbyte uses the CRON scheduler from [Quartz](http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html). We recommend reading their [documentation](http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html) to understand the required formatting. You can also refer to these examples: -When setting up the cron extpression, you will also be asked to choose a time zone the sync will run in. - -:::note -For Scheduled or cron scheduled syncs, Airbyte guarantees syncs will initiate with a schedule accuracy of +/- 30 minutes. -::: +| Cron string | Sync Timing| +| - | - | +| 0 0 * * * ? | Every hour, at 0 minutes past the hour | +| 0 0 15 * * ? | At 15:00 every day | +| 0 0 15 * * MON,TUE | At 15:00, only on Monday and Tuesday | +| 0 0 0,2,4,6 * * ? | At 12:00 AM, 02:00 AM, 04:00 AM and 06:00 AM every day | +| 0 0 */15 * * ? | At 0 minutes past the hour, every 15 hours | + +When setting up the cron expression, you will also be asked to choose a time zone the sync will run in. ## Manual Syncs When the connection is set to replicate with `Manual` frequency, the sync will not automatically run. -It can be triggered by clicking the "Sync Now" button at any time through the UI or be triggered through the UI. \ No newline at end of file +It can be triggered by clicking the "Sync Now" button at any time through the UI or be triggered through the API. \ No newline at end of file diff --git a/docs/using-airbyte/getting-started/add-a-destination.md b/docs/using-airbyte/getting-started/add-a-destination.md index d8957382efc6b..4aa05d8970f2f 100644 --- a/docs/using-airbyte/getting-started/add-a-destination.md +++ b/docs/using-airbyte/getting-started/add-a-destination.md @@ -2,13 +2,16 @@ products: all --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + # Add a Destination Destinations are the data warehouses, data lakes, databases and analytics tools where you will load the data from your chosen source(s). The steps to setting up your first destination are very similar to those for [setting up a source](./add-a-source). -Once you've logged in to your Airbyte Open Source deployment, click on the **Destinations** tab in the navigation bar found on the left side of the dashboard. This will take you to the list of available destinations. +Once you've signed up for Airbyte Cloud or logged in to your Airbyte Open Source deployment, click on the **Destinations** tab in the navigation bar found on the left side of the dashboard. This will take you to the list of available destinations. -![Destination List](../../.gitbook/assets/add-a-destination/getting-started-destination-list.png) +![Destination List](./assets/getting-started-destination-list.png) You can use the provided search bar at the top of the page, or scroll down the list to find the destination you want to replicate data from. @@ -16,17 +19,34 @@ You can use the provided search bar at the top of the page, or scroll down the l You can filter the list of destinations by support level. Airbyte connectors are categorized in two support levels, Certified and Community. See our [Connector Support Levels](/integrations/connector-support-levels.md) page for more information on this topic. ::: -As an example, we'll be setting up a simple JSON file that will be saved on our local system as the destination. Select **Local JSON** from the list of destinations. This will take you to the destination setup page. + + + As an example, we'll be setting up a simple Google Sheets spreadsheet that will move data to a Google Sheet. Select **Google Sheets** from the list of destinations. This will take you to the destination setup page. + + ![Destination Page](./assets/getting-started-google-sheets-destination.png) -![Destination Page](../../.gitbook/assets/add-a-destination/getting-started-destination-page.png) +:::info +Google Sheets imposes rate limits and hard limits on the amount of data it can receive. Only use Google Sheets as a destination for small, non-production use cases, as it is not designed for handling large-scale data operations. + +Read more about the [specific limitations](/integrations/destinations/google-sheets.md#limitations) in our Google Sheets documentation. +::: -The left half of the page contains a set of fields that you will have to fill out. In the **Destination name** field, you can enter a name of your choosing to help you identify this instance of the connector. By default, this will be set to the name of the destination (i.e., `Local JSON`). + The left half of the page contains a set of fields that you will have to fill out. In the **Destination name** field, you can enter a name of your choosing to help you identify this instance of the connector. By default, this will be set to the name of the destination (i.e., `Google Sheets`). -Because this is a simple JSON file, there is only one more required field, **Destination Path**. This is the path in your local filesystem where the JSON file containing your data will be saved. In our example, if we set the path to `/my_first_destination`, the file will be saved in `/tmp/airbyte_local/my_first_destination`. + Authenticate into your Google account by clicking "Sign in with Google" and granting permissions to Airbyte. Because this is a simple Google Sheets destination, there is only one more required field, **Spreadsheet Link**. This is the path to your spreadsheet that can be copied directly from your browser. + + + As an example, we'll be setting up a simple JSON file that will be saved on our local system as the destination. Select **Local JSON** from the list of destinations. This will take you to the destination setup page. + + The left half of the page contains a set of fields that you will have to fill out. In the **Destination name** field, you can enter a name of your choosing to help you identify this instance of the connector. By default, this will be set to the name of the destination (i.e., `Local JSON`). + + Because this is a simple JSON file, there is only one more required field, **Destination Path**. This is the path in your local filesystem where the JSON file containing your data will be saved. In our example, if we set the path to `/my_first_destination`, the file will be saved in `/tmp/airbyte_local/my_first_destination`. + + Each destination will have its own set of required fields to configure during setup. You can refer to your destination's provided setup guide on the right side of the page for specific details on the nature of each field. -:::info +:::tip Some destinations will also have an **Optional Fields** tab located beneath the required fields. You can open this tab to view and configure any additional optional parameters that exist for the source. These fields generally grant you more fine-grained control over your data replication, but you can safely ignore them. ::: diff --git a/docs/using-airbyte/getting-started/add-a-source.md b/docs/using-airbyte/getting-started/add-a-source.md index 92b7798bba937..15f4ce57bd615 100644 --- a/docs/using-airbyte/getting-started/add-a-source.md +++ b/docs/using-airbyte/getting-started/add-a-source.md @@ -6,21 +6,22 @@ products: all Setting up a new source in Airbyte is a quick and simple process! When viewing the Airbyte UI, you'll see the main navigation bar on the left side of your screen. Click the **Sources** tab to bring up a list of all available sources. -![](../../.gitbook/assets/add-a-source/getting-started-source-list.png) + -You can use the provided search bar, or simply scroll down the list to find the source you want to replicate data from. Let's use Google Sheets as an example. Clicking on the **Google Sheets** card will bring us to its setup page. -![](../../.gitbook/assets/add-a-source/getting-started-source-page.png) +You can use the provided search bar, or simply scroll down the list to find the source you want to replicate data from. Let's use a demo source, Faker, as an example. Clicking on the **Sample Data (Faker)** card will bring us to its setup page. -The left half of the page contains a set of fields that you will have to fill out. In the **Source name** field, you can enter a name of your choosing to help you identify this instance of the connector. By default, this will be set to the name of the source (ie, `Google Sheets`). +![](./assets/getting-started-faker-source.png) -Each connector in Airbyte will have its own set of authentication methods and configurable parameters. In the case of Google Sheets, you can select one of two authentication methods (OAuth2.0 or a Service Account Key), and must provide the link to the Google Sheet you want to replicate. You can always refer to your source's provided setup guide for specific instructions on filling out each field. +The left half of the page contains a set of fields that you will have to fill out. In the **Source name** field, you can enter a name of your choosing to help you identify this instance of the connector. By default, this will be set to the name of the source (ie, `Sample Data (Faker)`). + +Each connector in Airbyte will have its own set of authentication methods and configurable parameters. In the case of Sample Data (Faker), you can adjust the number of records you want returned in your `Users` data, and optionally adjust additional configuration settings. You can always refer to your source's provided setup guide for specific instructions on filling out each field. :::info -Some sources will also have an **Optional Fields** tab. You can open this tab to view and configure any additional optional parameters that exist for the souce, but you do not have to do so to successfully set up the connector. +Some sources will have an **Optional Fields** tab. You can open this tab to view and configure any additional optional parameters that exist for the souce, but you do not have to do so to successfully set up the connector. ::: Once you've filled out all the required fields, click on the **Set up source** button and Airbyte will run a check to verify the connection. Happy replicating! -Can't find the connectors that you want? Try your hand at easily building one yourself using our [Connector Builder!](../../connector-development/connector-builder-ui/overview.md) +Can't find the connectors that you want? Try your hand at easily building one yourself using our [Connector Builder](../../connector-development/connector-builder-ui/overview.md)! diff --git a/docs/using-airbyte/getting-started/assets/getting-started-connection-complete.png b/docs/using-airbyte/getting-started/assets/getting-started-connection-complete.png new file mode 100644 index 0000000000000000000000000000000000000000..f034a559f0263527c11afc1f0fde9d330236f88e GIT binary patch literal 144567 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