From c80383dd1a92a6fd5da935f5cd315e854292bbfc Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 24 Jan 2024 21:45:00 +0000 Subject: [PATCH 01/19] feat(docs): Add documentation on Incident Change Event (#9709) --- .../datahub-api/entity-events-api.md | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/managed-datahub/datahub-api/entity-events-api.md b/docs/managed-datahub/datahub-api/entity-events-api.md index 07fa252249452..23499904d5505 100644 --- a/docs/managed-datahub/datahub-api/entity-events-api.md +++ b/docs/managed-datahub/datahub-api/entity-events-api.md @@ -563,7 +563,7 @@ This event is emitted when an Assertion has been run has succeeded on DataHub. "parameters": { "runResult": "SUCCESS", "runId": "123", - "aserteeUrn": "urn:li:dataset:def" + "asserteeUrn": "urn:li:dataset:def" }, "auditStamp": { "actor": "urn:li:corpuser:jdoe", @@ -808,4 +808,36 @@ These are the common parameters for all parameters. "time": 1649953100653 } } -``` \ No newline at end of file +``` + +### Incident Change Event + +This event is emitted when an Incident has been created or it's status changes. + +#### Header + +
CategoryOperationEntity Types
INCIDENTACTIVE, RESOLVEDincident
+ +#### Parameters + +| Name | Type | Description | Optional | +|--------------| ------ |---------------------------------------------------| -------- | +| entities | String | The list of entities associated with the incident | False | + +#### Sample Event + +``` +{ + "entityUrn": "urn:li:incident:16ff200a-0ac5-4a7d-bbab-d4bdb4f831f9", + "entityType": "incident", + "category": "INCIDENT", + "operation": "ACTIVE", + "parameters": { + "entities": "[urn:li:dataset:abc, urn:li:dataset:abc2]", + }, + "auditStamp": { + "actor": "urn:li:corpuser:jdoe", + "time": 1649953100653 + } +} +``` From 9b051e38d6bd9f62ea42ecce1cfbfdf686d9b0e9 Mon Sep 17 00:00:00 2001 From: Harshal Sheth Date: Wed, 24 Jan 2024 14:29:41 -0800 Subject: [PATCH 02/19] feat(ingest/dbt): support aws config without region (#9650) Co-authored-by: Tamas Nemeth --- .../ingestion/source/aws/aws_common.py | 4 +-- .../datahub/ingestion/source/aws/sagemaker.py | 4 +-- .../datahub/ingestion/source/dbt/dbt_core.py | 2 +- .../tests/unit/test_dbt_source.py | 33 +++++++++++++++---- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/metadata-ingestion/src/datahub/ingestion/source/aws/aws_common.py b/metadata-ingestion/src/datahub/ingestion/source/aws/aws_common.py index 421991a0966c3..95ca10045f1bb 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/aws/aws_common.py +++ b/metadata-ingestion/src/datahub/ingestion/source/aws/aws_common.py @@ -34,7 +34,7 @@ class AwsAssumeRoleConfig(PermissiveConfigModel): def assume_role( role: AwsAssumeRoleConfig, - aws_region: str, + aws_region: Optional[str], credentials: Optional[dict] = None, ) -> dict: credentials = credentials or {} @@ -93,7 +93,7 @@ class AwsConnectionConfig(ConfigModel): default=None, description="Named AWS profile to use. Only used if access key / secret are unset. If not set the default will be used", ) - aws_region: str = Field(description="AWS region code.") + aws_region: Optional[str] = Field(None, description="AWS region code.") aws_endpoint_url: Optional[str] = Field( default=None, diff --git a/metadata-ingestion/src/datahub/ingestion/source/aws/sagemaker.py b/metadata-ingestion/src/datahub/ingestion/source/aws/sagemaker.py index 6f6e8bbc05661..e335174eeb003 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/aws/sagemaker.py +++ b/metadata-ingestion/src/datahub/ingestion/source/aws/sagemaker.py @@ -82,7 +82,7 @@ def get_workunits_internal(self) -> Iterable[MetadataWorkUnit]: env=self.env, report=self.report, job_type_filter=self.source_config.extract_jobs, - aws_region=self.source_config.aws_region, + aws_region=self.sagemaker_client.meta.region_name, ) yield from job_processor.get_workunits() @@ -98,7 +98,7 @@ def get_workunits_internal(self) -> Iterable[MetadataWorkUnit]: model_image_to_jobs=model_image_to_jobs, model_name_to_jobs=model_name_to_jobs, lineage=lineage, - aws_region=self.source_config.aws_region, + aws_region=self.sagemaker_client.meta.region_name, ) yield from model_processor.get_workunits() diff --git a/metadata-ingestion/src/datahub/ingestion/source/dbt/dbt_core.py b/metadata-ingestion/src/datahub/ingestion/source/dbt/dbt_core.py index 6fd3c5ba309f9..a2f96264b7f64 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/dbt/dbt_core.py +++ b/metadata-ingestion/src/datahub/ingestion/source/dbt/dbt_core.py @@ -81,7 +81,7 @@ def aws_connection_needed_if_s3_uris_present( if (values.get(f) or "").startswith("s3://") ] - if uri_containing_fields and not aws_connection: + if uri_containing_fields and aws_connection is None: raise ValueError( f"Please provide aws_connection configuration, since s3 uris have been provided in fields {uri_containing_fields}" ) diff --git a/metadata-ingestion/tests/unit/test_dbt_source.py b/metadata-ingestion/tests/unit/test_dbt_source.py index 0fbe9ecbcc43c..737cf6aca33cc 100644 --- a/metadata-ingestion/tests/unit/test_dbt_source.py +++ b/metadata-ingestion/tests/unit/test_dbt_source.py @@ -1,6 +1,7 @@ from typing import Dict, List, Union from unittest import mock +import pytest from pydantic import ValidationError from datahub.emitter import mce_builder @@ -180,14 +181,12 @@ def test_dbt_entity_emission_configuration(): "target_platform": "dummy_platform", "entities_enabled": {"models": "Only", "seeds": "Only"}, } - try: + with pytest.raises( + ValidationError, + match="Cannot have more than 1 type of entity emission set to ONLY", + ): DBTCoreConfig.parse_obj(config_dict) - except ValidationError as ve: - assert len(ve.errors()) == 1 - assert ( - "Cannot have more than 1 type of entity emission set to ONLY" - in ve.errors()[0]["msg"] - ) + # valid config config_dict = { "manifest_path": "dummy_path", @@ -198,6 +197,26 @@ def test_dbt_entity_emission_configuration(): DBTCoreConfig.parse_obj(config_dict) +def test_dbt_s3_config(): + # test missing aws config + config_dict: dict = { + "manifest_path": "s3://dummy_path", + "catalog_path": "s3://dummy_path", + "target_platform": "dummy_platform", + } + with pytest.raises(ValidationError, match="provide aws_connection"): + DBTCoreConfig.parse_obj(config_dict) + + # valid config + config_dict = { + "manifest_path": "s3://dummy_path", + "catalog_path": "s3://dummy_path", + "target_platform": "dummy_platform", + "aws_connection": {}, + } + DBTCoreConfig.parse_obj(config_dict) + + def test_default_convert_column_urns_to_lowercase(): config_dict = { "manifest_path": "dummy_path", From d6a30a74a7877bb22f9cb1c00d22afde8b492a66 Mon Sep 17 00:00:00 2001 From: david-leifker <114954101+david-leifker@users.noreply.github.com> Date: Wed, 24 Jan 2024 16:30:40 -0600 Subject: [PATCH 03/19] fix(test): improve cypress tests (#9711) --- smoke-test/tests/cypress/cypress/e2e/glossary/glossary.js | 6 +++--- .../cypress/cypress/e2e/mutations/managed_ingestion.js | 2 +- smoke-test/tests/cypress/cypress/support/commands.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/smoke-test/tests/cypress/cypress/e2e/glossary/glossary.js b/smoke-test/tests/cypress/cypress/e2e/glossary/glossary.js index dbc4e1db72943..b0e24d5346fea 100644 --- a/smoke-test/tests/cypress/cypress/e2e/glossary/glossary.js +++ b/smoke-test/tests/cypress/cypress/e2e/glossary/glossary.js @@ -1,6 +1,6 @@ const urn = "urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)"; const datasetName = "cypress_logging_events"; -const glossaryTerm = "CypressGlosssaryTerm"; +const glossaryTerm = "CypressGlossaryTerm"; const glossaryTermGroup = "CypressGlossaryGroup"; describe("glossary", () => { @@ -8,9 +8,9 @@ describe("glossary", () => { cy.loginWithCredentials(); cy.goToGlossaryList(); cy.clickOptionWithText("Add Term"); - cy.addViaModal(glossaryTerm, "Create Glossary Term", glossaryTerm); + cy.addViaModal(glossaryTerm, "Create Glossary Term", glossaryTerm, "glossary-entity-modal-create-button"); cy.clickOptionWithText("Add Term Group"); - cy.addViaModal(glossaryTermGroup, "Create Term Group", glossaryTermGroup); + cy.addViaModal(glossaryTermGroup, "Create Term Group", glossaryTermGroup, "glossary-entity-modal-create-button"); cy.addTermToDataset(urn, datasetName, glossaryTerm); cy.waitTextVisible(glossaryTerm) cy.goToGlossaryList(); diff --git a/smoke-test/tests/cypress/cypress/e2e/mutations/managed_ingestion.js b/smoke-test/tests/cypress/cypress/e2e/mutations/managed_ingestion.js index 05f94c94bfe2a..c355aaabc336a 100644 --- a/smoke-test/tests/cypress/cypress/e2e/mutations/managed_ingestion.js +++ b/smoke-test/tests/cypress/cypress/e2e/mutations/managed_ingestion.js @@ -26,7 +26,7 @@ describe("run managed ingestion", () => { cy.enterTextInTestId('source-name-input', testName) cy.clickOptionWithText("Advanced") cy.enterTextInTestId('cli-version-input', cli_version) - cy.clickOptionWithText("Save & Run") + cy.clickOptionWithTextToScrollintoView("Save & Run") cy.waitTextVisible(testName) cy.contains(testName).parent().within(() => { diff --git a/smoke-test/tests/cypress/cypress/support/commands.js b/smoke-test/tests/cypress/cypress/support/commands.js index ba5600b79f5f6..f32512aff45fa 100644 --- a/smoke-test/tests/cypress/cypress/support/commands.js +++ b/smoke-test/tests/cypress/cypress/support/commands.js @@ -183,10 +183,10 @@ Cypress.Commands.add("addViaFormModal", (text, modelHeader) => { cy.get(".ant-modal-footer > button:nth-child(2)").click(); }); -Cypress.Commands.add("addViaModal", (text, modelHeader,value) => { +Cypress.Commands.add("addViaModal", (text, modelHeader, value, dataTestId) => { cy.waitTextVisible(modelHeader); cy.get(".ant-input-affix-wrapper > input[type='text']").first().type(text); - cy.get(".ant-modal-footer > button:nth-child(2)").click(); + cy.get('[data-testid="' + dataTestId + '"]').click(); cy.contains(value).should('be.visible'); }); From 9d8e2b9067781f0eabb53362609b3a19e5d5adfb Mon Sep 17 00:00:00 2001 From: Harshal Sheth Date: Wed, 24 Jan 2024 14:38:25 -0800 Subject: [PATCH 04/19] feat(ingest/tableau): map trino_jdbc platform type (#9708) --- .../src/datahub/ingestion/source/tableau_common.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/metadata-ingestion/src/datahub/ingestion/source/tableau_common.py b/metadata-ingestion/src/datahub/ingestion/source/tableau_common.py index a2f460feca388..121b2e257a6ba 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/tableau_common.py +++ b/metadata-ingestion/src/datahub/ingestion/source/tableau_common.py @@ -533,6 +533,9 @@ def get_platform(connection_type: str) -> str: platform = "mssql" elif connection_type in ("athena"): platform = "athena" + elif connection_type.endswith("_jdbc"): + # e.g. convert trino_jdbc -> trino + platform = connection_type[: -len("_jdbc")] else: platform = connection_type return platform From 23277f8dc4cabc5252c8eafed58ed75a3b62e27d Mon Sep 17 00:00:00 2001 From: Davi Arnaut Date: Wed, 24 Jan 2024 17:36:30 -0800 Subject: [PATCH 05/19] fix(oidc settings): effective JWS algorithm setting (#9712) --- datahub-frontend/app/auth/AuthUtils.java | 3 + .../app/auth/sso/oidc/OidcConfigs.java | 4 +- datahub-frontend/play.gradle | 3 + .../test/security/OidcConfigurationTest.java | 24 +++++ .../linkedin/settings/global/OidcSettings.pdl | 9 +- .../auth-servlet-impl/build.gradle | 8 ++ .../authentication/AuthServiceController.java | 6 +- .../AuthServiceControllerTest.java | 96 +++++++++++++++++++ .../AuthServiceTestConfiguration.java | 32 +++++++ 9 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 metadata-service/auth-servlet-impl/src/test/java/com/datahub/auth/authentication/AuthServiceControllerTest.java create mode 100644 metadata-service/auth-servlet-impl/src/test/java/com/datahub/auth/authentication/AuthServiceTestConfiguration.java diff --git a/datahub-frontend/app/auth/AuthUtils.java b/datahub-frontend/app/auth/AuthUtils.java index 84488a43f253e..51bb784c61b3b 100644 --- a/datahub-frontend/app/auth/AuthUtils.java +++ b/datahub-frontend/app/auth/AuthUtils.java @@ -76,6 +76,9 @@ public class AuthUtils { public static final String USE_NONCE = "useNonce"; public static final String READ_TIMEOUT = "readTimeout"; public static final String EXTRACT_JWT_ACCESS_TOKEN_CLAIMS = "extractJwtAccessTokenClaims"; + // Retained for backwards compatibility + public static final String PREFERRED_JWS_ALGORITHM = "preferredJwsAlgorithm"; + public static final String PREFERRED_JWS_ALGORITHM_2 = "preferredJwsAlgorithm2"; /** * Determines whether the inbound request should be forward to downstream Metadata Service. Today, diff --git a/datahub-frontend/app/auth/sso/oidc/OidcConfigs.java b/datahub-frontend/app/auth/sso/oidc/OidcConfigs.java index bf3384527af11..5de4eba9cb679 100644 --- a/datahub-frontend/app/auth/sso/oidc/OidcConfigs.java +++ b/datahub-frontend/app/auth/sso/oidc/OidcConfigs.java @@ -226,8 +226,8 @@ public Builder from(final com.typesafe.config.Config configs, final String ssoSe extractJwtAccessTokenClaims = Optional.of(jsonNode.get(EXTRACT_JWT_ACCESS_TOKEN_CLAIMS).asBoolean()); } - if (jsonNode.has(OIDC_PREFERRED_JWS_ALGORITHM)) { - preferredJwsAlgorithm = Optional.of(jsonNode.get(OIDC_PREFERRED_JWS_ALGORITHM).asText()); + if (jsonNode.has(PREFERRED_JWS_ALGORITHM_2)) { + preferredJwsAlgorithm = Optional.of(jsonNode.get(PREFERRED_JWS_ALGORITHM_2).asText()); } else { preferredJwsAlgorithm = Optional.ofNullable(getOptional(configs, OIDC_PREFERRED_JWS_ALGORITHM, null)); diff --git a/datahub-frontend/play.gradle b/datahub-frontend/play.gradle index 1e3a2767852d6..9bd77e5279a91 100644 --- a/datahub-frontend/play.gradle +++ b/datahub-frontend/play.gradle @@ -101,6 +101,9 @@ play { test { useJUnitPlatform() + testLogging.showStandardStreams = true + testLogging.exceptionFormat = 'full' + def playJava17CompatibleJvmArgs = [ "--add-opens=java.base/java.lang=ALL-UNNAMED", //"--add-opens=java.base/java.lang.invoke=ALL-UNNAMED", diff --git a/datahub-frontend/test/security/OidcConfigurationTest.java b/datahub-frontend/test/security/OidcConfigurationTest.java index c1147ae936b3a..8226d4e74cc21 100644 --- a/datahub-frontend/test/security/OidcConfigurationTest.java +++ b/datahub-frontend/test/security/OidcConfigurationTest.java @@ -1,5 +1,6 @@ package security; +import static auth.AuthUtils.*; import static auth.sso.oidc.OidcConfigs.*; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -24,6 +25,7 @@ import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; import org.pac4j.oidc.client.OidcClient; +import org.json.JSONObject; public class OidcConfigurationTest { @@ -317,4 +319,26 @@ public void readTimeoutPropagation() { OidcProvider oidcProvider = new OidcProvider(oidcConfigs); assertEquals(10000, ((OidcClient) oidcProvider.client()).getConfiguration().getReadTimeout()); } + + @Test + public void readPreferredJwsAlgorithmPropagationFromConfig() { + final String SSO_SETTINGS_JSON_STR = new JSONObject().put(PREFERRED_JWS_ALGORITHM, "HS256").toString(); + CONFIG.withValue(OIDC_PREFERRED_JWS_ALGORITHM, ConfigValueFactory.fromAnyRef("RS256")); + OidcConfigs.Builder oidcConfigsBuilder = new OidcConfigs.Builder(); + oidcConfigsBuilder.from(CONFIG, SSO_SETTINGS_JSON_STR); + OidcConfigs oidcConfigs = new OidcConfigs(oidcConfigsBuilder); + OidcProvider oidcProvider = new OidcProvider(oidcConfigs); + assertEquals("RS256", ((OidcClient) oidcProvider.client()).getConfiguration().getPreferredJwsAlgorithm().toString()); + } + + @Test + public void readPreferredJwsAlgorithmPropagationFromJSON() { + final String SSO_SETTINGS_JSON_STR = new JSONObject().put(PREFERRED_JWS_ALGORITHM, "Unused").put(PREFERRED_JWS_ALGORITHM_2, "HS256").toString(); + CONFIG.withValue(OIDC_PREFERRED_JWS_ALGORITHM, ConfigValueFactory.fromAnyRef("RS256")); + OidcConfigs.Builder oidcConfigsBuilder = new OidcConfigs.Builder(); + oidcConfigsBuilder.from(CONFIG, SSO_SETTINGS_JSON_STR); + OidcConfigs oidcConfigs = new OidcConfigs(oidcConfigsBuilder); + OidcProvider oidcProvider = new OidcProvider(oidcConfigs); + assertEquals("HS256", ((OidcClient) oidcProvider.client()).getConfiguration().getPreferredJwsAlgorithm().toString()); + } } diff --git a/metadata-models/src/main/pegasus/com/linkedin/settings/global/OidcSettings.pdl b/metadata-models/src/main/pegasus/com/linkedin/settings/global/OidcSettings.pdl index d5b23c28cb227..f925505c8e54f 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/settings/global/OidcSettings.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/settings/global/OidcSettings.pdl @@ -90,7 +90,12 @@ record OidcSettings { extractJwtAccessTokenClaims: optional boolean /** - * ADVANCED. Which jws algorithm to use. + * ADVANCED. Which jws algorithm to use. Unused. */ preferredJwsAlgorithm: optional string -} \ No newline at end of file + + /** + * ADVANCED. Which jws algorithm to use. + */ + preferredJwsAlgorithm2: optional string +} diff --git a/metadata-service/auth-servlet-impl/build.gradle b/metadata-service/auth-servlet-impl/build.gradle index b8310bbd4ebc0..29e452472358b 100644 --- a/metadata-service/auth-servlet-impl/build.gradle +++ b/metadata-service/auth-servlet-impl/build.gradle @@ -18,4 +18,12 @@ dependencies { compileOnly externalDependency.lombok annotationProcessor externalDependency.lombok + + testImplementation externalDependency.testng + testImplementation externalDependency.springBootTest +} + +test { + testLogging.showStandardStreams = true + testLogging.exceptionFormat = 'full' } diff --git a/metadata-service/auth-servlet-impl/src/main/java/com/datahub/auth/authentication/AuthServiceController.java b/metadata-service/auth-servlet-impl/src/main/java/com/datahub/auth/authentication/AuthServiceController.java index 430ed2d236219..fc283b7e986bb 100644 --- a/metadata-service/auth-servlet-impl/src/main/java/com/datahub/auth/authentication/AuthServiceController.java +++ b/metadata-service/auth-servlet-impl/src/main/java/com/datahub/auth/authentication/AuthServiceController.java @@ -72,7 +72,9 @@ public class AuthServiceController { private static final String USE_NONCE = "useNonce"; private static final String READ_TIMEOUT = "readTimeout"; private static final String EXTRACT_JWT_ACCESS_TOKEN_CLAIMS = "extractJwtAccessTokenClaims"; + // Retained for backwards compatibility private static final String PREFERRED_JWS_ALGORITHM = "preferredJwsAlgorithm"; + private static final String PREFERRED_JWS_ALGORITHM_2 = "preferredJwsAlgorithm2"; @Inject StatelessTokenService _statelessTokenService; @@ -514,8 +516,8 @@ private void buildOidcSettingsResponse(JSONObject json, final OidcSettings oidcS if (oidcSettings.hasExtractJwtAccessTokenClaims()) { json.put(EXTRACT_JWT_ACCESS_TOKEN_CLAIMS, oidcSettings.isExtractJwtAccessTokenClaims()); } - if (oidcSettings.hasPreferredJwsAlgorithm()) { - json.put(PREFERRED_JWS_ALGORITHM, oidcSettings.getPreferredJwsAlgorithm()); + if (oidcSettings.hasPreferredJwsAlgorithm2()) { + json.put(PREFERRED_JWS_ALGORITHM, oidcSettings.getPreferredJwsAlgorithm2()); } } } diff --git a/metadata-service/auth-servlet-impl/src/test/java/com/datahub/auth/authentication/AuthServiceControllerTest.java b/metadata-service/auth-servlet-impl/src/test/java/com/datahub/auth/authentication/AuthServiceControllerTest.java new file mode 100644 index 0000000000000..bb305ae16900c --- /dev/null +++ b/metadata-service/auth-servlet-impl/src/test/java/com/datahub/auth/authentication/AuthServiceControllerTest.java @@ -0,0 +1,96 @@ +package com.datahub.auth.authentication; + +import static com.linkedin.metadata.Constants.GLOBAL_SETTINGS_INFO_ASPECT_NAME; +import static com.linkedin.metadata.Constants.GLOBAL_SETTINGS_URN; +import static org.mockito.Mockito.when; +import static org.testng.Assert.*; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.linkedin.data.schema.annotation.PathSpecBasedSchemaAnnotationVisitor; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.settings.global.GlobalSettingsInfo; +import com.linkedin.settings.global.OidcSettings; +import com.linkedin.settings.global.SsoSettings; +import java.io.IOException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.springframework.web.servlet.DispatcherServlet; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +@SpringBootTest(classes = {DispatcherServlet.class}) +@ComponentScan(basePackages = {"com.datahub.auth.authentication"}) +@Import({AuthServiceTestConfiguration.class}) +public class AuthServiceControllerTest extends AbstractTestNGSpringContextTests { + @BeforeTest + public void disableAssert() { + PathSpecBasedSchemaAnnotationVisitor.class + .getClassLoader() + .setClassAssertionStatus(PathSpecBasedSchemaAnnotationVisitor.class.getName(), false); + } + + @Autowired private AuthServiceController authServiceController; + @Autowired private EntityService mockEntityService; + + private final String PREFERRED_JWS_ALGORITHM = "preferredJwsAlgorithm"; + + @Test + public void initTest() { + assertNotNull(authServiceController); + assertNotNull(mockEntityService); + } + + @Test + public void oldPreferredJwsAlgorithmIsNotReturned() throws IOException { + OidcSettings mockOidcSettings = + new OidcSettings() + .setEnabled(true) + .setClientId("1") + .setClientSecret("2") + .setDiscoveryUri("http://localhost") + .setPreferredJwsAlgorithm("test"); + SsoSettings mockSsoSettings = + new SsoSettings().setBaseUrl("http://localhost").setOidcSettings(mockOidcSettings); + GlobalSettingsInfo mockGlobalSettingsInfo = new GlobalSettingsInfo().setSso(mockSsoSettings); + + when(mockEntityService.getLatestAspect(GLOBAL_SETTINGS_URN, GLOBAL_SETTINGS_INFO_ASPECT_NAME)) + .thenReturn(mockGlobalSettingsInfo); + + ResponseEntity httpResponse = authServiceController.getSsoSettings(null).join(); + assertEquals(httpResponse.getStatusCode(), HttpStatus.OK); + + JsonNode jsonNode = new ObjectMapper().readTree(httpResponse.getBody()); + assertFalse(jsonNode.has(PREFERRED_JWS_ALGORITHM)); + } + + @Test + public void newPreferredJwsAlgorithmIsReturned() throws IOException { + OidcSettings mockOidcSettings = + new OidcSettings() + .setEnabled(true) + .setClientId("1") + .setClientSecret("2") + .setDiscoveryUri("http://localhost") + .setPreferredJwsAlgorithm("jws1") + .setPreferredJwsAlgorithm2("jws2"); + SsoSettings mockSsoSettings = + new SsoSettings().setBaseUrl("http://localhost").setOidcSettings(mockOidcSettings); + GlobalSettingsInfo mockGlobalSettingsInfo = new GlobalSettingsInfo().setSso(mockSsoSettings); + + when(mockEntityService.getLatestAspect(GLOBAL_SETTINGS_URN, GLOBAL_SETTINGS_INFO_ASPECT_NAME)) + .thenReturn(mockGlobalSettingsInfo); + + ResponseEntity httpResponse = authServiceController.getSsoSettings(null).join(); + assertEquals(httpResponse.getStatusCode(), HttpStatus.OK); + + JsonNode jsonNode = new ObjectMapper().readTree(httpResponse.getBody()); + assertTrue(jsonNode.has(PREFERRED_JWS_ALGORITHM)); + assertEquals(jsonNode.get(PREFERRED_JWS_ALGORITHM).asText(), "jws2"); + } +} diff --git a/metadata-service/auth-servlet-impl/src/test/java/com/datahub/auth/authentication/AuthServiceTestConfiguration.java b/metadata-service/auth-servlet-impl/src/test/java/com/datahub/auth/authentication/AuthServiceTestConfiguration.java new file mode 100644 index 0000000000000..428f14e67d137 --- /dev/null +++ b/metadata-service/auth-servlet-impl/src/test/java/com/datahub/auth/authentication/AuthServiceTestConfiguration.java @@ -0,0 +1,32 @@ +package com.datahub.auth.authentication; + +import com.datahub.authentication.Authentication; +import com.datahub.authentication.invite.InviteTokenService; +import com.datahub.authentication.token.StatelessTokenService; +import com.datahub.authentication.user.NativeUserService; +import com.datahub.telemetry.TrackingService; +import com.linkedin.gms.factory.config.ConfigurationProvider; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.secret.SecretService; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; + +@TestConfiguration +public class AuthServiceTestConfiguration { + @MockBean StatelessTokenService _statelessTokenService; + + @MockBean Authentication _systemAuthentication; + + @MockBean(name = "configurationProvider") + ConfigurationProvider _configProvider; + + @MockBean NativeUserService _nativeUserService; + + @MockBean EntityService _entityService; + + @MockBean SecretService _secretService; + + @MockBean InviteTokenService _inviteTokenService; + + @MockBean TrackingService _trackingService; +} From 53c7790f9aa56eeb6695d3fbf602b3b84a7283e4 Mon Sep 17 00:00:00 2001 From: Andrew Sikowitz Date: Thu, 25 Jan 2024 01:36:59 -0800 Subject: [PATCH 06/19] feat(ingest/metabase): Use new sql parser; reduce error reporting levels (#9714) --- .../src/datahub_airflow_plugin/_extractors.py | 4 +- .../src/datahub/ingestion/source/metabase.py | 100 ++++++++---------- .../powerbi/m_query/native_sql_parser.py | 4 +- .../ingestion/source/redshift/lineage.py | 4 +- .../src/datahub/ingestion/source/tableau.py | 11 +- .../src/datahub/utilities/sqlglot_lineage.py | 40 +++---- .../metabase/metabase_mces_golden.json | 32 ++++-- .../integration/metabase/setup/card.json | 2 +- .../integration/metabase/setup/card_1.json | 4 +- .../metabase/setup/dashboard_1.json | 4 +- .../tableau/test_tableau_ingest.py | 10 +- 11 files changed, 108 insertions(+), 107 deletions(-) diff --git a/metadata-ingestion-modules/airflow-plugin/src/datahub_airflow_plugin/_extractors.py b/metadata-ingestion-modules/airflow-plugin/src/datahub_airflow_plugin/_extractors.py index f84b7b56f6119..32bbe88481636 100644 --- a/metadata-ingestion-modules/airflow-plugin/src/datahub_airflow_plugin/_extractors.py +++ b/metadata-ingestion-modules/airflow-plugin/src/datahub_airflow_plugin/_extractors.py @@ -199,8 +199,8 @@ def _sql_extractor_extract(self: "SqlExtractor") -> TaskMetadata: platform=platform, platform_instance=None, env=builder.DEFAULT_ENV, - database=default_database, - schema=default_schema, + default_db=default_database, + default_schema=default_schema, ) self.log.debug(f"Got sql lineage {sql_parsing_result}") diff --git a/metadata-ingestion/src/datahub/ingestion/source/metabase.py b/metadata-ingestion/src/datahub/ingestion/source/metabase.py index 9f09a4322bb5d..af41a74f311f6 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/metabase.py +++ b/metadata-ingestion/src/datahub/ingestion/source/metabase.py @@ -1,3 +1,4 @@ +import logging from datetime import datetime, timezone from functools import lru_cache from typing import Dict, Iterable, List, Optional, Tuple, Union @@ -7,7 +8,6 @@ import requests from pydantic import Field, validator from requests.models import HTTPError -from sqllineage.runner import LineageRunner import datahub.emitter.mce_builder as builder from datahub.configuration.source_common import DatasetLineageProviderConfigBase @@ -42,6 +42,9 @@ OwnershipTypeClass, ) from datahub.utilities import config_clean +from datahub.utilities.sqlglot_lineage import create_lineage_sql_parsed_result + +logger = logging.getLogger(__name__) DATASOURCE_URN_RECURSION_LIMIT = 5 @@ -225,7 +228,7 @@ def construct_dashboard_from_api_data( dashboard_response.raise_for_status() dashboard_details = dashboard_response.json() except HTTPError as http_error: - self.report.report_failure( + self.report.report_warning( key=f"metabase-dashboard-{dashboard_id}", reason=f"Unable to retrieve dashboard. " f"Reason: {str(http_error)}", ) @@ -293,7 +296,7 @@ def _get_ownership(self, creator_id: int) -> Optional[OwnershipClass]: ) return None # For cases when the error is not 404 but something else - self.report.report_failure( + self.report.report_warning( key=f"metabase-user-{creator_id}", reason=f"Unable to retrieve User info. " f"Reason: {str(http_error)}", ) @@ -348,7 +351,7 @@ def get_card_details_by_id(self, card_id: Union[int, str]) -> dict: card_response.raise_for_status() return card_response.json() except HTTPError as http_error: - self.report.report_failure( + self.report.report_warning( key=f"metabase-card-{card_id}", reason=f"Unable to retrieve Card info. " f"Reason: {str(http_error)}", ) @@ -357,7 +360,7 @@ def get_card_details_by_id(self, card_id: Union[int, str]) -> dict: def construct_card_from_api_data(self, card_data: dict) -> Optional[ChartSnapshot]: card_id = card_data.get("id") if card_id is None: - self.report.report_failure( + self.report.report_warning( key="metabase-card", reason=f"Unable to get Card id from card data {str(card_data)}", ) @@ -365,7 +368,7 @@ def construct_card_from_api_data(self, card_data: dict) -> Optional[ChartSnapsho card_details = self.get_card_details_by_id(card_id) if not card_details: - self.report.report_failure( + self.report.report_warning( key=f"metabase-card-{card_id}", reason="Unable to construct Card due to empty card details", ) @@ -482,7 +485,7 @@ def get_datasource_urn( self, card_details: dict, recursion_depth: int = 0 ) -> Optional[List]: if recursion_depth > DATASOURCE_URN_RECURSION_LIMIT: - self.report.report_failure( + self.report.report_warning( key=f"metabase-card-{card_details.get('id')}", reason="Unable to retrieve Card info. Reason: source table recursion depth exceeded", ) @@ -496,14 +499,13 @@ def get_datasource_urn( platform_instance, ) = self.get_datasource_from_id(datasource_id) if not platform: - self.report.report_failure( + self.report.report_warning( key=f"metabase-datasource-{datasource_id}", reason=f"Unable to detect platform for database id {datasource_id}", ) return None query_type = card_details.get("dataset_query", {}).get("type", {}) - source_tables = set() if query_type == "query": source_table_id = ( @@ -525,57 +527,40 @@ def get_datasource_urn( # the question is built directly from table in DB schema_name, table_name = self.get_source_table_from_id(source_table_id) if table_name: - source_tables.add( - f"{database_name + '.' if database_name else ''}{schema_name + '.' if schema_name else ''}{table_name}" - ) - else: - try: - raw_query = ( - card_details.get("dataset_query", {}) - .get("native", {}) - .get("query", "") - ) - parser = LineageRunner(raw_query) - - for table in parser.source_tables: - sources = str(table).split(".") - - source_db = sources[-3] if len(sources) > 2 else database_name - source_schema, source_table = sources[-2], sources[-1] - if source_schema == "": - source_schema = ( - database_schema - if database_schema is not None - else str(self.config.default_schema) + name_components = [database_name, schema_name, table_name] + return [ + builder.make_dataset_urn_with_platform_instance( + platform=platform, + name=".".join([v for v in name_components if v]), + platform_instance=platform_instance, + env=self.config.env, ) - - source_tables.add( - f"{source_db + '.' if source_db else ''}{source_schema}.{source_table}" - ) - except Exception as e: - self.report.report_failure( - key="metabase-query", - reason=f"Unable to retrieve lineage from query. " - f"Query: {raw_query} " - f"Reason: {str(e)} ", - ) - return None - - if platform == "snowflake": - source_tables = set(i.lower() for i in source_tables) - - # Create dataset URNs - dataset_urn = [ - builder.make_dataset_urn_with_platform_instance( + ] + else: + raw_query = ( + card_details.get("dataset_query", {}).get("native", {}).get("query", "") + ) + result = create_lineage_sql_parsed_result( + query=raw_query, + default_db=database_name, + default_schema=database_schema or self.config.default_schema, platform=platform, - name=name, platform_instance=platform_instance, env=self.config.env, + graph=self.ctx.graph, ) - for name in source_tables - ] + if result.debug_info.table_error: + logger.info( + f"Failed to parse lineage from query {raw_query}: " + f"{result.debug_info.table_error}" + ) + self.report.report_warning( + key="metabase-query", + reason=f"Unable to retrieve lineage from query: {raw_query}", + ) + return result.in_tables - return dataset_urn + return None @lru_cache(maxsize=None) def get_source_table_from_id( @@ -592,10 +577,9 @@ def get_source_table_from_id( return schema, name except HTTPError as http_error: - self.report.report_failure( + self.report.report_warning( key=f"metabase-table-{table_id}", - reason=f"Unable to retrieve source table. " - f"Reason: {str(http_error)}", + reason=f"Unable to retrieve source table. Reason: {str(http_error)}", ) return None, None @@ -641,7 +625,7 @@ def get_datasource_from_id( dataset_response.raise_for_status() dataset_json = dataset_response.json() except HTTPError as http_error: - self.report.report_failure( + self.report.report_warning( key=f"metabase-datasource-{datasource_id}", reason=f"Unable to retrieve Datasource. " f"Reason: {str(http_error)}", ) diff --git a/metadata-ingestion/src/datahub/ingestion/source/powerbi/m_query/native_sql_parser.py b/metadata-ingestion/src/datahub/ingestion/source/powerbi/m_query/native_sql_parser.py index 0afa8e7ff4564..56c9a4abe18ad 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/powerbi/m_query/native_sql_parser.py +++ b/metadata-ingestion/src/datahub/ingestion/source/powerbi/m_query/native_sql_parser.py @@ -69,8 +69,8 @@ def parse_custom_sql( return sqlglot_l.create_lineage_sql_parsed_result( query=sql_query, - schema=schema, - database=database, + default_schema=schema, + default_db=database, platform=platform, platform_instance=platform_instance, env=env, diff --git a/metadata-ingestion/src/datahub/ingestion/source/redshift/lineage.py b/metadata-ingestion/src/datahub/ingestion/source/redshift/lineage.py index 8135e1d44c102..3efef58737c6e 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/redshift/lineage.py +++ b/metadata-ingestion/src/datahub/ingestion/source/redshift/lineage.py @@ -167,8 +167,8 @@ def _get_sources_from_query( query=query, platform=LineageDatasetPlatform.REDSHIFT.value, platform_instance=self.config.platform_instance, - database=db_name, - schema=str(self.config.default_schema), + default_db=db_name, + default_schema=str(self.config.default_schema), graph=self.context.graph, env=self.config.env, ) diff --git a/metadata-ingestion/src/datahub/ingestion/source/tableau.py b/metadata-ingestion/src/datahub/ingestion/source/tableau.py index 46694dfcc47d1..acdece14a6440 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/tableau.py +++ b/metadata-ingestion/src/datahub/ingestion/source/tableau.py @@ -32,7 +32,6 @@ from urllib3 import Retry import datahub.emitter.mce_builder as builder -import datahub.utilities.sqlglot_lineage as sqlglot_l from datahub.configuration.common import ( AllowDenyPattern, ConfigModel, @@ -144,7 +143,11 @@ ViewPropertiesClass, ) from datahub.utilities import config_clean -from datahub.utilities.sqlglot_lineage import ColumnLineageInfo, SqlParsingResult +from datahub.utilities.sqlglot_lineage import ( + ColumnLineageInfo, + SqlParsingResult, + create_lineage_sql_parsed_result, +) from datahub.utilities.urns.dataset_urn import DatasetUrn logger: logging.Logger = logging.getLogger(__name__) @@ -1617,9 +1620,9 @@ def parse_custom_sql( f"Overridden info upstream_db={upstream_db}, platform_instance={platform_instance}, platform={platform}" ) - return sqlglot_l.create_lineage_sql_parsed_result( + return create_lineage_sql_parsed_result( query=query, - database=upstream_db, + default_db=upstream_db, platform=platform, platform_instance=platform_instance, env=env, diff --git a/metadata-ingestion/src/datahub/utilities/sqlglot_lineage.py b/metadata-ingestion/src/datahub/utilities/sqlglot_lineage.py index 46ca17609f3ea..abe4f82673777 100644 --- a/metadata-ingestion/src/datahub/utilities/sqlglot_lineage.py +++ b/metadata-ingestion/src/datahub/utilities/sqlglot_lineage.py @@ -1280,35 +1280,35 @@ def replace_cte_refs(node: sqlglot.exp.Expression) -> sqlglot.exp.Expression: def create_lineage_sql_parsed_result( query: str, - database: Optional[str], + default_db: Optional[str], platform: str, platform_instance: Optional[str], env: str, - schema: Optional[str] = None, + default_schema: Optional[str] = None, graph: Optional[DataHubGraph] = None, ) -> SqlParsingResult: - needs_close = False - try: - if graph: - schema_resolver = graph._make_schema_resolver( - platform=platform, - platform_instance=platform_instance, - env=env, - ) - else: - needs_close = True - schema_resolver = SchemaResolver( - platform=platform, - platform_instance=platform_instance, - env=env, - graph=None, - ) + if graph: + needs_close = False + schema_resolver = graph._make_schema_resolver( + platform=platform, + platform_instance=platform_instance, + env=env, + ) + else: + needs_close = True + schema_resolver = SchemaResolver( + platform=platform, + platform_instance=platform_instance, + env=env, + graph=None, + ) + try: return sqlglot_lineage( query, schema_resolver=schema_resolver, - default_db=database, - default_schema=schema, + default_db=default_db, + default_schema=default_schema, ) except Exception as e: return SqlParsingResult.make_from_error(e) diff --git a/metadata-ingestion/tests/integration/metabase/metabase_mces_golden.json b/metadata-ingestion/tests/integration/metabase/metabase_mces_golden.json index 0ba6afbd04fc9..9b143348fdf60 100644 --- a/metadata-ingestion/tests/integration/metabase/metabase_mces_golden.json +++ b/metadata-ingestion/tests/integration/metabase/metabase_mces_golden.json @@ -25,6 +25,9 @@ }, "chartUrl": "http://localhost:3000/card/1", "inputs": [ + { + "string": "urn:li:dataset:(urn:li:dataPlatform:bigquery,acryl-data.public.customer,PROD)" + }, { "string": "urn:li:dataset:(urn:li:dataPlatform:bigquery,acryl-data.public.payment,PROD)" } @@ -34,7 +37,7 @@ }, { "com.linkedin.pegasus2avro.chart.ChartQuery": { - "rawQuery": "SELECT\\n\\tcustomer.customer_id,\\n\\tfirst_name,\\n\\tlast_name,\\n\\tamount,\\n\\tpayment_date,\\n\\trental_id\\nFROM\\n\\tcustomer\\nINNER JOIN payment \\n ON payment.customer_id = customer.customer_id\\nORDER BY payment_date", + "rawQuery": "SELECT\n\tcustomer.customer_id,\n\tfirst_name,\n\tlast_name,\n\tamount,\n\tpayment_date,\n\trental_id\nFROM\n\tcustomer\nINNER JOIN payment \n ON payment.customer_id = customer.customer_id\nORDER BY payment_date", "type": "SQL" } }, @@ -57,7 +60,8 @@ }, "systemMetadata": { "lastObserved": 1636614000000, - "runId": "metabase-test" + "runId": "metabase-test", + "lastRunId": "no-run-id-provided" } }, { @@ -112,7 +116,8 @@ }, "systemMetadata": { "lastObserved": 1636614000000, - "runId": "metabase-test" + "runId": "metabase-test", + "lastRunId": "no-run-id-provided" } }, { @@ -141,6 +146,9 @@ }, "chartUrl": "http://localhost:3000/card/3", "inputs": [ + { + "string": "urn:li:dataset:(urn:li:dataPlatform:bigquery,acryl-data.public.customer,PROD)" + }, { "string": "urn:li:dataset:(urn:li:dataPlatform:bigquery,acryl-data.public.payment,PROD)" } @@ -167,7 +175,8 @@ }, "systemMetadata": { "lastObserved": 1636614000000, - "runId": "metabase-test" + "runId": "metabase-test", + "lastRunId": "no-run-id-provided" } }, { @@ -217,7 +226,8 @@ }, "systemMetadata": { "lastObserved": 1636614000000, - "runId": "metabase-test" + "runId": "metabase-test", + "lastRunId": "no-run-id-provided" } }, { @@ -232,7 +242,8 @@ }, "systemMetadata": { "lastObserved": 1636614000000, - "runId": "metabase-test" + "runId": "metabase-test", + "lastRunId": "no-run-id-provided" } }, { @@ -247,7 +258,8 @@ }, "systemMetadata": { "lastObserved": 1636614000000, - "runId": "metabase-test" + "runId": "metabase-test", + "lastRunId": "no-run-id-provided" } }, { @@ -262,7 +274,8 @@ }, "systemMetadata": { "lastObserved": 1636614000000, - "runId": "metabase-test" + "runId": "metabase-test", + "lastRunId": "no-run-id-provided" } }, { @@ -277,7 +290,8 @@ }, "systemMetadata": { "lastObserved": 1636614000000, - "runId": "metabase-test" + "runId": "metabase-test", + "lastRunId": "no-run-id-provided" } } ] \ No newline at end of file diff --git a/metadata-ingestion/tests/integration/metabase/setup/card.json b/metadata-ingestion/tests/integration/metabase/setup/card.json index 83bff66e6c9f3..7ded73d02ad7d 100644 --- a/metadata-ingestion/tests/integration/metabase/setup/card.json +++ b/metadata-ingestion/tests/integration/metabase/setup/card.json @@ -172,7 +172,7 @@ "dataset_query": { "type": "native", "native": { - "query": "SELECT\\n\\tcustomer.customer_id,\\n\\tfirst_name,\\n\\tlast_name,\\n\\tamount,\\n\\tpayment_date,\\n\\trental_id\\nFROM\\n\\tcustomer\\nINNER JOIN payment \\n ON payment.customer_id = customer.customer_id\\nORDER BY payment_date", + "query": "SELECT\n\tcustomer.customer_id,\n\tfirst_name,\n\tlast_name,\n\tamount,\n\tpayment_date,\n\trental_id\nFROM\n\tcustomer\nINNER JOIN payment \n ON payment.customer_id = customer.customer_id\nORDER BY payment_date", "template-tags": {} }, "database": 2 diff --git a/metadata-ingestion/tests/integration/metabase/setup/card_1.json b/metadata-ingestion/tests/integration/metabase/setup/card_1.json index 01e35c5b30844..66c46a72997d0 100644 --- a/metadata-ingestion/tests/integration/metabase/setup/card_1.json +++ b/metadata-ingestion/tests/integration/metabase/setup/card_1.json @@ -177,7 +177,7 @@ "dataset_query": { "type": "native", "native": { - "query": "SELECT\\n\\tcustomer.customer_id,\\n\\tfirst_name,\\n\\tlast_name,\\n\\tamount,\\n\\tpayment_date,\\n\\trental_id\\nFROM\\n\\tcustomer\\nINNER JOIN payment \\n ON payment.customer_id = customer.customer_id\\nORDER BY payment_date", + "query": "SELECT\n\tcustomer.customer_id,\n\tfirst_name,\n\tlast_name,\n\tamount,\n\tpayment_date,\n\trental_id\nFROM\n\tcustomer\nINNER JOIN payment \n ON payment.customer_id = customer.customer_id\nORDER BY payment_date", "template-tags": {} }, "database": 2 @@ -198,4 +198,4 @@ "collection": null, "created_at": "2021-12-13T17:46:32.77", "public_uuid": null -} \ No newline at end of file +} diff --git a/metadata-ingestion/tests/integration/metabase/setup/dashboard_1.json b/metadata-ingestion/tests/integration/metabase/setup/dashboard_1.json index 0b232cd220045..288087a67da6d 100644 --- a/metadata-ingestion/tests/integration/metabase/setup/dashboard_1.json +++ b/metadata-ingestion/tests/integration/metabase/setup/dashboard_1.json @@ -171,7 +171,7 @@ "dataset_query": { "type": "native", "native": { - "query": "SELECT\\n\\tcustomer.customer_id,\\n\\tfirst_name,\\n\\tlast_name,\\n\\tamount,\\n\\tpayment_date,\\n\\trental_id\\nFROM\\n\\tcustomer\\nINNER JOIN payment \\n ON payment.customer_id = customer.customer_id\\nORDER BY payment_date", + "query": "SELECT\n\tcustomer.customer_id,\n\tfirst_name,\n\tlast_name,\n\tamount,\n\tpayment_date,\n\trental_id\nFROM\n\tcustomer\nINNER JOIN payment \n ON payment.customer_id = customer.customer_id\nORDER BY payment_date", "template-tags": {} }, "database": 2 @@ -330,4 +330,4 @@ "created_at": "2021-12-13T17:46:48.185", "public_uuid": null, "points_of_interest": null -} \ No newline at end of file +} diff --git a/metadata-ingestion/tests/integration/tableau/test_tableau_ingest.py b/metadata-ingestion/tests/integration/tableau/test_tableau_ingest.py index 90fa71013338d..474228e9c9fc4 100644 --- a/metadata-ingestion/tests/integration/tableau/test_tableau_ingest.py +++ b/metadata-ingestion/tests/integration/tableau/test_tableau_ingest.py @@ -812,16 +812,16 @@ def test_tableau_unsupported_csql(mock_datahub_graph): database_override_map={"production database": "prod"} ) - with mock.patch("datahub.ingestion.source.tableau.sqlglot_l") as sqlglot_lineage: - - sqlglot_lineage.create_lineage_sql_parsed_result.return_value = SqlParsingResult( # type:ignore + with mock.patch( + "datahub.ingestion.source.tableau.create_lineage_sql_parsed_result", + return_value=SqlParsingResult( in_tables=[ "urn:li:dataset:(urn:li:dataPlatform:bigquery,my_bigquery_project.invent_dw.userdetail,PROD)" ], out_tables=[], column_lineage=None, - ) - + ), + ): source = TableauSource(config=config, ctx=context) lineage = source._create_lineage_from_unsupported_csql( From f83a2fab4415bd31f88cae1e05384282ab4d955c Mon Sep 17 00:00:00 2001 From: Shubham Jagtap <132359390+shubhamjagtap639@users.noreply.github.com> Date: Thu, 25 Jan 2024 18:48:41 +0530 Subject: [PATCH 07/19] fix(ingestion/bigquery): Table-view-snapshot Lineage Bug fix (#9579) Co-authored-by: Aseem Bansal Co-authored-by: Harshal Sheth --- .../ingestion/source/bigquery_v2/bigquery.py | 133 ++++++++++++++++-- .../source/bigquery_v2/bigquery_config.py | 9 ++ .../source/bigquery_v2/bigquery_report.py | 3 + .../source/bigquery_v2/bigquery_schema.py | 82 ++++++++++- .../ingestion/source/bigquery_v2/lineage.py | 72 ++++++++-- .../ingestion/source/bigquery_v2/queries.py | 56 ++++++++ .../ingestion/source/common/subtypes.py | 1 + .../tests/unit/test_bigquery_source.py | 91 +++++++++++- 8 files changed, 416 insertions(+), 31 deletions(-) diff --git a/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery.py b/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery.py index 3704eae96aece..b8bc07b9a3559 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery.py +++ b/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery.py @@ -47,6 +47,7 @@ BigqueryProject, BigQuerySchemaApi, BigqueryTable, + BigqueryTableSnapshot, BigqueryView, ) from datahub.ingestion.source.bigquery_v2.common import ( @@ -234,7 +235,7 @@ def __init__(self, ctx: PipelineContext, config: BigQueryV2Config): run_id=self.ctx.run_id, ) - # For database, schema, tables, views, etc + # For database, schema, tables, views, snapshots etc self.lineage_extractor = BigqueryLineageExtractor( config, self.report, @@ -282,8 +283,12 @@ def __init__(self, ctx: PipelineContext, config: BigQueryV2Config): # Maps project -> view_ref, so we can find all views in a project self.view_refs_by_project: Dict[str, Set[str]] = defaultdict(set) + # Maps project -> snapshot_ref, so we can find all snapshots in a project + self.snapshot_refs_by_project: Dict[str, Set[str]] = defaultdict(set) # Maps view ref -> actual sql self.view_definitions: FileBackedDict[str] = FileBackedDict() + # Maps snapshot ref -> Snapshot + self.snapshots_by_ref: FileBackedDict[BigqueryTableSnapshot] = FileBackedDict() self.add_config_to_report() atexit.register(cleanup, config) @@ -303,6 +308,10 @@ def connectivity_test(client: bigquery.Client) -> CapabilityReport: else: return CapabilityReport(capable=True) + @property + def store_table_refs(self): + return self.config.include_table_lineage or self.config.include_usage_statistics + @staticmethod def metadata_read_capability_test( project_ids: List[str], config: BigQueryV2Config @@ -453,6 +462,7 @@ def _init_schema_resolver(self) -> SchemaResolver: self.config.include_schema_metadata and self.config.include_tables and self.config.include_views + and self.config.include_table_snapshots ) if schema_resolution_required and not schema_ingestion_enabled: @@ -567,6 +577,8 @@ def get_workunits_internal(self) -> Iterable[MetadataWorkUnit]: self.sql_parser_schema_resolver, self.view_refs_by_project, self.view_definitions, + self.snapshot_refs_by_project, + self.snapshots_by_ref, self.table_refs, ) @@ -603,6 +615,7 @@ def _process_project( ) -> Iterable[MetadataWorkUnit]: db_tables: Dict[str, List[BigqueryTable]] = {} db_views: Dict[str, List[BigqueryView]] = {} + db_snapshots: Dict[str, List[BigqueryTableSnapshot]] = {} project_id = bigquery_project.id try: @@ -651,9 +664,9 @@ def _process_project( self.report.report_dropped(f"{bigquery_dataset.name}.*") continue try: - # db_tables and db_views are populated in the this method + # db_tables, db_views, and db_snapshots are populated in the this method yield from self._process_schema( - project_id, bigquery_dataset, db_tables, db_views + project_id, bigquery_dataset, db_tables, db_views, db_snapshots ) except Exception as e: @@ -684,6 +697,7 @@ def _process_schema( bigquery_dataset: BigqueryDataset, db_tables: Dict[str, List[BigqueryTable]], db_views: Dict[str, List[BigqueryView]], + db_snapshots: Dict[str, List[BigqueryTableSnapshot]], ) -> Iterable[MetadataWorkUnit]: dataset_name = bigquery_dataset.name @@ -692,7 +706,11 @@ def _process_schema( ) columns = None - if self.config.include_tables or self.config.include_views: + if ( + self.config.include_tables + or self.config.include_views + or self.config.include_table_snapshots + ): columns = self.bigquery_data_dictionary.get_columns_for_dataset( project_id=project_id, dataset_name=dataset_name, @@ -713,7 +731,7 @@ def _process_schema( project_id=project_id, dataset_name=dataset_name, ) - elif self.config.include_table_lineage or self.config.include_usage_statistics: + elif self.store_table_refs: # Need table_refs to calculate lineage and usage for table_item in self.bigquery_data_dictionary.list_tables( dataset_name, project_id @@ -738,7 +756,10 @@ def _process_schema( if self.config.include_views: db_views[dataset_name] = list( self.bigquery_data_dictionary.get_views_for_dataset( - project_id, dataset_name, self.config.is_profiling_enabled() + project_id, + dataset_name, + self.config.is_profiling_enabled(), + self.report, ) ) @@ -751,6 +772,25 @@ def _process_schema( dataset_name=dataset_name, ) + if self.config.include_table_snapshots: + db_snapshots[dataset_name] = list( + self.bigquery_data_dictionary.get_snapshots_for_dataset( + project_id, + dataset_name, + self.config.is_profiling_enabled(), + self.report, + ) + ) + + for snapshot in db_snapshots[dataset_name]: + snapshot_columns = columns.get(snapshot.name, []) if columns else [] + yield from self._process_snapshot( + snapshot=snapshot, + columns=snapshot_columns, + project_id=project_id, + dataset_name=dataset_name, + ) + # This method is used to generate the ignore list for datatypes the profiler doesn't support we have to do it here # because the profiler doesn't have access to columns def generate_profile_ignore_list(self, columns: List[BigqueryColumn]) -> List[str]: @@ -778,7 +818,7 @@ def _process_table( self.report.report_dropped(table_identifier.raw_table_name()) return - if self.config.include_table_lineage or self.config.include_usage_statistics: + if self.store_table_refs: self.table_refs.add( str(BigQueryTableRef(table_identifier).get_sanitized_table_ref()) ) @@ -827,7 +867,7 @@ def _process_view( self.report.report_dropped(table_identifier.raw_table_name()) return - if self.config.include_table_lineage or self.config.include_usage_statistics: + if self.store_table_refs: table_ref = str( BigQueryTableRef(table_identifier).get_sanitized_table_ref() ) @@ -849,6 +889,48 @@ def _process_view( dataset_name=dataset_name, ) + def _process_snapshot( + self, + snapshot: BigqueryTableSnapshot, + columns: List[BigqueryColumn], + project_id: str, + dataset_name: str, + ) -> Iterable[MetadataWorkUnit]: + table_identifier = BigqueryTableIdentifier( + project_id, dataset_name, snapshot.name + ) + + self.report.snapshots_scanned += 1 + + if not self.config.table_snapshot_pattern.allowed( + table_identifier.raw_table_name() + ): + self.report.report_dropped(table_identifier.raw_table_name()) + return + + snapshot.columns = columns + snapshot.column_count = len(columns) + if not snapshot.column_count: + logger.warning( + f"Snapshot doesn't have any column or unable to get columns for table: {table_identifier}" + ) + + if self.store_table_refs: + table_ref = str( + BigQueryTableRef(table_identifier).get_sanitized_table_ref() + ) + self.table_refs.add(table_ref) + if snapshot.base_table_identifier: + self.snapshot_refs_by_project[project_id].add(table_ref) + self.snapshots_by_ref[table_ref] = snapshot + + yield from self.gen_snapshot_dataset_workunits( + table=snapshot, + columns=columns, + project_id=project_id, + dataset_name=dataset_name, + ) + def gen_table_dataset_workunits( self, table: BigqueryTable, @@ -933,9 +1015,34 @@ def gen_view_dataset_workunits( aspect=view_properties_aspect, ).as_workunit() + def gen_snapshot_dataset_workunits( + self, + table: BigqueryTableSnapshot, + columns: List[BigqueryColumn], + project_id: str, + dataset_name: str, + ) -> Iterable[MetadataWorkUnit]: + custom_properties: Dict[str, str] = {} + if table.ddl: + custom_properties["snapshot_ddl"] = table.ddl + if table.snapshot_time: + custom_properties["snapshot_time"] = str(table.snapshot_time) + if table.size_in_bytes: + custom_properties["size_in_bytes"] = str(table.size_in_bytes) + if table.rows_count: + custom_properties["rows_count"] = str(table.rows_count) + yield from self.gen_dataset_workunits( + table=table, + columns=columns, + project_id=project_id, + dataset_name=dataset_name, + sub_types=[DatasetSubTypes.BIGQUERY_TABLE_SNAPSHOT], + custom_properties=custom_properties, + ) + def gen_dataset_workunits( self, - table: Union[BigqueryTable, BigqueryView], + table: Union[BigqueryTable, BigqueryView, BigqueryTableSnapshot], columns: List[BigqueryColumn], project_id: str, dataset_name: str, @@ -1041,6 +1148,9 @@ def gen_schema_fields(self, columns: List[BigqueryColumn]) -> List[SchemaField]: # TODO: Refractor this such that # converter = HiveColumnToAvroConverter(struct_type_separator=" "); # converter.get_schema_fields_for_hive_column(...) + original_struct_type_separator = ( + HiveColumnToAvroConverter._STRUCT_TYPE_SEPARATOR + ) HiveColumnToAvroConverter._STRUCT_TYPE_SEPARATOR = " " _COMPLEX_TYPE = re.compile("^(struct|array)") last_id = -1 @@ -1101,12 +1211,15 @@ def gen_schema_fields(self, columns: List[BigqueryColumn]) -> List[SchemaField]: ) schema_fields.append(field) last_id = col.ordinal_position + HiveColumnToAvroConverter._STRUCT_TYPE_SEPARATOR = ( + original_struct_type_separator + ) return schema_fields def gen_schema_metadata( self, dataset_urn: str, - table: Union[BigqueryTable, BigqueryView], + table: Union[BigqueryTable, BigqueryView, BigqueryTableSnapshot], columns: List[BigqueryColumn], dataset_name: str, ) -> MetadataWorkUnit: diff --git a/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery_config.py b/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery_config.py index bb14295bc38a8..2f4978d49e687 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery_config.py +++ b/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery_config.py @@ -148,6 +148,15 @@ class BigQueryV2Config( " because the project id is represented as the top-level container.", ) + include_table_snapshots: Optional[bool] = Field( + default=True, description="Whether table snapshots should be ingested." + ) + + table_snapshot_pattern: AllowDenyPattern = Field( + default=AllowDenyPattern.allow_all(), + description="Regex patterns for table snapshots to filter in ingestion. Specify regex to match the entire snapshot name in database.schema.snapshot format. e.g. to match all snapshots starting with customer in Customer database and public schema, use the regex 'Customer.public.customer.*'", + ) + debug_include_full_payloads: bool = Field( default=False, description="Include full payload into events. It is only for debugging and internal use.", diff --git a/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery_report.py b/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery_report.py index 69913b383af87..ad7b86219e7c1 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery_report.py +++ b/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery_report.py @@ -25,6 +25,7 @@ class BigQuerySchemaApiPerfReport(Report): get_tables_for_dataset: PerfTimer = field(default_factory=PerfTimer) list_tables: PerfTimer = field(default_factory=PerfTimer) get_views_for_dataset: PerfTimer = field(default_factory=PerfTimer) + get_snapshots_for_dataset: PerfTimer = field(default_factory=PerfTimer) @dataclass @@ -119,6 +120,8 @@ class BigQueryV2Report(ProfilingSqlReport, IngestionStageReport, BaseTimeWindowR num_usage_query_hash_collisions: int = 0 num_operational_stats_workunits_emitted: int = 0 + snapshots_scanned: int = 0 + num_view_definitions_parsed: int = 0 num_view_definitions_failed_parsing: int = 0 num_view_definitions_failed_column_parsing: int = 0 diff --git a/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery_schema.py b/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery_schema.py index 7edc8656360bb..d918782691c77 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery_schema.py +++ b/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/bigquery_schema.py @@ -106,6 +106,14 @@ class BigqueryView(BaseView): materialized: bool = False +@dataclass +class BigqueryTableSnapshot(BaseTable): + # Upstream table identifier + base_table_identifier: Optional[BigqueryTableIdentifier] = None + snapshot_time: Optional[datetime] = None + columns: List[BigqueryColumn] = field(default_factory=list) + + @dataclass class BigqueryDataset: name: str @@ -116,6 +124,7 @@ class BigqueryDataset: comment: Optional[str] = None tables: List[BigqueryTable] = field(default_factory=list) views: List[BigqueryView] = field(default_factory=list) + snapshots: List[BigqueryTableSnapshot] = field(default_factory=list) columns: List[BigqueryColumn] = field(default_factory=list) @@ -289,10 +298,11 @@ def get_views_for_dataset( project_id: str, dataset_name: str, has_data_read: bool, - report: Optional[BigQueryV2Report] = None, + report: BigQueryV2Report, ) -> Iterator[BigqueryView]: with self.report.get_views_for_dataset as current_timer: if has_data_read: + # If profiling is enabled cur = self.get_query_result( BigqueryQuery.views_for_dataset.format( project_id=project_id, dataset_name=dataset_name @@ -315,11 +325,10 @@ def get_views_for_dataset( f"Error while processing view {view_name}", exc_info=True, ) - if report: - report.report_warning( - "metadata-extraction", - f"Failed to get view {view_name}: {e}", - ) + report.report_warning( + "metadata-extraction", + f"Failed to get view {view_name}: {e}", + ) @staticmethod def _make_bigquery_view(view: bigquery.Row) -> BigqueryView: @@ -334,6 +343,8 @@ def _make_bigquery_view(view: bigquery.Row) -> BigqueryView: comment=view.comment, view_definition=view.view_definition, materialized=view.table_type == BigqueryTableType.MATERIALIZED_VIEW, + size_in_bytes=view.get("size_bytes"), + rows_count=view.get("row_count"), ) def get_columns_for_dataset( @@ -429,3 +440,62 @@ def get_columns_for_table( last_seen_table = column.table_name return columns + + def get_snapshots_for_dataset( + self, + project_id: str, + dataset_name: str, + has_data_read: bool, + report: BigQueryV2Report, + ) -> Iterator[BigqueryTableSnapshot]: + with self.report.get_snapshots_for_dataset as current_timer: + if has_data_read: + # If profiling is enabled + cur = self.get_query_result( + BigqueryQuery.snapshots_for_dataset.format( + project_id=project_id, dataset_name=dataset_name + ), + ) + else: + cur = self.get_query_result( + BigqueryQuery.snapshots_for_dataset_without_data_read.format( + project_id=project_id, dataset_name=dataset_name + ), + ) + + for table in cur: + try: + with current_timer.pause(): + yield BigQuerySchemaApi._make_bigquery_table_snapshot(table) + except Exception as e: + snapshot_name = f"{project_id}.{dataset_name}.{table.table_name}" + logger.warning( + f"Error while processing view {snapshot_name}", + exc_info=True, + ) + report.report_warning( + "metadata-extraction", + f"Failed to get view {snapshot_name}: {e}", + ) + + @staticmethod + def _make_bigquery_table_snapshot(snapshot: bigquery.Row) -> BigqueryTableSnapshot: + return BigqueryTableSnapshot( + name=snapshot.table_name, + created=snapshot.created, + last_altered=datetime.fromtimestamp( + snapshot.get("last_altered") / 1000, tz=timezone.utc + ) + if snapshot.get("last_altered") is not None + else snapshot.created, + comment=snapshot.comment, + ddl=snapshot.ddl, + snapshot_time=snapshot.snapshot_time, + size_in_bytes=snapshot.get("size_bytes"), + rows_count=snapshot.get("row_count"), + base_table_identifier=BigqueryTableIdentifier( + project_id=snapshot.base_table_catalog, + dataset=snapshot.base_table_schema, + table=snapshot.base_table_name, + ), + ) diff --git a/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/lineage.py b/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/lineage.py index b44b06feb95af..7db36867b4e69 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/lineage.py +++ b/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/lineage.py @@ -37,7 +37,10 @@ ) from datahub.ingestion.source.bigquery_v2.bigquery_config import BigQueryV2Config from datahub.ingestion.source.bigquery_v2.bigquery_report import BigQueryV2Report -from datahub.ingestion.source.bigquery_v2.bigquery_schema import BigQuerySchemaApi +from datahub.ingestion.source.bigquery_v2.bigquery_schema import ( + BigQuerySchemaApi, + BigqueryTableSnapshot, +) from datahub.ingestion.source.bigquery_v2.common import BQ_DATETIME_FORMAT from datahub.ingestion.source.bigquery_v2.queries import ( BQ_FILTER_RULE_TEMPLATE_V2_LINEAGE, @@ -198,6 +201,28 @@ def make_lineage_edges_from_parsing_result( return list(table_edges.values()) +def make_lineage_edge_for_snapshot( + snapshot: BigqueryTableSnapshot, +) -> Optional[LineageEdge]: + if snapshot.base_table_identifier: + base_table_name = str( + BigQueryTableRef.from_bigquery_table(snapshot.base_table_identifier) + ) + return LineageEdge( + table=base_table_name, + column_mapping=frozenset( + LineageEdgeColumnMapping( + out_column=column.field_path, + in_columns=frozenset([column.field_path]), + ) + for column in snapshot.columns + ), + auditStamp=datetime.now(timezone.utc), + type=DatasetLineageTypeClass.TRANSFORMED, + ) + return None + + class BigqueryLineageExtractor: def __init__( self, @@ -256,27 +281,35 @@ def get_lineage_workunits( sql_parser_schema_resolver: SchemaResolver, view_refs_by_project: Dict[str, Set[str]], view_definitions: FileBackedDict[str], + snapshot_refs_by_project: Dict[str, Set[str]], + snapshots_by_ref: FileBackedDict[BigqueryTableSnapshot], table_refs: Set[str], ) -> Iterable[MetadataWorkUnit]: if not self._should_ingest_lineage(): return - views_skip_audit_log_lineage: Set[str] = set() - if self.config.lineage_parse_view_ddl: - view_lineage: Dict[str, Set[LineageEdge]] = {} - for project in projects: + datasets_skip_audit_log_lineage: Set[str] = set() + dataset_lineage: Dict[str, Set[LineageEdge]] = {} + for project in projects: + self.populate_snapshot_lineage( + dataset_lineage, + snapshot_refs_by_project[project], + snapshots_by_ref, + ) + + if self.config.lineage_parse_view_ddl: self.populate_view_lineage_with_sql_parsing( - view_lineage, + dataset_lineage, view_refs_by_project[project], view_definitions, sql_parser_schema_resolver, project, ) - views_skip_audit_log_lineage.update(view_lineage.keys()) - for lineage_key in view_lineage.keys(): - yield from self.gen_lineage_workunits_for_table( - view_lineage, BigQueryTableRef.from_string_name(lineage_key) - ) + datasets_skip_audit_log_lineage.update(dataset_lineage.keys()) + for lineage_key in dataset_lineage.keys(): + yield from self.gen_lineage_workunits_for_table( + dataset_lineage, BigQueryTableRef.from_string_name(lineage_key) + ) if self.config.use_exported_bigquery_audit_metadata: projects = ["*"] # project_id not used when using exported metadata @@ -286,7 +319,7 @@ def get_lineage_workunits( yield from self.generate_lineage( project, sql_parser_schema_resolver, - views_skip_audit_log_lineage, + datasets_skip_audit_log_lineage, table_refs, ) @@ -300,7 +333,7 @@ def generate_lineage( self, project_id: str, sql_parser_schema_resolver: SchemaResolver, - views_skip_audit_log_lineage: Set[str], + datasets_skip_audit_log_lineage: Set[str], table_refs: Set[str], ) -> Iterable[MetadataWorkUnit]: logger.info(f"Generate lineage for {project_id}") @@ -338,7 +371,7 @@ def generate_lineage( # as they may contain indirectly referenced tables. if ( lineage_key not in table_refs - or lineage_key in views_skip_audit_log_lineage + or lineage_key in datasets_skip_audit_log_lineage ): continue @@ -387,6 +420,17 @@ def populate_view_lineage_with_sql_parsing( ) ) + def populate_snapshot_lineage( + self, + snapshot_lineage: Dict[str, Set[LineageEdge]], + snapshot_refs: Set[str], + snapshots_by_ref: FileBackedDict[BigqueryTableSnapshot], + ) -> None: + for snapshot in snapshot_refs: + lineage_edge = make_lineage_edge_for_snapshot(snapshots_by_ref[snapshot]) + if lineage_edge: + snapshot_lineage[snapshot] = {lineage_edge} + def gen_lineage_workunits_for_table( self, lineage: Dict[str, Set[LineageEdge]], table_ref: BigQueryTableRef ) -> Iterable[MetadataWorkUnit]: diff --git a/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/queries.py b/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/queries.py index 67fcc33cdf218..86971fce36a53 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/queries.py +++ b/metadata-ingestion/src/datahub/ingestion/source/bigquery_v2/queries.py @@ -157,6 +157,62 @@ class BigqueryQuery: table_name ASC """ + snapshots_for_dataset: str = f""" +SELECT + t.table_catalog as table_catalog, + t.table_schema as table_schema, + t.table_name as table_name, + t.table_type as table_type, + t.creation_time as created, + t.is_insertable_into, + t.ddl, + t.snapshot_time_ms as snapshot_time, + t.base_table_catalog, + t.base_table_schema, + t.base_table_name, + ts.last_modified_time as last_altered, + tos.OPTION_VALUE as comment, + ts.row_count, + ts.size_bytes +FROM + `{{project_id}}`.`{{dataset_name}}`.INFORMATION_SCHEMA.TABLES t + join `{{project_id}}`.`{{dataset_name}}`.__TABLES__ as ts on ts.table_id = t.TABLE_NAME + left join `{{project_id}}`.`{{dataset_name}}`.INFORMATION_SCHEMA.TABLE_OPTIONS as tos on t.table_schema = tos.table_schema + and t.TABLE_NAME = tos.TABLE_NAME + and tos.OPTION_NAME = "description" +WHERE + table_type = '{BigqueryTableType.SNAPSHOT}' +order by + table_schema ASC, + table_name ASC +""" + + snapshots_for_dataset_without_data_read: str = f""" +SELECT + t.table_catalog as table_catalog, + t.table_schema as table_schema, + t.table_name as table_name, + t.table_type as table_type, + t.creation_time as created, + t.is_insertable_into, + t.ddl, + t.snapshot_time_ms as snapshot_time, + t.base_table_catalog, + t.base_table_schema, + t.base_table_name, + tos.OPTION_VALUE as comment, +FROM + `{{project_id}}`.`{{dataset_name}}`.INFORMATION_SCHEMA.TABLES t + left join `{{project_id}}`.`{{dataset_name}}`.INFORMATION_SCHEMA.TABLE_OPTIONS as tos on t.table_schema = tos.table_schema + and t.TABLE_NAME = tos.TABLE_NAME + and tos.OPTION_NAME = "description" +WHERE + table_type = '{BigqueryTableType.SNAPSHOT}' +order by + table_schema ASC, + table_name ASC +""" + columns_for_dataset: str = """ select c.table_catalog as table_catalog, diff --git a/metadata-ingestion/src/datahub/ingestion/source/common/subtypes.py b/metadata-ingestion/src/datahub/ingestion/source/common/subtypes.py index 741b4789bef21..3296a8fb29354 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/common/subtypes.py +++ b/metadata-ingestion/src/datahub/ingestion/source/common/subtypes.py @@ -15,6 +15,7 @@ class DatasetSubTypes(str, Enum): SALESFORCE_CUSTOM_OBJECT = "Custom Object" SALESFORCE_STANDARD_OBJECT = "Object" POWERBI_DATASET_TABLE = "PowerBI Dataset Table" + BIGQUERY_TABLE_SNAPSHOT = "Bigquery Table Snapshot" # TODO: Create separate entity... NOTEBOOK = "Notebook" diff --git a/metadata-ingestion/tests/unit/test_bigquery_source.py b/metadata-ingestion/tests/unit/test_bigquery_source.py index 3cdb73d77d0a1..42d65fdf02683 100644 --- a/metadata-ingestion/tests/unit/test_bigquery_source.py +++ b/metadata-ingestion/tests/unit/test_bigquery_source.py @@ -11,6 +11,7 @@ from google.cloud.bigquery.table import Row, TableListItem from datahub.configuration.common import AllowDenyPattern +from datahub.emitter.mcp import MetadataChangeProposalWrapper from datahub.ingestion.api.common import PipelineContext from datahub.ingestion.source.bigquery_v2.bigquery import BigqueryV2Source from datahub.ingestion.source.bigquery_v2.bigquery_audit import ( @@ -27,6 +28,7 @@ BigqueryDataset, BigqueryProject, BigQuerySchemaApi, + BigqueryTableSnapshot, BigqueryView, ) from datahub.ingestion.source.bigquery_v2.lineage import ( @@ -34,7 +36,10 @@ LineageEdgeColumnMapping, ) from datahub.metadata.com.linkedin.pegasus2avro.dataset import ViewProperties -from datahub.metadata.schema_classes import MetadataChangeProposalClass +from datahub.metadata.schema_classes import ( + DatasetPropertiesClass, + MetadataChangeProposalClass, +) def test_bigquery_uri(): @@ -769,6 +774,7 @@ def test_get_views_for_dataset( project_id="test-project", dataset_name="test-dataset", has_data_read=False, + report=BigQueryV2Report(), ) assert list(views) == [bigquery_view_1, bigquery_view_2] @@ -810,6 +816,89 @@ def test_gen_view_dataset_workunits( ) +@pytest.fixture +def bigquery_snapshot() -> BigqueryTableSnapshot: + now = datetime.now(tz=timezone.utc) + return BigqueryTableSnapshot( + name="table-snapshot", + created=now - timedelta(days=10), + last_altered=now - timedelta(hours=1), + comment="comment1", + ddl="CREATE SNAPSHOT TABLE 1", + size_in_bytes=None, + rows_count=None, + snapshot_time=now - timedelta(days=10), + base_table_identifier=BigqueryTableIdentifier( + project_id="test-project", + dataset="test-dataset", + table="test-table", + ), + ) + + +@patch.object(BigQuerySchemaApi, "get_query_result") +@patch.object(BigQueryV2Config, "get_bigquery_client") +def test_get_snapshots_for_dataset( + get_bq_client_mock: Mock, + query_mock: Mock, + bigquery_snapshot: BigqueryTableSnapshot, +) -> None: + client_mock = MagicMock() + get_bq_client_mock.return_value = client_mock + assert bigquery_snapshot.last_altered + assert bigquery_snapshot.base_table_identifier + row1 = create_row( + dict( + table_name=bigquery_snapshot.name, + created=bigquery_snapshot.created, + last_altered=bigquery_snapshot.last_altered.timestamp() * 1000, + comment=bigquery_snapshot.comment, + ddl=bigquery_snapshot.ddl, + snapshot_time=bigquery_snapshot.snapshot_time, + table_type="SNAPSHOT", + base_table_catalog=bigquery_snapshot.base_table_identifier.project_id, + base_table_schema=bigquery_snapshot.base_table_identifier.dataset, + base_table_name=bigquery_snapshot.base_table_identifier.table, + ) + ) + query_mock.return_value = [row1] + bigquery_data_dictionary = BigQuerySchemaApi( + BigQueryV2Report().schema_api_perf, client_mock + ) + + snapshots = bigquery_data_dictionary.get_snapshots_for_dataset( + project_id="test-project", + dataset_name="test-dataset", + has_data_read=False, + report=BigQueryV2Report(), + ) + assert list(snapshots) == [bigquery_snapshot] + + +@patch.object(BigQueryV2Config, "get_bigquery_client") +def test_gen_snapshot_dataset_workunits(get_bq_client_mock, bigquery_snapshot): + project_id = "test-project" + dataset_name = "test-dataset" + config = BigQueryV2Config.parse_obj( + { + "project_id": project_id, + } + ) + source: BigqueryV2Source = BigqueryV2Source( + config=config, ctx=PipelineContext(run_id="test") + ) + + gen = source.gen_snapshot_dataset_workunits( + bigquery_snapshot, [], project_id, dataset_name + ) + mcp = cast(MetadataChangeProposalWrapper, list(gen)[2].metadata) + dataset_properties = cast(DatasetPropertiesClass, mcp.aspect) + assert dataset_properties.customProperties["snapshot_ddl"] == bigquery_snapshot.ddl + assert dataset_properties.customProperties["snapshot_time"] == str( + bigquery_snapshot.snapshot_time + ) + + @pytest.mark.parametrize( "table_name, expected_table_prefix, expected_shard", [ From a78c6899a2cde4277d854b264feef313f929531d Mon Sep 17 00:00:00 2001 From: Chris Collins Date: Thu, 25 Jan 2024 10:12:01 -0500 Subject: [PATCH 08/19] feat(ui) Add structured properties support in the UI (#9695) --- datahub-web-react/src/Mocks.tsx | 5 + .../components/SchemaDescriptionField.tsx | 14 +- .../components/legacy/DescriptionModal.tsx | 2 +- .../shared/components/styled/EntityIcon.tsx | 24 ++ .../containers/profile/EntityProfile.tsx | 29 +-- .../profile/sidebar/EntitySidebar.tsx | 4 +- .../profile/sidebar/ProfileSidebar.tsx | 77 +++++++ .../shared/tabs/Dataset/Schema/SchemaTab.tsx | 10 +- .../tabs/Dataset/Schema/SchemaTable.tsx | 103 ++++++--- .../Schema/components/ChildCountLabel.tsx | 32 +++ .../Schema/components/PropertiesColumn.tsx | 30 +++ .../Schema/components/PropertyTypeLabel.tsx | 39 ++++ .../SchemaFieldDrawer/DrawerHeader.tsx | 106 +++++++++ .../SchemaFieldDrawer/FieldDescription.tsx | 115 ++++++++++ .../SchemaFieldDrawer/FieldHeader.tsx | 60 +++++ .../SchemaFieldDrawer/FieldProperties.tsx | 70 ++++++ .../SchemaFieldDrawer/FieldTags.tsx | 33 +++ .../SchemaFieldDrawer/FieldTerms.tsx | 34 +++ .../SchemaFieldDrawer/FieldUsageStats.tsx | 59 +++++ .../SchemaFieldDrawer/SchemaFieldDrawer.tsx | 83 +++++++ .../SchemaFieldDrawer/components.ts | 12 + .../Schema/utils/useDescriptionRenderer.tsx | 2 +- .../Schema/utils/useTagsAndTermsRenderer.tsx | 38 ++-- .../Schema/utils/useUsageStatsRenderer.tsx | 2 +- .../components/editor/Editor.tsx | 5 +- .../tabs/Properties/CardinalityLabel.tsx | 43 ++++ .../shared/tabs/Properties/NameColumn.tsx | 87 +++++++ .../shared/tabs/Properties/PropertiesTab.tsx | 91 +++++--- .../Properties/StructuredPropertyTooltip.tsx | 31 +++ .../Properties/StructuredPropertyValue.tsx | 69 ++++++ .../shared/tabs/Properties/TabHeader.tsx | 32 +++ .../shared/tabs/Properties/ValuesColumn.tsx | 24 ++ .../tabs/Properties/__tests__/utils.test.ts | 87 +++++++ .../entity/shared/tabs/Properties/types.ts | 25 ++ .../Properties/useStructuredProperties.tsx | 215 ++++++++++++++++++ .../useUpdateExpandedRowsFromFilter.ts | 23 ++ .../entity/shared/tabs/Properties/utils.ts | 68 ++++++ .../src/app/entity/shared/types.ts | 2 + .../src/app/entity/shared/utils.ts | 12 +- .../src/graphql/fragments.graphql | 77 +++++++ .../tests/cypress/cypress/e2e/login/login.js | 4 +- .../e2e/mutations/edit_documentation.js | 9 +- .../cypress/e2e/mutations/mutations.js | 19 +- .../cypress/e2e/schema_blame/schema_blame.js | 2 + .../tests/cypress/cypress/support/commands.js | 4 + 45 files changed, 1772 insertions(+), 140 deletions(-) create mode 100644 datahub-web-react/src/app/entity/shared/components/styled/EntityIcon.tsx create mode 100644 datahub-web-react/src/app/entity/shared/containers/profile/sidebar/ProfileSidebar.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/ChildCountLabel.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/PropertiesColumn.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/PropertyTypeLabel.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/DrawerHeader.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldDescription.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldHeader.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldProperties.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldTags.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldTerms.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldUsageStats.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/components.ts create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Properties/CardinalityLabel.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Properties/NameColumn.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Properties/StructuredPropertyTooltip.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Properties/StructuredPropertyValue.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Properties/TabHeader.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Properties/ValuesColumn.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Properties/__tests__/utils.test.ts create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Properties/types.ts create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Properties/useStructuredProperties.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Properties/useUpdateExpandedRowsFromFilter.ts create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Properties/utils.ts diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index 03d6f4a624c3d..9f339bb7db548 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -297,6 +297,7 @@ export const dataset1 = { embed: null, browsePathV2: { path: [{ name: 'test', entity: null }], __typename: 'BrowsePathV2' }, autoRenderAspects: [], + structuredProperties: null, }; export const dataset2 = { @@ -393,6 +394,7 @@ export const dataset2 = { embed: null, browsePathV2: { path: [{ name: 'test', entity: null }], __typename: 'BrowsePathV2' }, autoRenderAspects: [], + structuredProperties: null, }; export const dataset3 = { @@ -626,6 +628,7 @@ export const dataset3 = { dataProduct: null, lastProfile: null, lastOperation: null, + structuredProperties: null, } as Dataset; export const dataset3WithSchema = { @@ -650,6 +653,7 @@ export const dataset3WithSchema = { globalTags: null, glossaryTerms: null, label: 'hi', + schemaFieldEntity: null, }, { __typename: 'SchemaField', @@ -665,6 +669,7 @@ export const dataset3WithSchema = { globalTags: null, glossaryTerms: null, label: 'hi', + schemaFieldEntity: null, }, ], hash: '', diff --git a/datahub-web-react/src/app/entity/dataset/profile/schema/components/SchemaDescriptionField.tsx b/datahub-web-react/src/app/entity/dataset/profile/schema/components/SchemaDescriptionField.tsx index 1d4f155f797e0..2cd4cbd6dcb6c 100644 --- a/datahub-web-react/src/app/entity/dataset/profile/schema/components/SchemaDescriptionField.tsx +++ b/datahub-web-react/src/app/entity/dataset/profile/schema/components/SchemaDescriptionField.tsx @@ -86,6 +86,7 @@ type Props = { description: string, ) => Promise, Record> | void>; isEdited?: boolean; + isReadOnly?: boolean; }; const ABBREVIATED_LIMIT = 80; @@ -97,10 +98,11 @@ export default function DescriptionField({ onUpdate, isEdited = false, original, + isReadOnly, }: Props) { const [showAddModal, setShowAddModal] = useState(false); const overLimit = removeMarkdown(description).length > 80; - const isSchemaEditable = React.useContext(SchemaEditableContext); + const isSchemaEditable = React.useContext(SchemaEditableContext) && !isReadOnly; const onCloseModal = () => setShowAddModal(false); const { urn, entityType } = useEntityData(); @@ -140,11 +142,12 @@ export default function DescriptionField({ {expanded || !overLimit ? ( <> {!!description && } - {!!description && ( + {!!description && (EditButton || overLimit) && ( {overLimit && ( { + onClick={(e) => { + e.stopPropagation(); handleExpanded(false); }} > @@ -162,7 +165,8 @@ export default function DescriptionField({ readMore={ <> { + onClick={(e) => { + e.stopPropagation(); handleExpanded(true); }} > @@ -177,7 +181,7 @@ export default function DescriptionField({ )} - {isSchemaEditable && isEdited && (edited)} + {isEdited && (edited)} {showAddModal && (
- + {!isAddDesc && description && original && ( Original:}> diff --git a/datahub-web-react/src/app/entity/shared/components/styled/EntityIcon.tsx b/datahub-web-react/src/app/entity/shared/components/styled/EntityIcon.tsx new file mode 100644 index 0000000000000..bd001b51d53ce --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/components/styled/EntityIcon.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useEntityRegistry } from '../../../../useEntityRegistry'; +import { PlatformIcon } from '../../../../search/filters/utils'; +import { Entity } from '../../../../../types.generated'; +import { IconStyleType } from '../../../Entity'; +import { ANTD_GRAY } from '../../constants'; + +interface Props { + entity: Entity; + size?: number; +} + +export default function EntityIcon({ entity, size = 14 }: Props) { + const entityRegistry = useEntityRegistry(); + const genericEntityProps = entityRegistry.getGenericEntityProperties(entity.type, entity); + const logoUrl = genericEntityProps?.platform?.properties?.logoUrl; + const icon = logoUrl ? ( + + ) : ( + entityRegistry.getIcon(entity.type, size, IconStyleType.ACCENT, ANTD_GRAY[9]) + ); + + return <>{icon}; +} diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/EntityProfile.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/EntityProfile.tsx index d7b7a4da804ef..a781c732c9de6 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/EntityProfile.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/EntityProfile.tsx @@ -30,7 +30,6 @@ import LineageExplorer from '../../../../lineage/LineageExplorer'; import CompactContext from '../../../../shared/CompactContext'; import DynamicTab from '../../tabs/Entity/weaklyTypedAspects/DynamicTab'; import analytics, { EventType } from '../../../../analytics'; -import { ProfileSidebarResizer } from './sidebar/ProfileSidebarResizer'; import { EntityMenuItems } from '../../EntityDropdown/EntityDropdown'; import { useIsSeparateSiblingsMode } from '../../siblingUtils'; import { EntityActionItem } from '../../entity/EntityActions'; @@ -45,6 +44,7 @@ import { } from '../../../../onboarding/config/LineageGraphOnboardingConfig'; import { useAppConfig } from '../../../../useAppConfig'; import { useUpdateDomainEntityDataOnChange } from '../../../../domain/utils'; +import ProfileSidebar from './sidebar/ProfileSidebar'; type Props = { urn: string; @@ -75,8 +75,6 @@ type Props = { isNameEditable?: boolean; }; -const MAX_SIDEBAR_WIDTH = 800; -const MIN_SIDEBAR_WIDTH = 200; const MAX_COMPACT_WIDTH = 490 - 24 * 2; const ContentContainer = styled.div` @@ -85,6 +83,7 @@ const ContentContainer = styled.div` min-height: 100%; flex: 1; min-width: 0; + overflow: hidden; `; const HeaderAndTabs = styled.div` @@ -113,15 +112,6 @@ const HeaderAndTabsFlex = styled.div` -webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.75); } `; -const Sidebar = styled.div<{ $width: number }>` - max-height: 100%; - overflow: auto; - width: ${(props) => props.$width}px; - min-width: ${(props) => props.$width}px; - padding-left: 20px; - padding-right: 20px; - padding-bottom: 20px; -`; const Header = styled.div` border-bottom: 1px solid ${ANTD_GRAY[4.5]}; @@ -145,7 +135,7 @@ const defaultTabDisplayConfig = { enabled: (_, _1) => true, }; -const defaultSidebarSection = { +export const DEFAULT_SIDEBAR_SECTION = { visible: (_, _1) => true, }; @@ -176,11 +166,10 @@ export const EntityProfile = ({ const sortedTabs = sortEntityProfileTabs(appConfig.config, entityType, tabsWithDefaults); const sideBarSectionsWithDefaults = sidebarSections.map((sidebarSection) => ({ ...sidebarSection, - display: { ...defaultSidebarSection, ...sidebarSection.display }, + display: { ...DEFAULT_SIDEBAR_SECTION, ...sidebarSection.display }, })); const [shouldRefetchEmbeddedListSearch, setShouldRefetchEmbeddedListSearch] = useState(false); - const [sidebarWidth, setSidebarWidth] = useState(window.innerWidth * 0.25); const entityStepIds: string[] = getOnboardingStepIdsForEntityType(entityType); const lineageGraphStepIds: string[] = [LINEAGE_GRAPH_INTRO_ID, LINEAGE_GRAPH_TIME_FILTER_ID]; const stepIds = isLineageMode ? lineageGraphStepIds : entityStepIds; @@ -344,15 +333,7 @@ export const EntityProfile = ({ - - setSidebarWidth(Math.min(Math.max(width, MIN_SIDEBAR_WIDTH), MAX_SIDEBAR_WIDTH)) - } - initialSize={sidebarWidth} - /> - - - + )} diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/EntitySidebar.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/EntitySidebar.tsx index fbece870706f5..a8d1dceb71ec9 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/EntitySidebar.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/EntitySidebar.tsx @@ -36,14 +36,16 @@ const LastIngestedSection = styled.div` type Props = { sidebarSections: EntitySidebarSection[]; + topSection?: EntitySidebarSection; }; -export const EntitySidebar = ({ sidebarSections }: Props) => { +export const EntitySidebar = ({ sidebarSections, topSection }: Props) => { const { entityData } = useEntityData(); const baseEntity = useBaseEntity(); return ( <> + {topSection && } {entityData?.lastIngested && ( diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/ProfileSidebar.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/ProfileSidebar.tsx new file mode 100644 index 0000000000000..b5e6737c16641 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/ProfileSidebar.tsx @@ -0,0 +1,77 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { ProfileSidebarResizer } from './ProfileSidebarResizer'; +import { EntitySidebar } from './EntitySidebar'; +import { EntitySidebarSection } from '../../../types'; + +export const MAX_SIDEBAR_WIDTH = 800; +export const MIN_SIDEBAR_WIDTH = 200; + +const Sidebar = styled.div<{ $width: number; backgroundColor?: string }>` + max-height: 100%; + position: relative; + width: ${(props) => props.$width}px; + min-width: ${(props) => props.$width}px; + ${(props) => props.backgroundColor && `background-color: ${props.backgroundColor};`} +`; + +const ScrollWrapper = styled.div` + overflow: auto; + max-height: 100%; + padding: 0 20px 20px 20px; +`; + +const DEFAULT_SIDEBAR_SECTION = { + visible: (_, _1) => true, +}; + +interface Props { + sidebarSections: EntitySidebarSection[]; + backgroundColor?: string; + topSection?: EntitySidebarSection; + alignLeft?: boolean; +} + +export default function ProfileSidebar({ sidebarSections, backgroundColor, topSection, alignLeft }: Props) { + const sideBarSectionsWithDefaults = sidebarSections.map((sidebarSection) => ({ + ...sidebarSection, + display: { ...DEFAULT_SIDEBAR_SECTION, ...sidebarSection.display }, + })); + + const [sidebarWidth, setSidebarWidth] = useState(window.innerWidth * 0.25); + + if (alignLeft) { + return ( + <> + + + + + + + setSidebarWidth(Math.min(Math.max(width, MIN_SIDEBAR_WIDTH), MAX_SIDEBAR_WIDTH)) + } + initialSize={sidebarWidth} + isSidebarOnLeft + /> + + ); + } + + return ( + <> + + setSidebarWidth(Math.min(Math.max(width, MIN_SIDEBAR_WIDTH), MAX_SIDEBAR_WIDTH)) + } + initialSize={sidebarWidth} + /> + + + + + + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTab.tsx index 75027e17b6d0c..28dc3ba5c6ce5 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTab.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTab.tsx @@ -76,6 +76,14 @@ export const SchemaTab = ({ properties }: { properties?: any }) => { [schemaMetadata], ); + const hasProperties = useMemo( + () => + entityWithSchema?.schemaMetadata?.fields.some( + (schemaField) => !!schemaField.schemaFieldEntity?.structuredProperties?.properties?.length, + ), + [entityWithSchema], + ); + const [showKeySchema, setShowKeySchema] = useState(false); const [showSchemaAuditView, setShowSchemaAuditView] = useState(false); @@ -190,13 +198,13 @@ export const SchemaTab = ({ properties }: { properties?: any }) => { diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx index 41b92aea93b5a..bd092e86b3584 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx @@ -21,9 +21,10 @@ import { StyledTable } from '../../../components/styled/StyledTable'; import { SchemaRow } from './components/SchemaRow'; import { FkContext } from './utils/selectedFkContext'; import useSchemaBlameRenderer from './utils/useSchemaBlameRenderer'; -import { ANTD_GRAY } from '../../../constants'; -import MenuColumn from './components/MenuColumn'; +import { ANTD_GRAY, ANTD_GRAY_V2 } from '../../../constants'; import translateFieldPath from '../../../../dataset/profile/schema/utils/translateFieldPath'; +import PropertiesColumn from './components/PropertiesColumn'; +import SchemaFieldDrawer from './components/SchemaFieldDrawer/SchemaFieldDrawer'; const TableContainer = styled.div` overflow: inherit; @@ -41,18 +42,36 @@ const TableContainer = styled.div` padding-bottom: 600px; vertical-align: top; } + + &&& .ant-table-cell { + background-color: inherit; + cursor: pointer; + } + + &&& tbody > tr:hover > td { + background-color: ${ANTD_GRAY_V2[2]}; + } + + &&& .expanded-row { + background-color: ${(props) => props.theme.styles['highlight-color']} !important; + + td { + background-color: ${(props) => props.theme.styles['highlight-color']} !important; + } + } `; export type Props = { rows: Array; schemaMetadata: SchemaMetadata | undefined | null; editableSchemaMetadata?: EditableSchemaMetadata | null; - editMode?: boolean; usageStats?: UsageQueryResult | null; schemaFieldBlameList?: Array | null; showSchemaAuditView: boolean; expandedRowsFromFilter?: Set; filterText?: string; + hasProperties?: boolean; + inputFields?: SchemaField[]; }; const EMPTY_SET: Set = new Set(); @@ -63,56 +82,46 @@ export default function SchemaTable({ schemaMetadata, editableSchemaMetadata, usageStats, - editMode = true, schemaFieldBlameList, showSchemaAuditView, expandedRowsFromFilter = EMPTY_SET, filterText = '', + hasProperties, + inputFields, }: Props): JSX.Element { const hasUsageStats = useMemo(() => (usageStats?.aggregations?.fields?.length || 0) > 0, [usageStats]); const [tableHeight, setTableHeight] = useState(0); - const [tagHoveredIndex, setTagHoveredIndex] = useState(undefined); - const [selectedFkFieldPath, setSelectedFkFieldPath] = - useState(null); + const [selectedFkFieldPath, setSelectedFkFieldPath] = useState(null); + const [expandedDrawerFieldPath, setExpandedDrawerFieldPath] = useState(null); + + const schemaFields = schemaMetadata ? schemaMetadata.fields : inputFields; const descriptionRender = useDescriptionRenderer(editableSchemaMetadata); const usageStatsRenderer = useUsageStatsRenderer(usageStats); const tagRenderer = useTagsAndTermsRenderer( editableSchemaMetadata, - tagHoveredIndex, - setTagHoveredIndex, { showTags: true, showTerms: false, }, filterText, + false, ); const termRenderer = useTagsAndTermsRenderer( editableSchemaMetadata, - tagHoveredIndex, - setTagHoveredIndex, { showTags: false, showTerms: true, }, filterText, + false, ); const schemaTitleRenderer = useSchemaTitleRenderer(schemaMetadata, setSelectedFkFieldPath, filterText); const schemaBlameRenderer = useSchemaBlameRenderer(schemaFieldBlameList); - const onTagTermCell = (record: SchemaField) => ({ - onMouseEnter: () => { - if (editMode) { - setTagHoveredIndex(record.fieldPath); - } - }, - onMouseLeave: () => { - if (editMode) { - setTagHoveredIndex(undefined); - } - }, - }); - const fieldColumn = { width: '22%', title: 'Field', @@ -139,7 +148,6 @@ export default function SchemaTable({ dataIndex: 'globalTags', key: 'tag', render: tagRenderer, - onCell: onTagTermCell, }; const termColumn = { @@ -148,7 +156,6 @@ export default function SchemaTable({ dataIndex: 'globalTags', key: 'tag', render: termRenderer, - onCell: onTagTermCell, }; const blameColumn = { @@ -184,16 +191,20 @@ export default function SchemaTable({ sorter: (sourceA, sourceB) => getCount(sourceA.fieldPath) - getCount(sourceB.fieldPath), }; - const menuColumn = { - width: '5%', - title: '', + const propertiesColumn = { + width: '13%', + title: 'Properties', dataIndex: '', key: 'menu', - render: (field: SchemaField) => , + render: (field: SchemaField) => , }; let allColumns: ColumnsType = [fieldColumn, descriptionColumn, tagColumn, termColumn]; + if (hasProperties) { + allColumns = [...allColumns, propertiesColumn]; + } + if (hasUsageStats) { allColumns = [...allColumns, usageColumn]; } @@ -202,8 +213,6 @@ export default function SchemaTable({ allColumns = [...allColumns, blameColumn]; } - allColumns = [...allColumns, menuColumn]; - const [expandedRows, setExpandedRows] = useState>(new Set()); useEffect(() => { @@ -224,9 +233,15 @@ export default function SchemaTable({ setTableHeight(dimensions.height - TABLE_HEADER_HEIGHT)}> - record.fieldPath === selectedFkFieldPath?.fieldPath ? 'open-fk-row' : '' - } + rowClassName={(record) => { + if (record.fieldPath === selectedFkFieldPath?.fieldPath) { + return 'open-fk-row'; + } + if (expandedDrawerFieldPath === record.fieldPath) { + return 'expanded-row'; + } + return ''; + }} columns={allColumns} dataSource={rows} rowKey="fieldPath" @@ -250,9 +265,27 @@ export default function SchemaTable({ indentSize: 0, }} pagination={false} + onRow={(record) => ({ + onClick: () => { + setExpandedDrawerFieldPath( + expandedDrawerFieldPath === record.fieldPath ? null : record.fieldPath, + ); + }, + style: { + backgroundColor: expandedDrawerFieldPath === record.fieldPath ? `` : 'white', + }, + })} /> + {!!schemaFields && ( + + )} ); } diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/ChildCountLabel.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/ChildCountLabel.tsx new file mode 100644 index 0000000000000..44bd48620649a --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/ChildCountLabel.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Badge } from 'antd'; +import styled from 'styled-components'; + +import { ANTD_GRAY_V2 } from '../../../../constants'; + +type Props = { + count: number; +}; + +const ChildCountBadge = styled(Badge)` + margin-left: 10px; + margin-top: 16px; + margin-bottom: 16px; + &&& .ant-badge-count { + background-color: ${ANTD_GRAY_V2[1]}; + color: ${ANTD_GRAY_V2[8]}; + box-shadow: 0 2px 1px -1px ${ANTD_GRAY_V2[6]}; + border-radius: 4px 4px 4px 4px; + font-size: 12px; + font-weight: 500; + height: 22px; + font-family: 'Manrope'; + } +`; + +export default function ChildCountLabel({ count }: Props) { + const propertyString = count > 1 ? ' properties' : ' property'; + + // eslint-disable-next-line + return ; +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/PropertiesColumn.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/PropertiesColumn.tsx new file mode 100644 index 0000000000000..b74de3e94e554 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/PropertiesColumn.tsx @@ -0,0 +1,30 @@ +import { ControlOutlined } from '@ant-design/icons'; +import React from 'react'; +import styled from 'styled-components'; +import { SchemaField } from '../../../../../../../types.generated'; + +const ColumnWrapper = styled.div` + font-size: 14px; +`; + +const StyledIcon = styled(ControlOutlined)` + margin-right: 4px; +`; + +interface Props { + field: SchemaField; +} + +export default function PropertiesColumn({ field }: Props) { + const { schemaFieldEntity } = field; + const numProperties = schemaFieldEntity?.structuredProperties?.properties?.length; + + if (!schemaFieldEntity || !numProperties) return null; + + return ( + + + {numProperties} {numProperties === 1 ? 'property' : 'properties'} + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/PropertyTypeLabel.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/PropertyTypeLabel.tsx new file mode 100644 index 0000000000000..366fc4762b210 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/PropertyTypeLabel.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Badge } from 'antd'; +import styled from 'styled-components'; +import { capitalizeFirstLetterOnly } from '../../../../../../shared/textUtil'; +import { DataTypeEntity, SchemaFieldDataType } from '../../../../../../../types.generated'; +import { truncate } from '../../../../utils'; +import { ANTD_GRAY, ANTD_GRAY_V2 } from '../../../../constants'; +import { TypeData } from '../../../Properties/types'; + +type Props = { + type: TypeData; + dataType?: DataTypeEntity; +}; + +export const PropertyTypeBadge = styled(Badge)` + margin: 4px 0 4px 8px; + &&& .ant-badge-count { + background-color: ${ANTD_GRAY[1]}; + color: ${ANTD_GRAY_V2[8]}; + border: 1px solid ${ANTD_GRAY_V2[6]}; + font-size: 12px; + font-weight: 500; + height: 22px; + font-family: 'Manrope'; + } +`; + +export default function PropertyTypeLabel({ type, dataType }: Props) { + // if unable to match type to DataHub, display native type info by default + const { nativeDataType } = type; + const nativeFallback = type.type === SchemaFieldDataType.Null; + + const typeText = + dataType?.info.displayName || + dataType?.info.type || + (nativeFallback ? truncate(250, nativeDataType) : type.type); + + return ; +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/DrawerHeader.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/DrawerHeader.tsx new file mode 100644 index 0000000000000..13f8ec869126d --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/DrawerHeader.tsx @@ -0,0 +1,106 @@ +import { CaretLeftOutlined, CaretRightOutlined, CloseOutlined } from '@ant-design/icons'; +import { Button } from 'antd'; +import React, { useEffect } from 'react'; +import styled from 'styled-components'; +import { ANTD_GRAY_V2 } from '../../../../../constants'; +import { SchemaField } from '../../../../../../../../types.generated'; +import { pluralize } from '../../../../../../../shared/textUtil'; + +const HeaderWrapper = styled.div` + border-bottom: 1px solid ${ANTD_GRAY_V2[4]}; + display: flex; + justify-content: space-between; + padding: 8px 16px; +`; + +const StyledButton = styled(Button)` + font-size: 12px; + padding: 0; + height: 26px; + width: 26px; + display: flex; + align-items: center; + justify-content: center; + + svg { + height: 10px; + width: 10px; + } +`; + +const FieldIndexText = styled.span` + font-size: 14px; + color: ${ANTD_GRAY_V2[8]}; + margin: 0 8px; +`; + +const ButtonsWrapper = styled.div` + display: flex; + align-items: center; +`; + +interface Props { + schemaFields?: SchemaField[]; + expandedFieldIndex?: number; + setExpandedDrawerFieldPath: (fieldPath: string | null) => void; +} + +export default function DrawerHeader({ schemaFields = [], expandedFieldIndex = 0, setExpandedDrawerFieldPath }: Props) { + function showNextField() { + if (expandedFieldIndex !== undefined && expandedFieldIndex !== -1) { + if (expandedFieldIndex === schemaFields.length - 1) { + const newField = schemaFields[0]; + setExpandedDrawerFieldPath(newField.fieldPath); + } else { + const newField = schemaFields[expandedFieldIndex + 1]; + const { fieldPath } = newField; + setExpandedDrawerFieldPath(fieldPath); + } + } + } + + function showPreviousField() { + if (expandedFieldIndex !== undefined && expandedFieldIndex !== -1) { + if (expandedFieldIndex === 0) { + const newField = schemaFields[schemaFields.length - 1]; + setExpandedDrawerFieldPath(newField.fieldPath); + } else { + const newField = schemaFields[expandedFieldIndex - 1]; + setExpandedDrawerFieldPath(newField.fieldPath); + } + } + } + + function handleArrowKeys(event: KeyboardEvent) { + if (event.code === 'ArrowUp' || event.code === 'ArrowLeft') { + showPreviousField(); + } else if (event.code === 'ArrowDown' || event.code === 'ArrowRight') { + showNextField(); + } + } + + useEffect(() => { + document.addEventListener('keydown', handleArrowKeys); + + return () => document.removeEventListener('keydown', handleArrowKeys); + }); + + return ( + + + + + + + {expandedFieldIndex + 1} of {schemaFields.length} {pluralize(schemaFields.length, 'field')} + + + + + + setExpandedDrawerFieldPath(null)}> + + + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldDescription.tsx new file mode 100644 index 0000000000000..410d2801d51c8 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldDescription.tsx @@ -0,0 +1,115 @@ +import { EditOutlined } from '@ant-design/icons'; +import { Button, message } from 'antd'; +import DOMPurify from 'dompurify'; +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { SectionHeader, StyledDivider } from './components'; +import UpdateDescriptionModal from '../../../../../components/legacy/DescriptionModal'; +import { EditableSchemaFieldInfo, SchemaField, SubResourceType } from '../../../../../../../../types.generated'; +import DescriptionSection from '../../../../../containers/profile/sidebar/AboutSection/DescriptionSection'; +import { useEntityData, useMutationUrn, useRefetch } from '../../../../../EntityContext'; +import { useSchemaRefetch } from '../../SchemaContext'; +import { useUpdateDescriptionMutation } from '../../../../../../../../graphql/mutations.generated'; +import analytics, { EntityActionType, EventType } from '../../../../../../../analytics'; +import SchemaEditableContext from '../../../../../../../shared/SchemaEditableContext'; + +const DescriptionWrapper = styled.div` + display: flex; + justify-content: space-between; +`; + +const EditIcon = styled(Button)` + border: none; + box-shadow: none; + height: 20px; + width: 20px; +`; + +interface Props { + expandedField: SchemaField; + editableFieldInfo?: EditableSchemaFieldInfo; +} + +export default function FieldDescription({ expandedField, editableFieldInfo }: Props) { + const isSchemaEditable = React.useContext(SchemaEditableContext); + const urn = useMutationUrn(); + const refetch = useRefetch(); + const schemaRefetch = useSchemaRefetch(); + const [updateDescription] = useUpdateDescriptionMutation(); + const [isModalVisible, setIsModalVisible] = useState(false); + const { entityType } = useEntityData(); + + const sendAnalytics = () => { + analytics.event({ + type: EventType.EntityActionEvent, + actionType: EntityActionType.UpdateSchemaDescription, + entityType, + entityUrn: urn, + }); + }; + + const refresh: any = () => { + refetch?.(); + schemaRefetch?.(); + }; + + const onSuccessfulMutation = () => { + refresh(); + sendAnalytics(); + message.destroy(); + message.success({ content: 'Updated!', duration: 2 }); + }; + + const onFailMutation = (e) => { + message.destroy(); + if (e instanceof Error) message.error({ content: `Proposal Failed! \n ${e.message || ''}`, duration: 2 }); + }; + + const generateMutationVariables = (updatedDescription: string) => ({ + variables: { + input: { + description: DOMPurify.sanitize(updatedDescription), + resourceUrn: urn, + subResource: expandedField.fieldPath, + subResourceType: SubResourceType.DatasetField, + }, + }, + }); + + const displayedDescription = editableFieldInfo?.description || expandedField.description; + + return ( + <> + +
+ Description + +
+ {isSchemaEditable && ( + setIsModalVisible(true)} + icon={} + /> + )} + {isModalVisible && ( + setIsModalVisible(false)} + onSubmit={(updatedDescription: string) => { + message.loading({ content: 'Updating...' }); + updateDescription(generateMutationVariables(updatedDescription)) + .then(onSuccessfulMutation) + .catch(onFailMutation); + setIsModalVisible(false); + }} + isAddDesc={!displayedDescription} + /> + )} +
+ + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldHeader.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldHeader.tsx new file mode 100644 index 0000000000000..7b06ff43393ef --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldHeader.tsx @@ -0,0 +1,60 @@ +import { Typography } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; +import translateFieldPath from '../../../../../../dataset/profile/schema/utils/translateFieldPath'; +import TypeLabel from '../TypeLabel'; +import PrimaryKeyLabel from '../PrimaryKeyLabel'; +import PartitioningKeyLabel from '../PartitioningKeyLabel'; +import NullableLabel from '../NullableLabel'; +import MenuColumn from '../MenuColumn'; +import { ANTD_GRAY_V2 } from '../../../../../constants'; +import { SchemaField } from '../../../../../../../../types.generated'; + +const FieldHeaderWrapper = styled.div` + padding: 16px; + display: flex; + justify-content: space-between; + border-bottom: 1px solid ${ANTD_GRAY_V2[4]}; +`; + +const FieldName = styled(Typography.Text)` + font-size: 16px; + font-family: 'Roboto Mono', monospace; +`; + +const TypesSection = styled.div` + margin-left: -4px; + margin-top: 8px; +`; + +const NameTypesWrapper = styled.div` + overflow: hidden; +`; + +const MenuWrapper = styled.div` + margin-right: 5px; +`; + +interface Props { + expandedField: SchemaField; +} + +export default function FieldHeader({ expandedField }: Props) { + const displayName = translateFieldPath(expandedField.fieldPath || ''); + return ( + + + {displayName} + + + {expandedField.isPartOfKey && } + {expandedField.isPartitioningKey && } + {expandedField.nullable && } + + + + + + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldProperties.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldProperties.tsx new file mode 100644 index 0000000000000..8c88cdce95f06 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldProperties.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import styled from 'styled-components'; +import { SchemaField, StdDataType } from '../../../../../../../../types.generated'; +import { SectionHeader, StyledDivider } from './components'; +import { mapStructuredPropertyValues } from '../../../../Properties/useStructuredProperties'; +import StructuredPropertyValue from '../../../../Properties/StructuredPropertyValue'; + +const PropertyTitle = styled.div` + font-size: 14px; + font-weight: 700; + margin-bottom: 4px; +`; + +const PropertyWrapper = styled.div` + margin-bottom: 12px; +`; + +const PropertiesWrapper = styled.div` + padding-left: 16px; +`; + +const StyledList = styled.ul` + padding-left: 24px; +`; + +interface Props { + expandedField: SchemaField; +} + +export default function FieldProperties({ expandedField }: Props) { + const { schemaFieldEntity } = expandedField; + + if (!schemaFieldEntity?.structuredProperties?.properties?.length) return null; + + return ( + <> + Properties + + {schemaFieldEntity.structuredProperties.properties.map((structuredProp) => { + const isRichText = + structuredProp.structuredProperty.definition.valueType?.info.type === StdDataType.RichText; + const valuesData = mapStructuredPropertyValues(structuredProp); + const hasMultipleValues = valuesData.length > 1; + + return ( + + {structuredProp.structuredProperty.definition.displayName} + {hasMultipleValues ? ( + + {valuesData.map((value) => ( +
  • + +
  • + ))} +
    + ) : ( + <> + {valuesData.map((value) => ( + + ))} + + )} +
    + ); + })} +
    + + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldTags.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldTags.tsx new file mode 100644 index 0000000000000..c071506d3ad79 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldTags.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { EditableSchemaMetadata, GlobalTags, SchemaField } from '../../../../../../../../types.generated'; +import useTagsAndTermsRenderer from '../../utils/useTagsAndTermsRenderer'; +import { SectionHeader, StyledDivider } from './components'; +import SchemaEditableContext from '../../../../../../../shared/SchemaEditableContext'; + +interface Props { + expandedField: SchemaField; + editableSchemaMetadata?: EditableSchemaMetadata | null; +} + +export default function FieldTags({ expandedField, editableSchemaMetadata }: Props) { + const isSchemaEditable = React.useContext(SchemaEditableContext); + const tagRenderer = useTagsAndTermsRenderer( + editableSchemaMetadata, + { + showTags: true, + showTerms: false, + }, + '', + isSchemaEditable, + ); + + return ( + <> + Tags +
    + {tagRenderer(expandedField.globalTags as GlobalTags, expandedField)} +
    + + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldTerms.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldTerms.tsx new file mode 100644 index 0000000000000..94349836539a6 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldTerms.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { EditableSchemaMetadata, GlobalTags, SchemaField } from '../../../../../../../../types.generated'; +import useTagsAndTermsRenderer from '../../utils/useTagsAndTermsRenderer'; +import { SectionHeader, StyledDivider } from './components'; +import SchemaEditableContext from '../../../../../../../shared/SchemaEditableContext'; + +interface Props { + expandedField: SchemaField; + editableSchemaMetadata?: EditableSchemaMetadata | null; +} + +export default function FieldTerms({ expandedField, editableSchemaMetadata }: Props) { + const isSchemaEditable = React.useContext(SchemaEditableContext); + const termRenderer = useTagsAndTermsRenderer( + editableSchemaMetadata, + { + showTags: false, + showTerms: true, + }, + '', + isSchemaEditable, + ); + + return ( + <> + Glossary Terms + {/* pass in globalTags since this is a shared component, tags will not be shown or used */} +
    + {termRenderer(expandedField.globalTags as GlobalTags, expandedField)} +
    + + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldUsageStats.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldUsageStats.tsx new file mode 100644 index 0000000000000..2f7288904b2df --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldUsageStats.tsx @@ -0,0 +1,59 @@ +import React, { useMemo } from 'react'; +import styled from 'styled-components'; +import { GetDatasetQuery } from '../../../../../../../../graphql/dataset.generated'; +import { useBaseEntity } from '../../../../../EntityContext'; +import { ANTD_GRAY_V2 } from '../../../../../constants'; +import { SectionHeader, StyledDivider } from './components'; +import { pathMatchesNewPath } from '../../../../../../dataset/profile/schema/utils/utils'; +import { UsageBar } from '../../utils/useUsageStatsRenderer'; +import { SchemaField } from '../../../../../../../../types.generated'; + +const USAGE_BAR_MAX_WIDTH = 100; + +const UsageBarWrapper = styled.div` + display: flex; + align-items: center; +`; + +const UsageBarBackground = styled.div` + background-color: ${ANTD_GRAY_V2[3]}; + border-radius: 2px; + height: 4px; + width: ${USAGE_BAR_MAX_WIDTH}px; +`; + +const UsageTextWrapper = styled.span` + margin-left: 8px; +`; + +interface Props { + expandedField: SchemaField; +} + +export default function FieldUsageStats({ expandedField }: Props) { + const baseEntity = useBaseEntity(); + const usageStats = baseEntity?.dataset?.usageStats; + const hasUsageStats = useMemo(() => (usageStats?.aggregations?.fields?.length || 0) > 0, [usageStats]); + const maxFieldUsageCount = useMemo( + () => Math.max(...(usageStats?.aggregations?.fields?.map((field) => field?.count || 0) || [])), + [usageStats], + ); + const relevantUsageStats = usageStats?.aggregations?.fields?.find((fieldStats) => + pathMatchesNewPath(fieldStats?.fieldName, expandedField.fieldPath), + ); + + if (!hasUsageStats || !relevantUsageStats) return null; + + return ( + <> + Usage + + + + + {relevantUsageStats.count || 0} queries / month + + + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx new file mode 100644 index 0000000000000..7a5366f04e983 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx @@ -0,0 +1,83 @@ +import { Drawer } from 'antd'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; +import DrawerHeader from './DrawerHeader'; +import FieldHeader from './FieldHeader'; +import FieldDescription from './FieldDescription'; +import { EditableSchemaMetadata, SchemaField } from '../../../../../../../../types.generated'; +import { pathMatchesNewPath } from '../../../../../../dataset/profile/schema/utils/utils'; +import FieldUsageStats from './FieldUsageStats'; +import FieldTags from './FieldTags'; +import FieldTerms from './FieldTerms'; +import FieldProperties from './FieldProperties'; + +const StyledDrawer = styled(Drawer)` + position: absolute; + + &&& .ant-drawer-body { + padding: 0; + } + + &&& .ant-drawer-content-wrapper { + border-left: 3px solid ${(props) => props.theme.styles['primary-color']}; + } +`; + +const MetadataSections = styled.div` + padding: 16px 24px; +`; + +interface Props { + schemaFields: SchemaField[]; + editableSchemaMetadata?: EditableSchemaMetadata | null; + expandedDrawerFieldPath: string | null; + setExpandedDrawerFieldPath: (fieldPath: string | null) => void; +} + +export default function SchemaFieldDrawer({ + schemaFields, + editableSchemaMetadata, + expandedDrawerFieldPath, + setExpandedDrawerFieldPath, +}: Props) { + const expandedFieldIndex = useMemo( + () => schemaFields.findIndex((row) => row.fieldPath === expandedDrawerFieldPath), + [expandedDrawerFieldPath, schemaFields], + ); + const expandedField = + expandedFieldIndex !== undefined && expandedFieldIndex !== -1 ? schemaFields[expandedFieldIndex] : undefined; + const editableFieldInfo = editableSchemaMetadata?.editableSchemaFieldInfo.find((candidateEditableFieldInfo) => + pathMatchesNewPath(candidateEditableFieldInfo.fieldPath, expandedField?.fieldPath), + ); + + return ( + setExpandedDrawerFieldPath(null)} + getContainer={() => document.getElementById('entity-profile-sidebar') as HTMLElement} + contentWrapperStyle={{ width: '100%', boxShadow: 'none' }} + mask={false} + maskClosable={false} + placement="right" + closable={false} + > + + {expandedField && ( + <> + + + + + + + + + + )} + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/components.ts b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/components.ts new file mode 100644 index 0000000000000..0348336d649b5 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/components.ts @@ -0,0 +1,12 @@ +import { Divider } from 'antd'; +import styled from 'styled-components'; + +export const SectionHeader = styled.div` + font-size: 16px; + font-weight: 600; + margin-bottom: 8px; +`; + +export const StyledDivider = styled(Divider)` + margin: 12px 0; +`; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useDescriptionRenderer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useDescriptionRenderer.tsx index d80143f4bb82c..5f2b5d23771c0 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useDescriptionRenderer.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useDescriptionRenderer.tsx @@ -48,8 +48,8 @@ export default function useDescriptionRenderer(editableSchemaMetadata: EditableS }, }).then(refresh) } + isReadOnly /> ); }; } -// diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx index a57344e5733b4..207deb31d7ab7 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx @@ -2,15 +2,14 @@ import React from 'react'; import { EditableSchemaMetadata, EntityType, GlobalTags, SchemaField } from '../../../../../../../types.generated'; import TagTermGroup from '../../../../../../shared/tags/TagTermGroup'; import { pathMatchesNewPath } from '../../../../../dataset/profile/schema/utils/utils'; -import { useMutationUrn, useRefetch } from '../../../../EntityContext'; import { useSchemaRefetch } from '../SchemaContext'; +import { useMutationUrn, useRefetch } from '../../../../EntityContext'; export default function useTagsAndTermsRenderer( editableSchemaMetadata: EditableSchemaMetadata | null | undefined, - tagHoveredIndex: string | undefined, - setTagHoveredIndex: (index: string | undefined) => void, options: { showTags: boolean; showTerms: boolean }, filterText: string, + canEdit: boolean, ) { const urn = useMutationUrn(); const refetch = useRefetch(); @@ -27,24 +26,21 @@ export default function useTagsAndTermsRenderer( ); return ( -
    - setTagHoveredIndex(undefined)} - entityUrn={urn} - entityType={EntityType.Dataset} - entitySubresource={record.fieldPath} - highlightText={filterText} - refetch={refresh} - /> -
    + ); }; return tagAndTermRender; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useUsageStatsRenderer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useUsageStatsRenderer.tsx index 393783c4ca787..e6b58eeb376f9 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useUsageStatsRenderer.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useUsageStatsRenderer.tsx @@ -7,7 +7,7 @@ import { pathMatchesNewPath } from '../../../../../dataset/profile/schema/utils/ const USAGE_BAR_MAX_WIDTH = 50; -const UsageBar = styled.div<{ width: number }>` +export const UsageBar = styled.div<{ width: number }>` width: ${(props) => props.width}px; height: 4px; background-color: ${geekblue[3]}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/editor/Editor.tsx b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/editor/Editor.tsx index bd2e410fb30d9..db56c092c8ccd 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/editor/Editor.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/editor/Editor.tsx @@ -40,10 +40,11 @@ type EditorProps = { onChange?: (md: string) => void; className?: string; doNotFocus?: boolean; + dataTestId?: string; }; export const Editor = forwardRef((props: EditorProps, ref) => { - const { content, readOnly, onChange, className } = props; + const { content, readOnly, onChange, className, dataTestId } = props; const { manager, state, getContext } = useRemirror({ extensions: () => [ new BlockquoteExtension(), @@ -98,7 +99,7 @@ export const Editor = forwardRef((props: EditorProps, ref) => { }, [readOnly, content]); return ( - + {!readOnly && ( diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/CardinalityLabel.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/CardinalityLabel.tsx new file mode 100644 index 0000000000000..14d3b2166554a --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/CardinalityLabel.tsx @@ -0,0 +1,43 @@ +import { Tooltip } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; +import { PropertyCardinality, StructuredPropertyEntity } from '../../../../../types.generated'; +import { PropertyTypeBadge } from '../Dataset/Schema/components/PropertyTypeLabel'; +import { getStructuredPropertyValue } from '../../utils'; + +const Header = styled.div` + font-size: 10px; +`; + +const List = styled.ul` + padding: 0 24px; + max-height: 500px; + overflow: auto; +`; + +interface Props { + structuredProperty: StructuredPropertyEntity; +} + +export default function CardinalityLabel({ structuredProperty }: Props) { + const labelText = + structuredProperty.definition.cardinality === PropertyCardinality.Single ? 'Single-Select' : 'Multi-Select'; + + return ( + +
    Property Options
    + + {structuredProperty.definition.allowedValues?.map((value) => ( +
  • {getStructuredPropertyValue(value.value)}
  • + ))} +
    + + } + > + +
    + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/NameColumn.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/NameColumn.tsx new file mode 100644 index 0000000000000..3b718c1ec30ed --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/NameColumn.tsx @@ -0,0 +1,87 @@ +import { Tooltip, Typography } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; +import Highlight from 'react-highlighter'; +import { PropertyRow } from './types'; +import ChildCountLabel from '../Dataset/Schema/components/ChildCountLabel'; +import PropertyTypeLabel from '../Dataset/Schema/components/PropertyTypeLabel'; +import StructuredPropertyTooltip from './StructuredPropertyTooltip'; +import CardinalityLabel from './CardinalityLabel'; + +const ParentNameText = styled(Typography.Text)` + color: #373d44; + font-size: 16px; + font-family: Manrope; + font-weight: 600; + line-height: 20px; + word-wrap: break-word; + padding-left: 16px; + display: flex; + align-items: center; +`; + +const ChildNameText = styled(Typography.Text)` + align-self: stretch; + color: #373d44; + font-size: 14px; + font-family: Manrope; + font-weight: 500; + line-height: 18px; + word-wrap: break-word; + padding-left: 16px; + display: flex; + align-items: center; +`; + +const NameLabelWrapper = styled.span` + display: inline-flex; + align-items: center; + flex-wrap: wrap; +`; + +interface Props { + propertyRow: PropertyRow; + filterText?: string; +} + +export default function NameColumn({ propertyRow, filterText }: Props) { + const { structuredProperty } = propertyRow; + return ( + <> + {propertyRow.children ? ( + + + {propertyRow.displayName} + + {propertyRow.childrenCount ? : } + + ) : ( + + + ) : ( + '' + ) + } + > + + {propertyRow.displayName} + + + {propertyRow.type ? ( + + ) : ( + + )} + {structuredProperty?.definition.allowedValues && ( + + )} + + )} + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/PropertiesTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/PropertiesTab.tsx index 277096e1c09cb..01d1145877e3b 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Properties/PropertiesTab.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/PropertiesTab.tsx @@ -1,52 +1,79 @@ -import React from 'react'; -import { Typography } from 'antd'; import styled from 'styled-components'; - -import { ANTD_GRAY } from '../../constants'; -import { StyledTable } from '../../components/styled/StyledTable'; +import React, { useState } from 'react'; +import ExpandIcon from '../Dataset/Schema/components/ExpandIcon'; +import { StyledTable as Table } from '../../components/styled/StyledTable'; import { useEntityData } from '../../EntityContext'; +import { PropertyRow } from './types'; +import useStructuredProperties from './useStructuredProperties'; +import { getFilteredCustomProperties, mapCustomPropertiesToPropertyRows } from './utils'; +import ValuesColumn from './ValuesColumn'; +import NameColumn from './NameColumn'; +import TabHeader from './TabHeader'; +import useUpdateExpandedRowsFromFilter from './useUpdateExpandedRowsFromFilter'; +import { useEntityRegistry } from '../../../../useEntityRegistry'; -const NameText = styled(Typography.Text)` - font-family: 'Roboto Mono', monospace; - font-weight: 600; - font-size: 12px; - color: ${ANTD_GRAY[9]}; -`; - -const ValueText = styled(Typography.Text)` - font-family: 'Roboto Mono', monospace; - font-weight: 400; - font-size: 12px; - color: ${ANTD_GRAY[8]}; -`; +const StyledTable = styled(Table)` + &&& .ant-table-cell-with-append { + padding: 4px; + } +` as typeof Table; export const PropertiesTab = () => { + const [filterText, setFilterText] = useState(''); const { entityData } = useEntityData(); + const entityRegistry = useEntityRegistry(); const propertyTableColumns = [ { - width: 210, + width: '40%', title: 'Name', - dataIndex: 'key', - sorter: (a, b) => a?.key.localeCompare(b?.key || '') || 0, defaultSortOrder: 'ascend', - render: (name: string) => {name}, + render: (propertyRow: PropertyRow) => , }, { title: 'Value', - dataIndex: 'value', - render: (value: string) => {value}, + render: (propertyRow: PropertyRow) => , }, ]; + const { structuredPropertyRows, expandedRowsFromFilter } = useStructuredProperties(entityRegistry, filterText); + const customProperties = getFilteredCustomProperties(filterText, entityData) || []; + const customPropertyRows = mapCustomPropertiesToPropertyRows(customProperties); + const dataSource: PropertyRow[] = structuredPropertyRows.concat(customPropertyRows); + + const [expandedRows, setExpandedRows] = useState>(new Set()); + + useUpdateExpandedRowsFromFilter({ expandedRowsFromFilter, setExpandedRows }); + return ( - + <> + + { + if (expanded) { + setExpandedRows((previousRows) => new Set(previousRows.add(record.qualifiedName))); + } else { + setExpandedRows((previousRows) => { + previousRows.delete(record.qualifiedName); + return new Set(previousRows); + }); + } + }, + indentSize: 0, + }} + /> + ); }; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/StructuredPropertyTooltip.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/StructuredPropertyTooltip.tsx new file mode 100644 index 0000000000000..be0f443ce01b2 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/StructuredPropertyTooltip.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import styled from 'styled-components'; +import { StructuredPropertyEntity } from '../../../../../types.generated'; + +const ContentWrapper = styled.div` + font-size: 12px; +`; + +const Header = styled.div` + font-size: 10px; +`; + +const Description = styled.div` + padding-left: 16px; +`; + +interface Props { + structuredProperty: StructuredPropertyEntity; +} + +export default function StructuredPropertyTooltip({ structuredProperty }: Props) { + return ( + +
    Structured Property
    +
    {structuredProperty.definition.displayName || structuredProperty.definition.qualifiedName}
    + {structuredProperty.definition.description && ( + {structuredProperty.definition.description} + )} +
    + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/StructuredPropertyValue.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/StructuredPropertyValue.tsx new file mode 100644 index 0000000000000..a8b4e6607b25e --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/StructuredPropertyValue.tsx @@ -0,0 +1,69 @@ +import Icon from '@ant-design/icons/lib/components/Icon'; +import React from 'react'; +import Highlight from 'react-highlighter'; +import { Typography } from 'antd'; +import styled from 'styled-components'; +import { ValueColumnData } from './types'; +import { ANTD_GRAY } from '../../constants'; +import { useEntityRegistry } from '../../../../useEntityRegistry'; +import ExternalLink from '../../../../../images/link-out.svg?react'; +import MarkdownViewer, { MarkdownView } from '../../components/legacy/MarkdownViewer'; +import EntityIcon from '../../components/styled/EntityIcon'; + +const ValueText = styled(Typography.Text)` + font-family: 'Manrope'; + font-weight: 400; + font-size: 14px; + color: ${ANTD_GRAY[9]}; + display: block; + + ${MarkdownView} { + font-size: 14px; + } +`; + +const StyledIcon = styled(Icon)` + margin-left: 6px; +`; + +const IconWrapper = styled.span` + margin-right: 4px; +`; + +interface Props { + value: ValueColumnData; + isRichText?: boolean; + filterText?: string; +} + +export default function StructuredPropertyValue({ value, isRichText, filterText }: Props) { + const entityRegistry = useEntityRegistry(); + + return ( + + {value.entity ? ( + <> + + + + {entityRegistry.getDisplayName(value.entity.type, value.entity)} + + + + + ) : ( + <> + {isRichText ? ( + + ) : ( + {value.value?.toString()} + )} + + )} + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/TabHeader.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/TabHeader.tsx new file mode 100644 index 0000000000000..9e0b4992d9c78 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/TabHeader.tsx @@ -0,0 +1,32 @@ +import { SearchOutlined } from '@ant-design/icons'; +import { Input } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; +import { ANTD_GRAY } from '../../constants'; + +const StyledInput = styled(Input)` + border-radius: 70px; + max-width: 300px; +`; + +const TableHeader = styled.div` + padding: 8px 16px; + border-bottom: 1px solid ${ANTD_GRAY[4.5]}; +`; + +interface Props { + setFilterText: (text: string) => void; +} + +export default function TabHeader({ setFilterText }: Props) { + return ( + + setFilterText(e.target.value)} + allowClear + prefix={} + /> + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/ValuesColumn.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/ValuesColumn.tsx new file mode 100644 index 0000000000000..b050e06f96de8 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/ValuesColumn.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { PropertyRow } from './types'; +import { StdDataType } from '../../../../../types.generated'; +import StructuredPropertyValue from './StructuredPropertyValue'; + +interface Props { + propertyRow: PropertyRow; + filterText?: string; +} + +export default function ValuesColumn({ propertyRow, filterText }: Props) { + const { values } = propertyRow; + const isRichText = propertyRow.dataType?.info.type === StdDataType.RichText; + + return ( + <> + {values ? ( + values.map((v) => ) + ) : ( + + )} + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/__tests__/utils.test.ts b/datahub-web-react/src/app/entity/shared/tabs/Properties/__tests__/utils.test.ts new file mode 100644 index 0000000000000..512510732d716 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/__tests__/utils.test.ts @@ -0,0 +1,87 @@ +import { getTestEntityRegistry } from '../../../../../../utils/test-utils/TestPageContainer'; +import { PropertyRow } from '../types'; +import { filterStructuredProperties } from '../utils'; + +describe('filterSchemaRows', () => { + const testEntityRegistry = getTestEntityRegistry(); + const rows = [ + { + displayName: 'Has PII', + qualifiedName: 'io.acryl.ads.data_protection.has_pii', + values: [{ value: 'yes', entity: null }], + }, + { + displayName: 'Discovery Date Utc', + qualifiedName: 'io.acryl.ads.change_management.discovery_date_utc', + values: [{ value: '2023-10-31', entity: null }], + }, + { + displayName: 'Link Data Location', + qualifiedName: 'io.acryl.ads.context.data_location', + values: [{ value: 'New York City', entity: null }], + }, + { + displayName: 'Number Prop', + qualifiedName: 'io.acryl.ads.number', + values: [{ value: 100, entity: null }], + }, + ] as PropertyRow[]; + + it('should properly filter structured properties based on field name', () => { + const filterText = 'has pi'; + const { filteredRows, expandedRowsFromFilter } = filterStructuredProperties( + testEntityRegistry, + rows, + filterText, + ); + + expect(filteredRows).toMatchObject([ + { + displayName: 'Has PII', + qualifiedName: 'io.acryl.ads.data_protection.has_pii', + values: [{ value: 'yes', entity: null }], + }, + ]); + expect(expandedRowsFromFilter).toMatchObject( + new Set(['io', 'io.acryl', 'io.acryl.ads', 'io.acryl.ads.data_protection']), + ); + }); + + it('should properly filter structured properties based on field value', () => { + const filterText = 'new york'; + const { filteredRows, expandedRowsFromFilter } = filterStructuredProperties( + testEntityRegistry, + rows, + filterText, + ); + + expect(filteredRows).toMatchObject([ + { + displayName: 'Link Data Location', + qualifiedName: 'io.acryl.ads.context.data_location', + values: [{ value: 'New York City', entity: null }], + }, + ]); + expect(expandedRowsFromFilter).toMatchObject( + new Set(['io', 'io.acryl', 'io.acryl.ads', 'io.acryl.ads.context']), + ); + }); + + it('should properly filter structured properties based on field value even for numbers', () => { + const filterText = '100'; + const { filteredRows, expandedRowsFromFilter } = filterStructuredProperties( + testEntityRegistry, + rows, + filterText, + ); + + expect(filteredRows).toMatchObject([ + { + displayName: 'Number Prop', + qualifiedName: 'io.acryl.ads.number', + values: [{ value: 100, entity: null }], + }, + ]); + expect(expandedRowsFromFilter).toMatchObject(new Set(['io', 'io.acryl', 'io.acryl.ads'])); + }); +}); diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/types.ts b/datahub-web-react/src/app/entity/shared/tabs/Properties/types.ts new file mode 100644 index 0000000000000..b93ba886d5a64 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/types.ts @@ -0,0 +1,25 @@ +import { DataTypeEntity, Entity, StructuredPropertyEntity } from '../../../../../types.generated'; + +export interface ValueColumnData { + value: string | number | null; + entity: Entity | null; +} + +export interface TypeData { + type: string; + nativeDataType: string; +} + +export interface PropertyRow { + displayName: string; + qualifiedName: string; + values?: ValueColumnData[]; + children?: PropertyRow[]; + childrenCount?: number; + parent?: PropertyRow; + depth?: number; + type?: TypeData; + dataType?: DataTypeEntity; + isParentRow?: boolean; + structuredProperty?: StructuredPropertyEntity; +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/useStructuredProperties.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/useStructuredProperties.tsx new file mode 100644 index 0000000000000..5600d7c3e8498 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/useStructuredProperties.tsx @@ -0,0 +1,215 @@ +import { PropertyValue, StructuredPropertiesEntry } from '../../../../../types.generated'; +import EntityRegistry from '../../../EntityRegistry'; +import { useEntityData } from '../../EntityContext'; +import { GenericEntityProperties } from '../../types'; +import { getStructuredPropertyValue } from '../../utils'; +import { PropertyRow } from './types'; +import { filterStructuredProperties } from './utils'; + +const typeNameToType = { + StringValue: { type: 'string', nativeDataType: 'text' }, + NumberValue: { type: 'number', nativeDataType: 'float' }, +}; + +export function mapStructuredPropertyValues(structuredPropertiesEntry: StructuredPropertiesEntry) { + return structuredPropertiesEntry.values + .filter((value) => !!value) + .map((value) => ({ + value: getStructuredPropertyValue(value as PropertyValue), + entity: + structuredPropertiesEntry.valueEntities?.find( + (entity) => entity?.urn === getStructuredPropertyValue(value as PropertyValue), + ) || null, + })); +} + +// map the properties map into a list of PropertyRow objects to render in a table +function getStructuredPropertyRows(entityData?: GenericEntityProperties | null) { + const structuredPropertyRows: PropertyRow[] = []; + + entityData?.structuredProperties?.properties?.forEach((structuredPropertiesEntry) => { + const { displayName, qualifiedName } = structuredPropertiesEntry.structuredProperty.definition; + structuredPropertyRows.push({ + displayName: displayName || qualifiedName, + qualifiedName, + values: mapStructuredPropertyValues(structuredPropertiesEntry), + dataType: structuredPropertiesEntry.structuredProperty.definition.valueType, + structuredProperty: structuredPropertiesEntry.structuredProperty, + type: + structuredPropertiesEntry.values[0] && structuredPropertiesEntry.values[0].__typename + ? { + type: typeNameToType[structuredPropertiesEntry.values[0].__typename].type, + nativeDataType: typeNameToType[structuredPropertiesEntry.values[0].__typename].nativeDataType, + } + : undefined, + }); + }); + + return structuredPropertyRows; +} + +export function findAllSubstrings(s: string): Array { + const substrings: Array = []; + + for (let i = 0; i < s.length; i++) { + if (s[i] === '.') { + substrings.push(s.substring(0, i)); + } + } + substrings.push(s); + return substrings; +} + +export function createParentPropertyRow(displayName: string, qualifiedName: string): PropertyRow { + return { + displayName, + qualifiedName, + isParentRow: true, + }; +} + +export function identifyAndAddParentRows(rows?: Array): Array { + /** + * This function takes in an array of PropertyRow objects and determines which rows are parents. These parents need + * to be extracted in order to organize the rows into a properly nested structure later on. The final product returned + * is a list of parent rows, without values or children assigned. + */ + const qualifiedNames: Array = []; + + // Get list of fqns + if (rows) { + rows.forEach((row) => { + qualifiedNames.push(row.qualifiedName); + }); + } + + const finalParents: PropertyRow[] = []; + const finalParentNames = new Set(); + + // Loop through list of fqns and find all substrings. + // e.g. a.b.c.d becomes a, a.b, a.b.c, a.b.c.d + qualifiedNames.forEach((fqn) => { + let previousCount: number | null = null; + let previousParentName = ''; + + const substrings = findAllSubstrings(fqn); + + // Loop through substrings and count how many other fqns have that substring in them. Use this to determine + // if a property should be nested. If the count is equal then we should not nest, because there's no split + // that would tell us to nest. If the count is not equal, we should nest the child properties. + for (let index = 0; index < substrings.length; index++) { + const token = substrings[index]; + const currentCount = qualifiedNames.filter((name) => name.startsWith(token)).length; + + // If we're at the beginning of the path and there is no nesting, break + if (index === 0 && currentCount === 1) { + break; + } + + // Add previous fqn, or,previousParentName, if we have found a viable parent path + if (previousCount !== null && previousCount !== currentCount) { + if (!finalParentNames.has(previousParentName)) { + const parent: PropertyRow = createParentPropertyRow(previousParentName, previousParentName); + parent.childrenCount = previousCount; + finalParentNames.add(previousParentName); + finalParents.push(parent); + } + } + + previousCount = currentCount; + previousParentName = token; + } + }); + + return finalParents; +} + +export function groupByParentProperty(rows?: Array): Array { + /** + * This function takes in an array of PropertyRow objects, representing parent and child properties. Parent properties + * will not have values, but child properties will. It organizes the rows into the parent and child structure and + * returns a list of PropertyRow objects representing it. + */ + const outputRows: Array = []; + const outputRowByPath = {}; + + if (rows) { + // Iterate through all rows + for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { + let parentRow: null | PropertyRow = null; + const row = { children: undefined, ...rows[rowIndex], depth: 0 }; + + // Iterate through a row's characters, and split the row's path into tokens + // e.g. a, b, c for the example a.b.c + for (let j = rowIndex - 1; j >= 0; j--) { + const rowTokens = row.qualifiedName.split('.'); + let parentPath: null | string = null; + let previousParentPath = rowTokens.slice(0, rowTokens.length - 1).join('.'); + + // Iterate through a row's path backwards, and check if the previous row's path has been seen. If it has, + // populate parentRow. If not, move on to the next path token. + // e.g. for a.b.c.d, first evaluate a.b.c to see if it has been seen. If it hasn't, move to a.b + for ( + let lastParentTokenIndex = rowTokens.length - 2; + lastParentTokenIndex >= 0; + --lastParentTokenIndex + ) { + const lastParentToken: string = rowTokens[lastParentTokenIndex]; + if (lastParentToken && Object.keys(outputRowByPath).includes(previousParentPath)) { + parentPath = rowTokens.slice(0, lastParentTokenIndex + 1).join('.'); + break; + } + previousParentPath = rowTokens.slice(0, lastParentTokenIndex).join('.'); + } + + if (parentPath && rows[j].qualifiedName === parentPath) { + parentRow = outputRowByPath[rows[j].qualifiedName]; + break; + } + } + + // If the parent row exists in the ouput, add the current row as a child. If not, add the current row + // to the final output. + if (parentRow) { + row.depth = (parentRow.depth || 0) + 1; + row.parent = parentRow; + if (row.isParentRow) { + row.displayName = row.displayName.replace(`${parentRow.displayName}.`, ''); + } + parentRow.children = [...(parentRow.children || []), row]; + } else { + outputRows.push(row); + } + outputRowByPath[row.qualifiedName] = row; + } + } + return outputRows; +} + +export default function useStructuredProperties(entityRegistry: EntityRegistry, filterText?: string) { + const { entityData } = useEntityData(); + + let structuredPropertyRowsRaw = getStructuredPropertyRows(entityData); + const parentRows = identifyAndAddParentRows(structuredPropertyRowsRaw); + + structuredPropertyRowsRaw = [...structuredPropertyRowsRaw, ...parentRows]; + + const { filteredRows, expandedRowsFromFilter } = filterStructuredProperties( + entityRegistry, + structuredPropertyRowsRaw, + filterText, + ); + + // Sort by fqn before nesting algorithm + const copy = [...filteredRows].sort((a, b) => { + return a.qualifiedName.localeCompare(b.qualifiedName); + }); + + // group properties by path + const structuredPropertyRows = groupByParentProperty(copy); + + return { + structuredPropertyRows, + expandedRowsFromFilter: expandedRowsFromFilter as Set, + }; +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/useUpdateExpandedRowsFromFilter.ts b/datahub-web-react/src/app/entity/shared/tabs/Properties/useUpdateExpandedRowsFromFilter.ts new file mode 100644 index 0000000000000..0dbe762c537db --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/useUpdateExpandedRowsFromFilter.ts @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; +import { isEqual } from 'lodash'; +import usePrevious from '../../../../shared/usePrevious'; + +interface Props { + expandedRowsFromFilter: Set; + setExpandedRows: React.Dispatch>>; +} + +export default function useUpdateExpandedRowsFromFilter({ expandedRowsFromFilter, setExpandedRows }: Props) { + const previousExpandedRowsFromFilter = usePrevious(expandedRowsFromFilter); + + useEffect(() => { + if (!isEqual(expandedRowsFromFilter, previousExpandedRowsFromFilter)) { + setExpandedRows((previousRows) => { + const finalRowsSet = new Set(); + expandedRowsFromFilter.forEach((row) => finalRowsSet.add(row)); + previousRows.forEach((row) => finalRowsSet.add(row)); + return finalRowsSet as Set; + }); + } + }, [expandedRowsFromFilter, previousExpandedRowsFromFilter, setExpandedRows]); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/utils.ts b/datahub-web-react/src/app/entity/shared/tabs/Properties/utils.ts new file mode 100644 index 0000000000000..91870e2e37e07 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/utils.ts @@ -0,0 +1,68 @@ +import { CustomPropertiesEntry } from '../../../../../types.generated'; +import EntityRegistry from '../../../EntityRegistry'; +import { GenericEntityProperties } from '../../types'; +import { PropertyRow, ValueColumnData } from './types'; + +export function mapCustomPropertiesToPropertyRows(customProperties: CustomPropertiesEntry[]) { + return (customProperties?.map((customProp) => ({ + displayName: customProp.key, + values: [{ value: customProp.value || '' }], + type: { type: 'string', nativeDataType: 'string' }, + })) || []) as PropertyRow[]; +} + +function matchesName(name: string, filterText: string) { + return name.toLocaleLowerCase().includes(filterText.toLocaleLowerCase()); +} + +function matchesAnyFromValues(values: ValueColumnData[], filterText: string, entityRegistry: EntityRegistry) { + return values.some( + (value) => + matchesName(value.value?.toString() || '', filterText) || + matchesName(value.entity ? entityRegistry.getDisplayName(value.entity.type, value.entity) : '', filterText), + ); +} + +export function getFilteredCustomProperties(filterText: string, entityData?: GenericEntityProperties | null) { + return entityData?.customProperties?.filter( + (property) => matchesName(property.key, filterText) || matchesName(property.value || '', filterText), + ); +} + +export function filterStructuredProperties( + entityRegistry: EntityRegistry, + propertyRows: PropertyRow[], + filterText?: string, +) { + if (!propertyRows) return { filteredRows: [], expandedRowsFromFilter: new Set() }; + if (!filterText) return { filteredRows: propertyRows, expandedRowsFromFilter: new Set() }; + const formattedFilterText = filterText.toLocaleLowerCase(); + + const finalQualifiedNames = new Set(); + const expandedRowsFromFilter = new Set(); + + propertyRows.forEach((row) => { + // if we match on the qualified name (maybe from a parent) do not filter out + if (matchesName(row.qualifiedName, formattedFilterText)) { + finalQualifiedNames.add(row.qualifiedName); + } + // if we match specifically on this property (not just its parent), add and expand all parents + if ( + matchesName(row.displayName, formattedFilterText) || + matchesAnyFromValues(row.values || [], formattedFilterText, entityRegistry) + ) { + finalQualifiedNames.add(row.qualifiedName); + + const splitFieldPath = row.qualifiedName.split('.'); + splitFieldPath.reduce((previous, current) => { + finalQualifiedNames.add(previous); + expandedRowsFromFilter.add(previous); + return `${previous}.${current}`; + }); + } + }); + + const filteredRows = propertyRows.filter((row) => finalQualifiedNames.has(row.qualifiedName)); + + return { filteredRows, expandedRowsFromFilter }; +} diff --git a/datahub-web-react/src/app/entity/shared/types.ts b/datahub-web-react/src/app/entity/shared/types.ts index d4e3965cd66f5..47cad4a69096d 100644 --- a/datahub-web-react/src/app/entity/shared/types.ts +++ b/datahub-web-react/src/app/entity/shared/types.ts @@ -38,6 +38,7 @@ import { BrowsePathV2, DataJobInputOutput, ParentDomainsResult, + StructuredProperties, } from '../../../types.generated'; import { FetchedEntity } from '../../lineage/types'; @@ -84,6 +85,7 @@ export type GenericEntityProperties = { platform?: Maybe; dataPlatformInstance?: Maybe; customProperties?: Maybe; + structuredProperties?: Maybe; institutionalMemory?: Maybe; schemaMetadata?: Maybe; externalUrl?: Maybe; diff --git a/datahub-web-react/src/app/entity/shared/utils.ts b/datahub-web-react/src/app/entity/shared/utils.ts index a158cc9b7c119..217aaaaf9dde8 100644 --- a/datahub-web-react/src/app/entity/shared/utils.ts +++ b/datahub-web-react/src/app/entity/shared/utils.ts @@ -1,6 +1,6 @@ import { Maybe } from 'graphql/jsutils/Maybe'; -import { Entity, EntityType, EntityRelationshipsResult, DataProduct } from '../../../types.generated'; +import { Entity, EntityType, EntityRelationshipsResult, DataProduct, PropertyValue } from '../../../types.generated'; import { capitalizeFirstLetterOnly } from '../../shared/textUtil'; import { GenericEntityProperties } from './types'; @@ -130,3 +130,13 @@ export function getDataProduct(dataProductResult: Maybe { it('logs in', () => { cy.visit('/'); - cy.get('input[data-testid=username]').type(Cypress.env('ADMIN_USERNAME')); - cy.get('input[data-testid=password]').type(Cypress.env('ADMIN_PASSWORD')); + cy.get('input[data-testid=username]').type('datahub'); + cy.get('input[data-testid=password]').type('datahub'); cy.contains('Sign In').click(); cy.contains('Welcome back, ' + Cypress.env('ADMIN_DISPLAYNAME')); }); diff --git a/smoke-test/tests/cypress/cypress/e2e/mutations/edit_documentation.js b/smoke-test/tests/cypress/cypress/e2e/mutations/edit_documentation.js index 5f9758a35ca0e..c6d2b205250e0 100644 --- a/smoke-test/tests/cypress/cypress/e2e/mutations/edit_documentation.js +++ b/smoke-test/tests/cypress/cypress/e2e/mutations/edit_documentation.js @@ -78,17 +78,18 @@ describe("edit documentation and link to dataset", () => { cy.visit( "/dataset/urn:li:dataset:(urn:li:dataPlatform:hive,SampleCypressHiveDataset,PROD)/Schema" ); - cy.get("tbody [data-icon='edit']").first().click({ force: true }); + cy.clickOptionWithText("field_foo"); + cy.clickOptionWithTestId("edit-field-description"); cy.waitTextVisible("Update description"); cy.waitTextVisible("Foo field description has changed"); - cy.focused().clear().wait(1000); + cy.getWithTestId("description-editor").clear().wait(1000); cy.focused().type(documentation_edited); cy.clickOptionWithTestId("description-modal-update-button"); cy.waitTextVisible("Updated!"); cy.waitTextVisible(documentation_edited); cy.waitTextVisible("(edited)"); - cy.get("tbody [data-icon='edit']").first().click({ force: true }); - cy.focused().clear().wait(1000); + cy.clickOptionWithTestId("edit-field-description"); + cy.getWithTestId("description-editor").clear().wait(1000); cy.focused().type("Foo field description has changed"); cy.clickOptionWithTestId("description-modal-update-button"); cy.waitTextVisible("Updated!"); diff --git a/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js b/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js index 1baa33901724f..7f8a4e4f8f335 100644 --- a/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js +++ b/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js @@ -77,7 +77,7 @@ describe("mutations", () => { cy.login(); cy.viewport(2000, 800); cy.goToDataset("urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)", "cypress_logging_events"); - cy.mouseover('[data-testid="schema-field-event_name-tags"]'); + cy.clickOptionWithText("event_name"); cy.get('[data-testid="schema-field-event_name-tags"]').within(() => cy.contains("Add Tag").click() ); @@ -116,7 +116,8 @@ describe("mutations", () => { // verify dataset shows up in search now cy.contains("of 1 result").click(); cy.contains("cypress_logging_events").click(); - cy.get('[data-testid="tag-CypressTestAddTag2"]').within(() => + cy.clickOptionWithText("event_name"); + cy.get('[data-testid="schema-field-event_name-tags"]').within(() => cy .get("span[aria-label=close]") .trigger("mouseover", { force: true }) @@ -134,10 +135,7 @@ describe("mutations", () => { // make space for the glossary term column cy.viewport(2000, 800); cy.goToDataset("urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)", "cypress_logging_events"); - cy.get('[data-testid="schema-field-event_name-terms"]').trigger( - "mouseover", - { force: true } - ); + cy.clickOptionWithText("event_name"); cy.get('[data-testid="schema-field-event_name-terms"]').within(() => cy.contains("Add Term").click({ force: true }) ); @@ -146,9 +144,12 @@ describe("mutations", () => { cy.contains("CypressTerm"); - cy.get( - 'a[href="/glossaryTerm/urn:li:glossaryTerm:CypressNode.CypressTerm"]' - ).within(() => cy.get("span[aria-label=close]").click({ force: true })); + cy.get('[data-testid="schema-field-event_name-terms"]').within(() => + cy + .get("span[aria-label=close]") + .trigger("mouseover", { force: true }) + .click({ force: true }) + ); cy.contains("Yes").click({ force: true }); cy.contains("CypressTerm").should("not.exist"); diff --git a/smoke-test/tests/cypress/cypress/e2e/schema_blame/schema_blame.js b/smoke-test/tests/cypress/cypress/e2e/schema_blame/schema_blame.js index 6e282b5249636..1ce1fbe900172 100644 --- a/smoke-test/tests/cypress/cypress/e2e/schema_blame/schema_blame.js +++ b/smoke-test/tests/cypress/cypress/e2e/schema_blame/schema_blame.js @@ -14,6 +14,7 @@ describe('schema blame', () => { cy.contains('field_bar').should('not.exist'); cy.contains('Foo field description has changed'); cy.contains('Baz field description'); + cy.clickOptionWithText("field_foo"); cy.get('[data-testid="schema-field-field_foo-tags"]').contains('Legacy'); // Make sure the schema blame is accurate @@ -41,6 +42,7 @@ describe('schema blame', () => { cy.contains('field_baz').should('not.exist'); cy.contains('Foo field description'); cy.contains('Bar field description'); + cy.clickOptionWithText("field_foo"); cy.get('[data-testid="schema-field-field_foo-tags"]').contains('Legacy').should('not.exist'); // Make sure the schema blame is accurate diff --git a/smoke-test/tests/cypress/cypress/support/commands.js b/smoke-test/tests/cypress/cypress/support/commands.js index f32512aff45fa..51b06a24c1921 100644 --- a/smoke-test/tests/cypress/cypress/support/commands.js +++ b/smoke-test/tests/cypress/cypress/support/commands.js @@ -218,6 +218,10 @@ Cypress.Commands.add( 'multiSelect', (within_data_id , text) => { cy.clickOptionWithText(text); }); +Cypress.Commands.add("getWithTestId", (id) => { + return cy.get(selectorWithtestId(id)); +}); + Cypress.Commands.add("enterTextInTestId", (id, text) => { cy.get(selectorWithtestId(id)).type(text); }) From caf6ebe3b7a7ebaafd2b5252763171f4dfbeb754 Mon Sep 17 00:00:00 2001 From: John Joyce Date: Thu, 25 Jan 2024 10:40:22 -0800 Subject: [PATCH 09/19] docs(): Updating docs for assertions to correct databricks assertions support (#9713) Co-authored-by: John Joyce --- .../observe/custom-sql-assertions.md | 2 +- .../observe/freshness-assertions.md | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/managed-datahub/observe/custom-sql-assertions.md b/docs/managed-datahub/observe/custom-sql-assertions.md index 11e9aa807b616..581b542688134 100644 --- a/docs/managed-datahub/observe/custom-sql-assertions.md +++ b/docs/managed-datahub/observe/custom-sql-assertions.md @@ -117,7 +117,7 @@ The **Assertion Description**: This is a human-readable description of the Asser ### Prerequisites 1. **Permissions**: To create or delete Custom SQL Assertions for a specific entity on DataHub, you'll need to be granted the - `Edit Assertions` and `Edit Monitors` privileges for the entity. This is granted to Entity owners by default. + `Edit Assertions`, `Edit Monitors`, **and the additional `Edit SQL Assertion Monitors`** privileges for the entity. This is granted to Entity owners by default. 2. **Data Platform Connection**: In order to create a Custom SQL Assertion, you'll need to have an **Ingestion Source** configured to your Data Platform: Snowflake, BigQuery, or Redshift under the **Integrations** tab. diff --git a/docs/managed-datahub/observe/freshness-assertions.md b/docs/managed-datahub/observe/freshness-assertions.md index 416db6a65343e..9704f475b1587 100644 --- a/docs/managed-datahub/observe/freshness-assertions.md +++ b/docs/managed-datahub/observe/freshness-assertions.md @@ -107,12 +107,14 @@ Change Source types vary by the platform, but generally fall into these categori - **Audit Log** (Default): A metadata API or Table that is exposed by the Data Warehouse which contains captures information about the operations that have been performed to each Table. It is usually efficient to check, but some useful operations are not - fully supported across all major Warehouse platforms. + fully supported across all major Warehouse platforms. Note that for Databricks, [this option](https://docs.databricks.com/en/delta/history.html) + is only available for tables stored in Delta format. - **Information Schema**: A system Table that is exposed by the Data Warehouse which contains live information about the Databases and Tables stored inside the Data Warehouse. It is usually efficient to check, but lacks detailed information about the _type_ - of change that was last made to a specific table (e.g. the operation itself - INSERT, UPDATE, DELETE, number of impacted rows, etc) - + of change that was last made to a specific table (e.g. the operation itself - INSERT, UPDATE, DELETE, number of impacted rows, etc). + Note that for Databricks, [this option](https://docs.databricks.com/en/delta/table-details.html) is only available for tables stored in Delta format. + - **Last Modified Column**: A Date or Timestamp column that represents the last time that a specific _row_ was touched or updated. Adding a Last Modified Column to each warehouse Table is a pattern is often used for existing use cases around change management. If this change source is used, a query will be issued to the Table to search for rows that have been modified within a specific @@ -128,8 +130,11 @@ Change Source types vary by the platform, but generally fall into these categori This relies on Operations being reported to DataHub, either via ingestion or via use of the DataHub APIs (see [Report Operation via API](#reporting-operations-via-api)). Note if you have not configured an ingestion source through DataHub, then this may be the only option available. By default, any operation type found will be considered a valid change. Use the **Operation Types** dropdown when selecting this option to specify which operation types should be considered valid changes. You may choose from one of DataHub's standard Operation Types, or specify a "Custom" Operation Type by typing in the name of the Operation Type. - Using either of the column value approaches (**Last Modified Column** or **High Watermark Column**) to determine whether a Table has changed can be useful because it can be customized to determine whether specific types of important changes have been made to a given Table. - Because it does not involve system warehouse tables, it is also easily portable across Data Warehouse and Data Lake providers. + - **File Metadata** (Databricks Only): A column that is exposed by Databricks for both Unity Catalog and Hive Metastore based tables + which includes information about the last time that a file for the table was changed. Read more about it [here](https://docs.databricks.com/en/ingestion/file-metadata-column.html). + + Using either of the column value approaches (**Last Modified Column** or **High Watermark Column**) to determine whether a Table has changed can be useful because it can be customized to determine whether specific types of changes have been made to a given Table. + And because this type of assertion does not involve system warehouse tables, they are easily portable across Data Warehouse and Data Lake providers. Freshness Assertions also have an off switch: they can be started or stopped at any time with the click of button. From d292b35f2340e227a49ad872a156d7b2f15fb9a9 Mon Sep 17 00:00:00 2001 From: david-leifker <114954101+david-leifker@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:41:51 -0600 Subject: [PATCH 10/19] test(spark-lineage): minor tweaks (#9717) --- .github/workflows/spark-smoke-test.yml | 18 +++++++++++++++++- docker/build.gradle | 17 ++++++++++++----- .../datahub/spark/TestSparkJobsLineage.java | 9 +++++++-- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/.github/workflows/spark-smoke-test.yml b/.github/workflows/spark-smoke-test.yml index e463e15243ee3..87fa3c85fc581 100644 --- a/.github/workflows/spark-smoke-test.yml +++ b/.github/workflows/spark-smoke-test.yml @@ -42,8 +42,12 @@ jobs: cache: "pip" - name: Install dependencies run: ./metadata-ingestion/scripts/install_deps.sh + - name: Disk Check + run: df -h . && docker images - name: Remove images run: docker image prune -a -f || true + - name: Disk Check + run: df -h . && docker images - name: Smoke test run: | ./gradlew :metadata-integration:java:spark-lineage:integrationTest \ @@ -54,12 +58,24 @@ jobs: -x :datahub-web-react:yarnBuild \ -x :datahub-web-react:distZip \ -x :datahub-web-react:jar + - name: store logs + if: failure() + run: | + docker ps -a + docker logs datahub-gms >& gms-${{ matrix.test_strategy }}.log || true + docker logs datahub-actions >& actions-${{ matrix.test_strategy }}.log || true + docker logs broker >& broker-${{ matrix.test_strategy }}.log || true + docker logs mysql >& mysql-${{ matrix.test_strategy }}.log || true + docker logs elasticsearch >& elasticsearch-${{ matrix.test_strategy }}.log || true + docker logs datahub-frontend-react >& frontend-${{ matrix.test_strategy }}.log || true - name: Upload logs uses: actions/upload-artifact@v3 if: failure() with: name: docker logs - path: "docker/build/container-logs/*.log" + path: | + "**/build/container-logs/*.log" + "*.log" - uses: actions/upload-artifact@v3 if: always() with: diff --git a/docker/build.gradle b/docker/build.gradle index cc95e12f26f76..b14739104a9f1 100644 --- a/docker/build.gradle +++ b/docker/build.gradle @@ -8,15 +8,17 @@ import com.avast.gradle.dockercompose.tasks.ComposeDownForced apply from: "../gradle/versioning/versioning.gradle" ext { - quickstart_modules = [ + backend_profile_modules = [ ':docker:elasticsearch-setup', ':docker:mysql-setup', ':docker:kafka-setup', ':datahub-upgrade', + ':metadata-service:war', + ] + quickstart_modules = backend_profile_modules + [ ':metadata-jobs:mce-consumer-job', ':metadata-jobs:mae-consumer-job', - ':metadata-service:war', - ':datahub-frontend', + ':datahub-frontend' ] debug_modules = quickstart_modules - [':metadata-jobs:mce-consumer-job', @@ -90,9 +92,14 @@ dockerCompose { removeVolumes = false } + /** + * The smallest disk footprint required for Spark integration tests + * + * No frontend, mae, mce, or other services + */ quickstartSlim { isRequiredBy(tasks.named('quickstartSlim')) - composeAdditionalArgs = ['--profile', 'quickstart-consumers'] + composeAdditionalArgs = ['--profile', 'quickstart-backend'] environment.put 'DATAHUB_VERSION', "v${version}" environment.put "DATAHUB_ACTIONS_IMAGE", "acryldata/datahub-ingestion" @@ -132,7 +139,7 @@ tasks.getByName('quickstartComposeUp').dependsOn( tasks.getByName('quickstartPgComposeUp').dependsOn( pg_quickstart_modules.collect { it + ':dockerTag' }) tasks.getByName('quickstartSlimComposeUp').dependsOn( - ([':docker:datahub-ingestion'] + quickstart_modules) + ([':docker:datahub-ingestion'] + backend_profile_modules) .collect { it + ':dockerTag' }) tasks.getByName('quickstartDebugComposeUp').dependsOn( debug_modules.collect { it + ':dockerTagDebug' } diff --git a/metadata-integration/java/spark-lineage/src/test/java/datahub/spark/TestSparkJobsLineage.java b/metadata-integration/java/spark-lineage/src/test/java/datahub/spark/TestSparkJobsLineage.java index fa896814d16f6..a4eb035b0abce 100644 --- a/metadata-integration/java/spark-lineage/src/test/java/datahub/spark/TestSparkJobsLineage.java +++ b/metadata-integration/java/spark-lineage/src/test/java/datahub/spark/TestSparkJobsLineage.java @@ -136,6 +136,7 @@ public static void resetBaseExpectations() { .respond(HttpResponse.response().withStatusCode(200)); } + @BeforeClass public static void init() { mockServer = startClientAndServer(GMS_PORT); resetBaseExpectations(); @@ -219,8 +220,12 @@ private static void clear() { @AfterClass public static void tearDown() throws Exception { - spark.stop(); - mockServer.stop(); + if (spark != null) { + spark.stop(); + } + if (mockServer != null) { + mockServer.stop(); + } } private static void check(List expected, List actual) { From acec2a7159bc7fcc7ad37a3709b3c68d5d26536e Mon Sep 17 00:00:00 2001 From: RyanHolstien Date: Thu, 25 Jan 2024 13:04:50 -0600 Subject: [PATCH 11/19] feat(search): support filtering on count type searchable fields for equality (#9700) --- .../linkedin/metadata/models/EntitySpec.java | 15 ++ .../metadata/models/EntitySpecBuilder.java | 4 +- .../models/registry/ConfigEntityRegistry.java | 2 +- .../models/registry/MergedEntityRegistry.java | 21 +-- .../models/registry/PatchEntityRegistry.java | 28 ++- .../registry/SnapshotEntityRegistry.java | 2 +- .../models/EntitySpecBuilderTest.java | 16 +- .../elasticsearch/query/ESBrowseDAO.java | 18 +- .../elasticsearch/query/ESSearchDAO.java | 15 +- .../request/AutocompleteRequestHandler.java | 19 +- .../query/request/SearchRequestHandler.java | 23 ++- .../metadata/search/utils/ESUtils.java | 172 +++++++++++++----- .../ElasticSearchTimeseriesAspectService.java | 45 +++-- .../elastic/query/ESAggregatedStatsDAO.java | 4 +- .../search/fixtures/GoldenTestBase.java | 40 +++- .../indexbuilder/MappingsBuilderTest.java | 3 +- .../request/SearchRequestHandlerTest.java | 2 +- .../metadata/search/utils/ESUtilsTest.java | 31 ++-- .../TimeseriesAspectServiceTestBase.java | 59 ++++++ .../test/search/SearchTestUtils.java | 18 ++ .../long_tail/datasetindex_v2.json.gz | Bin 183656 -> 183668 bytes .../com/datahub/test/TestEntityInfo.pdl | 6 + 22 files changed, 430 insertions(+), 113 deletions(-) diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpec.java b/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpec.java index e4c9dd55a3b4a..fac08c7e20646 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpec.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpec.java @@ -3,8 +3,11 @@ import com.linkedin.data.schema.RecordDataSchema; import com.linkedin.data.schema.TyperefDataSchema; import com.linkedin.metadata.models.annotation.EntityAnnotation; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; /** A specification of a DataHub Entity */ @@ -36,6 +39,18 @@ default List getSearchableFieldSpecs() { .collect(Collectors.toList()); } + default Map> getSearchableFieldSpecMap() { + return getSearchableFieldSpecs().stream() + .collect( + Collectors.toMap( + searchableFieldSpec -> searchableFieldSpec.getSearchableAnnotation().getFieldName(), + searchableFieldSpec -> new HashSet<>(Collections.singleton(searchableFieldSpec)), + (set1, set2) -> { + set1.addAll(set2); + return set1; + })); + } + default List getSearchScoreFieldSpecs() { return getAspectSpecs().stream() .map(AspectSpec::getSearchScoreFieldSpecs) diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpecBuilder.java b/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpecBuilder.java index 580134f566871..54f2206798da0 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpecBuilder.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpecBuilder.java @@ -248,9 +248,9 @@ public AspectSpec buildAspectSpec( // Extract SearchScore Field Specs final SearchScoreFieldSpecExtractor searchScoreFieldSpecExtractor = new SearchScoreFieldSpecExtractor(); - final DataSchemaRichContextTraverser searcScoreFieldSpecTraverser = + final DataSchemaRichContextTraverser searchScoreFieldSpecTraverser = new DataSchemaRichContextTraverser(searchScoreFieldSpecExtractor); - searcScoreFieldSpecTraverser.traverse(processedSearchScoreResult.getResultSchema()); + searchScoreFieldSpecTraverser.traverse(processedSearchScoreResult.getResultSchema()); final SchemaAnnotationProcessor.SchemaAnnotationProcessResult processedRelationshipResult = SchemaAnnotationProcessor.process( diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/registry/ConfigEntityRegistry.java b/entity-registry/src/main/java/com/linkedin/metadata/models/registry/ConfigEntityRegistry.java index 41043995a3b77..9aed29ab8595e 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/registry/ConfigEntityRegistry.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/registry/ConfigEntityRegistry.java @@ -91,7 +91,7 @@ private static Pair getFileAndClassPath(String entityRegistryRoot) .filter(Files::isRegularFile) .filter(f -> f.endsWith("entity-registry.yml") || f.endsWith("entity-registry.yaml")) .collect(Collectors.toList()); - if (yamlFiles.size() == 0) { + if (yamlFiles.isEmpty()) { throw new EntityRegistryException( String.format( "Did not find an entity registry (entity_registry.yaml/yml) under %s", diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/registry/MergedEntityRegistry.java b/entity-registry/src/main/java/com/linkedin/metadata/models/registry/MergedEntityRegistry.java index 650a1cd41066e..0dcd0420d4df8 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/registry/MergedEntityRegistry.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/registry/MergedEntityRegistry.java @@ -58,7 +58,7 @@ private void validateEntitySpec(EntitySpec entitySpec, final ValidationResult va validationResult.setValid(false); validationResult .getValidationFailures() - .add(String.format("Key aspect is missing in entity {}", entitySpec.getName())); + .add(String.format("Key aspect is missing in entity %s", entitySpec.getName())); } } @@ -86,7 +86,7 @@ public MergedEntityRegistry apply(EntityRegistry patchEntityRegistry) } // Merge Event Specs - if (patchEntityRegistry.getEventSpecs().size() > 0) { + if (!patchEntityRegistry.getEventSpecs().isEmpty()) { eventNameToSpec.putAll(patchEntityRegistry.getEventSpecs()); } // TODO: Validate that the entity registries don't have conflicts among each other @@ -116,19 +116,18 @@ private void checkMergeable( if (existingEntitySpec != null) { existingEntitySpec .getAspectSpecMap() - .entrySet() .forEach( - aspectSpecEntry -> { - if (newEntitySpec.hasAspect(aspectSpecEntry.getKey())) { + (key, value) -> { + if (newEntitySpec.hasAspect(key)) { CompatibilityResult result = CompatibilityChecker.checkCompatibility( - aspectSpecEntry.getValue().getPegasusSchema(), - newEntitySpec.getAspectSpec(aspectSpecEntry.getKey()).getPegasusSchema(), + value.getPegasusSchema(), + newEntitySpec.getAspectSpec(key).getPegasusSchema(), new CompatibilityOptions()); if (result.isError()) { log.error( "{} schema is not compatible with previous schema due to {}", - aspectSpecEntry.getKey(), + key, result.getMessages()); // we want to continue processing all aspects to collect all failures validationResult.setValid(false); @@ -137,11 +136,11 @@ private void checkMergeable( .add( String.format( "%s schema is not compatible with previous schema due to %s", - aspectSpecEntry.getKey(), result.getMessages())); + key, result.getMessages())); } else { log.info( "{} schema is compatible with previous schema due to {}", - aspectSpecEntry.getKey(), + key, result.getMessages()); } } @@ -222,7 +221,7 @@ public PluginFactory getPluginFactory() { @Setter @Getter - private class ValidationResult { + private static class ValidationResult { boolean valid = true; List validationFailures = new ArrayList<>(); } diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/registry/PatchEntityRegistry.java b/entity-registry/src/main/java/com/linkedin/metadata/models/registry/PatchEntityRegistry.java index b82b905c50004..b4fc4193e7263 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/registry/PatchEntityRegistry.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/registry/PatchEntityRegistry.java @@ -71,19 +71,17 @@ public class PatchEntityRegistry implements EntityRegistry { @Override public String toString() { StringBuilder sb = new StringBuilder("PatchEntityRegistry[" + "identifier=" + identifier + ';'); - entityNameToSpec.entrySet().stream() - .forEach( - entry -> - sb.append("[entityName=") - .append(entry.getKey()) - .append(";aspects=[") - .append( - entry.getValue().getAspectSpecs().stream() - .map(spec -> spec.getName()) - .collect(Collectors.joining(","))) - .append("]]")); - eventNameToSpec.entrySet().stream() - .forEach(entry -> sb.append("[eventName=").append(entry.getKey()).append("]")); + entityNameToSpec.forEach( + (key1, value1) -> + sb.append("[entityName=") + .append(key1) + .append(";aspects=[") + .append( + value1.getAspectSpecs().stream() + .map(AspectSpec::getName) + .collect(Collectors.joining(","))) + .append("]]")); + eventNameToSpec.forEach((key, value) -> sb.append("[eventName=").append(key).append("]")); return sb.toString(); } @@ -119,7 +117,7 @@ private static Pair getFileAndClassPath(String entityRegistryRoot) .filter(Files::isRegularFile) .filter(f -> f.endsWith("entity-registry.yml") || f.endsWith("entity-registry.yaml")) .collect(Collectors.toList()); - if (yamlFiles.size() == 0) { + if (yamlFiles.isEmpty()) { throw new EntityRegistryException( String.format( "Did not find an entity registry (entity-registry.yaml/yml) under %s", @@ -175,7 +173,7 @@ private PatchEntityRegistry( entities = OBJECT_MAPPER.readValue(configFileStream, Entities.class); this.pluginFactory = PluginFactory.withCustomClasspath(entities.getPlugins(), classLoaders); } catch (IOException e) { - e.printStackTrace(); + log.error("Unable to read Patch configuration.", e); throw new IllegalArgumentException( String.format( "Error while reading config file in path %s: %s", configFileStream, e.getMessage())); diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/registry/SnapshotEntityRegistry.java b/entity-registry/src/main/java/com/linkedin/metadata/models/registry/SnapshotEntityRegistry.java index 8fefa2fe00ae8..22aeddb6ac65f 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/registry/SnapshotEntityRegistry.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/registry/SnapshotEntityRegistry.java @@ -120,7 +120,7 @@ public AspectTemplateEngine getAspectTemplateEngine() { } @Override - public EventSpec getEventSpec(final String ignored) { + public EventSpec getEventSpec(@Nonnull final String ignored) { return null; } diff --git a/entity-registry/src/test/java/com/linkedin/metadata/models/EntitySpecBuilderTest.java b/entity-registry/src/test/java/com/linkedin/metadata/models/EntitySpecBuilderTest.java index d9cf8fd2603a8..8b043569dd16a 100644 --- a/entity-registry/src/test/java/com/linkedin/metadata/models/EntitySpecBuilderTest.java +++ b/entity-registry/src/test/java/com/linkedin/metadata/models/EntitySpecBuilderTest.java @@ -189,7 +189,7 @@ private void validateTestEntityInfo(final AspectSpec testEntityInfo) { testEntityInfo.getPegasusSchema().getFullName()); // Assert on Searchable Fields - assertEquals(testEntityInfo.getSearchableFieldSpecs().size(), 11); + assertEquals(testEntityInfo.getSearchableFieldSpecs().size(), 12); assertEquals( "customProperties", testEntityInfo @@ -340,6 +340,20 @@ private void validateTestEntityInfo(final AspectSpec testEntityInfo) { .get(new PathSpec("doubleField").toString()) .getSearchableAnnotation() .getFieldType()); + assertEquals( + "removed", + testEntityInfo + .getSearchableFieldSpecMap() + .get(new PathSpec("removed").toString()) + .getSearchableAnnotation() + .getFieldName()); + assertEquals( + SearchableAnnotation.FieldType.BOOLEAN, + testEntityInfo + .getSearchableFieldSpecMap() + .get(new PathSpec("removed").toString()) + .getSearchableAnnotation() + .getFieldType()); // Assert on Relationship Fields assertEquals(4, testEntityInfo.getRelationshipFieldSpecs().size()); diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESBrowseDAO.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESBrowseDAO.java index 3c71a2dfd9180..d610ea4b4e028 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESBrowseDAO.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESBrowseDAO.java @@ -19,6 +19,7 @@ import com.linkedin.metadata.config.search.SearchConfiguration; import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.models.EntitySpec; +import com.linkedin.metadata.models.SearchableFieldSpec; import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.search.elasticsearch.query.request.SearchRequestHandler; @@ -33,6 +34,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -554,7 +556,8 @@ private QueryBuilder buildQueryStringV2( queryBuilder.filter(QueryBuilders.rangeQuery(BROWSE_PATH_V2_DEPTH).gt(browseDepthVal)); - queryBuilder.filter(SearchRequestHandler.getFilterQuery(filter)); + queryBuilder.filter( + SearchRequestHandler.getFilterQuery(filter, entitySpec.getSearchableFieldSpecMap())); return queryBuilder; } @@ -580,7 +583,18 @@ private QueryBuilder buildQueryStringBrowseAcrossEntities( queryBuilder.filter(QueryBuilders.rangeQuery(BROWSE_PATH_V2_DEPTH).gt(browseDepthVal)); - queryBuilder.filter(SearchRequestHandler.getFilterQuery(filter)); + Map> searchableFields = + entitySpecs.stream() + .flatMap(entitySpec -> entitySpec.getSearchableFieldSpecMap().entrySet().stream()) + .collect( + Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (set1, set2) -> { + set1.addAll(set2); + return set1; + })); + queryBuilder.filter(SearchRequestHandler.getFilterQuery(filter, searchableFields)); return queryBuilder; } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java index 0eb44edfb11de..1ec90ed6f61e2 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java @@ -78,7 +78,8 @@ public long docCount(@Nonnull String entityName) { EntitySpec entitySpec = entityRegistry.getEntitySpec(entityName); CountRequest countRequest = new CountRequest(indexConvention.getIndexName(entitySpec)) - .query(SearchRequestHandler.getFilterQuery(null)); + .query( + SearchRequestHandler.getFilterQuery(null, entitySpec.getSearchableFieldSpecMap())); try (Timer.Context ignored = MetricUtils.timer(this.getClass(), "docCount").time()) { return client.count(countRequest, RequestOptions.DEFAULT).getCount(); } catch (IOException e) { @@ -315,9 +316,17 @@ public Map aggregateByValue( @Nonnull String field, @Nullable Filter requestParams, int limit) { + List entitySpecs; + if (entityNames == null || entityNames.isEmpty()) { + entitySpecs = new ArrayList<>(entityRegistry.getEntitySpecs().values()); + } else { + entitySpecs = + entityNames.stream().map(entityRegistry::getEntitySpec).collect(Collectors.toList()); + } final SearchRequest searchRequest = - SearchRequestHandler.getAggregationRequest( - field, transformFilterForEntities(requestParams, indexConvention), limit); + SearchRequestHandler.getBuilder(entitySpecs, searchConfiguration, customSearchConfiguration) + .getAggregationRequest( + field, transformFilterForEntities(requestParams, indexConvention), limit); if (entityNames == null) { String indexName = indexConvention.getAllEntityIndicesPattern(); searchRequest.indices(indexName); diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/AutocompleteRequestHandler.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/AutocompleteRequestHandler.java index cdcdae2f3d311..333d9602734d2 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/AutocompleteRequestHandler.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/AutocompleteRequestHandler.java @@ -14,6 +14,7 @@ import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.search.utils.ESUtils; import java.net.URISyntaxException; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -40,19 +41,33 @@ public class AutocompleteRequestHandler { private final List _defaultAutocompleteFields; + private final Map> searchableFields; private static final Map AUTOCOMPLETE_QUERY_BUILDER_BY_ENTITY_NAME = new ConcurrentHashMap<>(); public AutocompleteRequestHandler(@Nonnull EntitySpec entitySpec) { + List fieldSpecs = entitySpec.getSearchableFieldSpecs(); _defaultAutocompleteFields = Stream.concat( - entitySpec.getSearchableFieldSpecs().stream() + fieldSpecs.stream() .map(SearchableFieldSpec::getSearchableAnnotation) .filter(SearchableAnnotation::isEnableAutocomplete) .map(SearchableAnnotation::getFieldName), Stream.of("urn")) .collect(Collectors.toList()); + searchableFields = + fieldSpecs.stream() + .collect( + Collectors.toMap( + searchableFieldSpec -> + searchableFieldSpec.getSearchableAnnotation().getFieldName(), + searchableFieldSpec -> + new HashSet<>(Collections.singleton(searchableFieldSpec)), + (set1, set2) -> { + set1.addAll(set2); + return set1; + })); } public static AutocompleteRequestHandler getBuilder(@Nonnull EntitySpec entitySpec) { @@ -66,7 +81,7 @@ public SearchRequest getSearchRequest( SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.size(limit); searchSourceBuilder.query(getQuery(input, field)); - searchSourceBuilder.postFilter(ESUtils.buildFilterQuery(filter, false)); + searchSourceBuilder.postFilter(ESUtils.buildFilterQuery(filter, false, searchableFields)); searchSourceBuilder.highlighter(getHighlights(field)); searchRequest.source(searchSourceBuilder); return searchRequest; diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java index c5a5ade216bf7..e6ee909c80dae 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java @@ -97,6 +97,7 @@ public class SearchRequestHandler { private final SearchConfiguration _configs; private final SearchQueryBuilder _searchQueryBuilder; private final AggregationQueryBuilder _aggregationQueryBuilder; + private final Map> searchableFields; private SearchRequestHandler( @Nonnull EntitySpec entitySpec, @@ -121,6 +122,17 @@ private SearchRequestHandler( _searchQueryBuilder = new SearchQueryBuilder(configs, customSearchConfiguration); _aggregationQueryBuilder = new AggregationQueryBuilder(configs, annotations); _configs = configs; + searchableFields = + _entitySpecs.stream() + .flatMap(entitySpec -> entitySpec.getSearchableFieldSpecMap().entrySet().stream()) + .collect( + Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (set1, set2) -> { + set1.addAll(set2); + return set1; + })); } public static SearchRequestHandler getBuilder( @@ -169,8 +181,13 @@ private BinaryOperator mapMerger() { }; } - public static BoolQueryBuilder getFilterQuery(@Nullable Filter filter) { - BoolQueryBuilder filterQuery = ESUtils.buildFilterQuery(filter, false); + public BoolQueryBuilder getFilterQuery(@Nullable Filter filter) { + return getFilterQuery(filter, searchableFields); + } + + public static BoolQueryBuilder getFilterQuery( + @Nullable Filter filter, Map> searchableFields) { + BoolQueryBuilder filterQuery = ESUtils.buildFilterQuery(filter, false, searchableFields); return filterSoftDeletedByDefault(filter, filterQuery); } @@ -354,7 +371,7 @@ public SearchRequest getFilterRequest( * @return {@link SearchRequest} that contains the aggregation query */ @Nonnull - public static SearchRequest getAggregationRequest( + public SearchRequest getAggregationRequest( @Nonnull String field, @Nullable Filter filter, int limit) { SearchRequest searchRequest = new SearchRequest(); BoolQueryBuilder filterQuery = getFilterQuery(filter); diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java index aa854149de43a..77a67f100895c 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java @@ -7,7 +7,6 @@ import static com.linkedin.metadata.search.utils.SearchUtils.isUrn; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import com.linkedin.metadata.models.EntitySpec; import com.linkedin.metadata.models.SearchableFieldSpec; import com.linkedin.metadata.models.StructuredPropertyUtils; @@ -18,11 +17,13 @@ import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.query.filter.SortCriterion; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; @@ -32,6 +33,7 @@ import org.opensearch.index.query.BoolQueryBuilder; import org.opensearch.index.query.QueryBuilder; import org.opensearch.index.query.QueryBuilders; +import org.opensearch.index.query.RangeQueryBuilder; import org.opensearch.search.builder.PointInTimeBuilder; import org.opensearch.search.builder.SearchSourceBuilder; import org.opensearch.search.sort.FieldSortBuilder; @@ -76,6 +78,13 @@ public class ESUtils { SearchableAnnotation.FieldType.BROWSE_PATH_V2, SearchableAnnotation.FieldType.URN, SearchableAnnotation.FieldType.URN_PARTIAL); + + public static final Set RANGE_QUERY_CONDITIONS = + Set.of( + Condition.GREATER_THAN, + Condition.GREATER_THAN_OR_EQUAL_TO, + Condition.LESS_THAN, + Condition.LESS_THAN_OR_EQUAL_TO); public static final String ENTITY_NAME_FIELD = "_entityName"; public static final String NAME_SUGGESTION = "nameSuggestion"; @@ -100,9 +109,6 @@ public class ESUtils { } }; - // TODO - This has been expanded for has* in another branch - public static final Set BOOLEAN_FIELDS = ImmutableSet.of("removed"); - /* * Refer to https://www.elastic.co/guide/en/elasticsearch/reference/current/regexp-syntax.html for list of reserved * characters in an Elasticsearch regular expression. @@ -123,7 +129,10 @@ private ESUtils() {} * @return built filter query */ @Nonnull - public static BoolQueryBuilder buildFilterQuery(@Nullable Filter filter, boolean isTimeseries) { + public static BoolQueryBuilder buildFilterQuery( + @Nullable Filter filter, + boolean isTimeseries, + final Map> searchableFields) { BoolQueryBuilder finalQueryBuilder = QueryBuilders.boolQuery(); if (filter == null) { return finalQueryBuilder; @@ -134,7 +143,8 @@ public static BoolQueryBuilder buildFilterQuery(@Nullable Filter filter, boolean .getOr() .forEach( or -> - finalQueryBuilder.should(ESUtils.buildConjunctiveFilterQuery(or, isTimeseries))); + finalQueryBuilder.should( + ESUtils.buildConjunctiveFilterQuery(or, isTimeseries, searchableFields))); } else if (filter.getCriteria() != null) { // Otherwise, build boolean query from the deprecated "criteria" field. log.warn("Received query Filter with a deprecated field 'criteria'. Use 'or' instead."); @@ -146,7 +156,8 @@ public static BoolQueryBuilder buildFilterQuery(@Nullable Filter filter, boolean if (!criterion.getValue().trim().isEmpty() || criterion.hasValues() || criterion.getCondition() == Condition.IS_NULL) { - andQueryBuilder.must(getQueryBuilderFromCriterion(criterion, isTimeseries)); + andQueryBuilder.must( + getQueryBuilderFromCriterion(criterion, isTimeseries, searchableFields)); } }); finalQueryBuilder.should(andQueryBuilder); @@ -156,7 +167,9 @@ public static BoolQueryBuilder buildFilterQuery(@Nullable Filter filter, boolean @Nonnull public static BoolQueryBuilder buildConjunctiveFilterQuery( - @Nonnull ConjunctiveCriterion conjunctiveCriterion, boolean isTimeseries) { + @Nonnull ConjunctiveCriterion conjunctiveCriterion, + boolean isTimeseries, + Map> searchableFields) { final BoolQueryBuilder andQueryBuilder = new BoolQueryBuilder(); conjunctiveCriterion .getAnd() @@ -167,9 +180,11 @@ public static BoolQueryBuilder buildConjunctiveFilterQuery( || criterion.hasValues()) { if (!criterion.isNegated()) { // `filter` instead of `must` (enables caching and bypasses scoring) - andQueryBuilder.filter(getQueryBuilderFromCriterion(criterion, isTimeseries)); + andQueryBuilder.filter( + getQueryBuilderFromCriterion(criterion, isTimeseries, searchableFields)); } else { - andQueryBuilder.mustNot(getQueryBuilderFromCriterion(criterion, isTimeseries)); + andQueryBuilder.mustNot( + getQueryBuilderFromCriterion(criterion, isTimeseries, searchableFields)); } } }); @@ -205,7 +220,9 @@ public static BoolQueryBuilder buildConjunctiveFilterQuery( */ @Nonnull public static QueryBuilder getQueryBuilderFromCriterion( - @Nonnull final Criterion criterion, boolean isTimeseries) { + @Nonnull final Criterion criterion, + boolean isTimeseries, + final Map> searchableFields) { final String fieldName = toFacetField(criterion.getField()); if (fieldName.startsWith(STRUCTURED_PROPERTY_MAPPING_FIELD)) { criterion.setField(fieldName); @@ -224,10 +241,10 @@ public static QueryBuilder getQueryBuilderFromCriterion( if (maybeFieldToExpand.isPresent()) { return getQueryBuilderFromCriterionForFieldToExpand( - maybeFieldToExpand.get(), criterion, isTimeseries); + maybeFieldToExpand.get(), criterion, isTimeseries, searchableFields); } - return getQueryBuilderFromCriterionForSingleField(criterion, isTimeseries); + return getQueryBuilderFromCriterionForSingleField(criterion, isTimeseries, searchableFields); } public static String getElasticTypeForFieldType(SearchableAnnotation.FieldType fieldType) { @@ -378,7 +395,7 @@ public static String toFacetField(@Nonnull final String filterField) { @Nonnull public static String toKeywordField( - @Nonnull final String filterField, @Nonnull final boolean skipKeywordSuffix) { + @Nonnull final String filterField, final boolean skipKeywordSuffix) { return skipKeywordSuffix || KEYWORD_FIELDS.contains(filterField) || PATH_HIERARCHY_FIELDS.contains(filterField) @@ -428,7 +445,8 @@ public static void setSearchAfter( private static QueryBuilder getQueryBuilderFromCriterionForFieldToExpand( @Nonnull final List fields, @Nonnull final Criterion criterion, - final boolean isTimeseries) { + final boolean isTimeseries, + final Map> searchableFields) { final BoolQueryBuilder orQueryBuilder = new BoolQueryBuilder(); for (String field : fields) { Criterion criterionToQuery = new Criterion(); @@ -442,14 +460,17 @@ private static QueryBuilder getQueryBuilderFromCriterionForFieldToExpand( } criterionToQuery.setField(toKeywordField(field, isTimeseries)); orQueryBuilder.should( - getQueryBuilderFromCriterionForSingleField(criterionToQuery, isTimeseries)); + getQueryBuilderFromCriterionForSingleField( + criterionToQuery, isTimeseries, searchableFields)); } return orQueryBuilder; } @Nonnull private static QueryBuilder getQueryBuilderFromCriterionForSingleField( - @Nonnull Criterion criterion, @Nonnull boolean isTimeseries) { + @Nonnull Criterion criterion, + boolean isTimeseries, + final Map> searchableFields) { final Condition condition = criterion.getCondition(); final String fieldName = toFacetField(criterion.getField()); @@ -463,24 +484,11 @@ private static QueryBuilder getQueryBuilderFromCriterionForSingleField( .queryName(fieldName); } else if (criterion.hasValues() || criterion.hasValue()) { if (condition == Condition.EQUAL) { - return buildEqualsConditionFromCriterion(fieldName, criterion, isTimeseries); - // TODO: Support multi-match on the following operators (using new 'values' field) - } else if (condition == Condition.GREATER_THAN) { - return QueryBuilders.rangeQuery(criterion.getField()) - .gt(criterion.getValue().trim()) - .queryName(fieldName); - } else if (condition == Condition.GREATER_THAN_OR_EQUAL_TO) { - return QueryBuilders.rangeQuery(criterion.getField()) - .gte(criterion.getValue().trim()) - .queryName(fieldName); - } else if (condition == Condition.LESS_THAN) { - return QueryBuilders.rangeQuery(criterion.getField()) - .lt(criterion.getValue().trim()) - .queryName(fieldName); - } else if (condition == Condition.LESS_THAN_OR_EQUAL_TO) { - return QueryBuilders.rangeQuery(criterion.getField()) - .lte(criterion.getValue().trim()) - .queryName(fieldName); + return buildEqualsConditionFromCriterion( + fieldName, criterion, isTimeseries, searchableFields); + } else if (RANGE_QUERY_CONDITIONS.contains(condition)) { + return buildRangeQueryFromCriterion( + criterion, fieldName, searchableFields, condition, isTimeseries); } else if (condition == Condition.CONTAIN) { return QueryBuilders.wildcardQuery( toKeywordField(criterion.getField(), isTimeseries), @@ -504,13 +512,15 @@ private static QueryBuilder getQueryBuilderFromCriterionForSingleField( private static QueryBuilder buildEqualsConditionFromCriterion( @Nonnull final String fieldName, @Nonnull final Criterion criterion, - final boolean isTimeseries) { + final boolean isTimeseries, + final Map> searchableFields) { /* * If the newer 'values' field of Criterion.pdl is set, then we * handle using the following code to allow multi-match. */ if (!criterion.getValues().isEmpty()) { - return buildEqualsConditionFromCriterionWithValues(fieldName, criterion, isTimeseries); + return buildEqualsConditionFromCriterionWithValues( + fieldName, criterion, isTimeseries, searchableFields); } /* * Otherwise, we are likely using the deprecated 'value' field. @@ -526,21 +536,95 @@ private static QueryBuilder buildEqualsConditionFromCriterion( private static QueryBuilder buildEqualsConditionFromCriterionWithValues( @Nonnull final String fieldName, @Nonnull final Criterion criterion, - final boolean isTimeseries) { - if (BOOLEAN_FIELDS.contains(fieldName) && criterion.getValues().size() == 1) { - // Handle special-cased Boolean fields. - // here we special case boolean fields we recognize the names of and hard-cast - // the first provided value to a boolean to do the comparison. - // Ideally, we should detect the type of the field from the entity-registry in order - // to determine how to cast. + final boolean isTimeseries, + final Map> searchableFields) { + Set fieldTypes = getFieldTypes(searchableFields, fieldName); + if (fieldTypes.size() > 1) { + log.warn( + "Multiple field types for field name {}, determining best fit for set: {}", + fieldName, + fieldTypes); + } + if (fieldTypes.contains(BOOLEAN_FIELD_TYPE) && criterion.getValues().size() == 1) { return QueryBuilders.termQuery(fieldName, Boolean.parseBoolean(criterion.getValues().get(0))) .queryName(fieldName); + } else if (fieldTypes.contains(LONG_FIELD_TYPE) || fieldTypes.contains(DATE_FIELD_TYPE)) { + List longValues = + criterion.getValues().stream().map(Long::parseLong).collect(Collectors.toList()); + return QueryBuilders.termsQuery(fieldName, longValues).queryName(fieldName); + } else if (fieldTypes.contains(DOUBLE_FIELD_TYPE)) { + List doubleValues = + criterion.getValues().stream().map(Double::parseDouble).collect(Collectors.toList()); + return QueryBuilders.termsQuery(fieldName, doubleValues).queryName(fieldName); } return QueryBuilders.termsQuery( toKeywordField(criterion.getField(), isTimeseries), criterion.getValues()) .queryName(fieldName); } + private static Set getFieldTypes( + Map> searchableFields, String fieldName) { + Set fieldSpecs = + searchableFields.getOrDefault(fieldName, Collections.emptySet()); + Set fieldTypes = + fieldSpecs.stream() + .map(SearchableFieldSpec::getSearchableAnnotation) + .map(SearchableAnnotation::getFieldType) + .map(ESUtils::getElasticTypeForFieldType) + .collect(Collectors.toSet()); + if (fieldTypes.size() > 1) { + log.warn( + "Multiple field types for field name {}, determining best fit for set: {}", + fieldName, + fieldTypes); + } + return fieldTypes; + } + + private static RangeQueryBuilder buildRangeQueryFromCriterion( + Criterion criterion, + String fieldName, + Map> searchableFields, + Condition condition, + boolean isTimeseries) { + Set fieldTypes = getFieldTypes(searchableFields, fieldName); + + // Determine criterion value, range query only accepts single value so take first value in + // values if multiple + String criterionValueString; + if (!criterion.getValues().isEmpty()) { + criterionValueString = criterion.getValues().get(0).trim(); + } else { + criterionValueString = criterion.getValue().trim(); + } + Object criterionValue; + String documentFieldName; + if (fieldTypes.contains(BOOLEAN_FIELD_TYPE)) { + criterionValue = Boolean.parseBoolean(criterionValueString); + documentFieldName = criterion.getField(); + } else if (fieldTypes.contains(LONG_FIELD_TYPE) || fieldTypes.contains(DATE_FIELD_TYPE)) { + criterionValue = Long.parseLong(criterionValueString); + documentFieldName = criterion.getField(); + } else if (fieldTypes.contains(DOUBLE_FIELD_TYPE)) { + criterionValue = Double.parseDouble(criterionValueString); + documentFieldName = criterion.getField(); + } else { + criterionValue = criterionValueString; + documentFieldName = toKeywordField(criterion.getField(), isTimeseries); + } + + // Set up QueryBuilder based on condition + if (condition == Condition.GREATER_THAN) { + return QueryBuilders.rangeQuery(documentFieldName).gt(criterionValue).queryName(fieldName); + } else if (condition == Condition.GREATER_THAN_OR_EQUAL_TO) { + return QueryBuilders.rangeQuery(documentFieldName).gte(criterionValue).queryName(fieldName); + } else if (condition == Condition.LESS_THAN) { + return QueryBuilders.rangeQuery(documentFieldName).lt(criterionValue).queryName(fieldName); + } else /*if (condition == Condition.LESS_THAN_OR_EQUAL_TO)*/ { + return QueryBuilders.rangeQuery(documentFieldName).lte(criterionValue).queryName(fieldName); + } + } + /** * Builds an instance of {@link QueryBuilder} representing an EQUALS condition which was created * using the deprecated 'value' field of Criterion.pdl model. diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/ElasticSearchTimeseriesAspectService.java b/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/ElasticSearchTimeseriesAspectService.java index a2b36b7d8ddb8..6cf8e92d61929 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/ElasticSearchTimeseriesAspectService.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/ElasticSearchTimeseriesAspectService.java @@ -14,6 +14,7 @@ import com.linkedin.metadata.aspect.EnvelopedAspect; import com.linkedin.metadata.models.AspectSpec; import com.linkedin.metadata.models.EntitySpec; +import com.linkedin.metadata.models.SearchableFieldSpec; import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.query.filter.Condition; import com.linkedin.metadata.query.filter.Criterion; @@ -290,7 +291,12 @@ public long countByFilter( @Nullable final Filter filter) { final String indexName = _indexConvention.getTimeseriesAspectIndexName(entityName, aspectName); final BoolQueryBuilder filterQueryBuilder = - QueryBuilders.boolQuery().must(ESUtils.buildFilterQuery(filter, true)); + QueryBuilders.boolQuery() + .must( + ESUtils.buildFilterQuery( + filter, + true, + _entityRegistry.getEntitySpec(entityName).getSearchableFieldSpecMap())); CountRequest countRequest = new CountRequest(); countRequest.query(filterQueryBuilder); countRequest.indices(indexName); @@ -313,8 +319,10 @@ public List getAspectValues( @Nullable final Integer limit, @Nullable final Filter filter, @Nullable final SortCriterion sort) { + Map> searchableFields = + _entityRegistry.getEntitySpec(entityName).getSearchableFieldSpecMap(); final BoolQueryBuilder filterQueryBuilder = - QueryBuilders.boolQuery().must(ESUtils.buildFilterQuery(filter, true)); + QueryBuilders.boolQuery().must(ESUtils.buildFilterQuery(filter, true, searchableFields)); filterQueryBuilder.must(QueryBuilders.matchQuery("urn", urn.toString())); // NOTE: We are interested only in the un-exploded rows as only they carry the `event` payload. filterQueryBuilder.mustNot(QueryBuilders.termQuery(MappingsBuilder.IS_EXPLODED_FIELD, true)); @@ -324,7 +332,8 @@ public List getAspectValues( .setField(MappingsBuilder.TIMESTAMP_MILLIS_FIELD) .setCondition(Condition.GREATER_THAN_OR_EQUAL_TO) .setValue(startTimeMillis.toString()); - filterQueryBuilder.must(ESUtils.getQueryBuilderFromCriterion(startTimeCriterion, true)); + filterQueryBuilder.must( + ESUtils.getQueryBuilderFromCriterion(startTimeCriterion, true, searchableFields)); } if (endTimeMillis != null) { Criterion endTimeCriterion = @@ -332,7 +341,8 @@ public List getAspectValues( .setField(MappingsBuilder.TIMESTAMP_MILLIS_FIELD) .setCondition(Condition.LESS_THAN_OR_EQUAL_TO) .setValue(endTimeMillis.toString()); - filterQueryBuilder.must(ESUtils.getQueryBuilderFromCriterion(endTimeCriterion, true)); + filterQueryBuilder.must( + ESUtils.getQueryBuilderFromCriterion(endTimeCriterion, true, searchableFields)); } final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(filterQueryBuilder); @@ -400,7 +410,9 @@ public GenericTable getAggregatedStats( public DeleteAspectValuesResult deleteAspectValues( @Nonnull String entityName, @Nonnull String aspectName, @Nonnull Filter filter) { final String indexName = _indexConvention.getTimeseriesAspectIndexName(entityName, aspectName); - final BoolQueryBuilder filterQueryBuilder = ESUtils.buildFilterQuery(filter, true); + final BoolQueryBuilder filterQueryBuilder = + ESUtils.buildFilterQuery( + filter, true, _entityRegistry.getEntitySpec(entityName).getSearchableFieldSpecMap()); final Optional result = _bulkProcessor @@ -426,7 +438,9 @@ public String deleteAspectValuesAsync( @Nonnull Filter filter, @Nonnull BatchWriteOperationsOptions options) { final String indexName = _indexConvention.getTimeseriesAspectIndexName(entityName, aspectName); - final BoolQueryBuilder filterQueryBuilder = ESUtils.buildFilterQuery(filter, true); + final BoolQueryBuilder filterQueryBuilder = + ESUtils.buildFilterQuery( + filter, true, _entityRegistry.getEntitySpec(entityName).getSearchableFieldSpecMap()); final int batchSize = options.getBatchSize() > 0 ? options.getBatchSize() : DEFAULT_LIMIT; TimeValue timeout = options.getTimeoutSeconds() > 0 @@ -450,7 +464,9 @@ public String reindexAsync( @Nonnull Filter filter, @Nonnull BatchWriteOperationsOptions options) { final String indexName = _indexConvention.getTimeseriesAspectIndexName(entityName, aspectName); - final BoolQueryBuilder filterQueryBuilder = ESUtils.buildFilterQuery(filter, true); + final BoolQueryBuilder filterQueryBuilder = + ESUtils.buildFilterQuery( + filter, true, _entityRegistry.getEntitySpec(entityName).getSearchableFieldSpecMap()); try { return this.reindexAsync(indexName, filterQueryBuilder, options); } catch (Exception e) { @@ -498,8 +514,11 @@ public TimeseriesScrollResult scrollAspects( int count, @Nullable Long startTimeMillis, @Nullable Long endTimeMillis) { + + Map> searchableFields = + _entityRegistry.getEntitySpec(entityName).getSearchableFieldSpecMap(); final BoolQueryBuilder filterQueryBuilder = - QueryBuilders.boolQuery().filter(ESUtils.buildFilterQuery(filter, true)); + QueryBuilders.boolQuery().filter(ESUtils.buildFilterQuery(filter, true, searchableFields)); if (startTimeMillis != null) { Criterion startTimeCriterion = @@ -507,7 +526,8 @@ public TimeseriesScrollResult scrollAspects( .setField(MappingsBuilder.TIMESTAMP_MILLIS_FIELD) .setCondition(Condition.GREATER_THAN_OR_EQUAL_TO) .setValue(startTimeMillis.toString()); - filterQueryBuilder.filter(ESUtils.getQueryBuilderFromCriterion(startTimeCriterion, true)); + filterQueryBuilder.filter( + ESUtils.getQueryBuilderFromCriterion(startTimeCriterion, true, searchableFields)); } if (endTimeMillis != null) { Criterion endTimeCriterion = @@ -515,7 +535,8 @@ public TimeseriesScrollResult scrollAspects( .setField(MappingsBuilder.TIMESTAMP_MILLIS_FIELD) .setCondition(Condition.LESS_THAN_OR_EQUAL_TO) .setValue(endTimeMillis.toString()); - filterQueryBuilder.filter(ESUtils.getQueryBuilderFromCriterion(endTimeCriterion, true)); + filterQueryBuilder.filter( + ESUtils.getQueryBuilderFromCriterion(endTimeCriterion, true, searchableFields)); } SearchResponse response = @@ -537,7 +558,7 @@ public TimeseriesScrollResult scrollAspects( } private SearchResponse executeScrollSearchQuery( - @Nonnull final String entityNname, + @Nonnull final String entityName, @Nonnull final String aspectName, @Nonnull final QueryBuilder query, @Nonnull List sortCriterion, @@ -560,7 +581,7 @@ private SearchResponse executeScrollSearchQuery( searchRequest.source(searchSourceBuilder); ESUtils.setSearchAfter(searchSourceBuilder, sort, null, null); - searchRequest.indices(_indexConvention.getTimeseriesAspectIndexName(entityNname, aspectName)); + searchRequest.indices(_indexConvention.getTimeseriesAspectIndexName(entityName, aspectName)); try (Timer.Context ignored = MetricUtils.timer(this.getClass(), "scrollAspects_search").time()) { diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/query/ESAggregatedStatsDAO.java b/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/query/ESAggregatedStatsDAO.java index 539e5dfbaa1d0..f8b2cd8552357 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/query/ESAggregatedStatsDAO.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/query/ESAggregatedStatsDAO.java @@ -377,7 +377,9 @@ public GenericTable getAggregatedStats( @Nullable GroupingBucket[] groupingBuckets) { // Setup the filter query builder using the input filter provided. - final BoolQueryBuilder filterQueryBuilder = ESUtils.buildFilterQuery(filter, true); + final BoolQueryBuilder filterQueryBuilder = + ESUtils.buildFilterQuery( + filter, true, _entityRegistry.getEntitySpec(entityName).getSearchableFieldSpecMap()); AspectSpec aspectSpec = getTimeseriesAspectSpec(entityName, aspectName); // Build and attach the grouping aggregations diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/GoldenTestBase.java b/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/GoldenTestBase.java index d2aef982750bd..4c125065deb4d 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/GoldenTestBase.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/GoldenTestBase.java @@ -1,18 +1,27 @@ package com.linkedin.metadata.search.fixtures; +import static com.linkedin.metadata.Constants.*; import static io.datahubproject.test.search.SearchTestUtils.searchAcrossCustomEntities; import static io.datahubproject.test.search.SearchTestUtils.searchAcrossEntities; -import static org.testng.Assert.assertTrue; +import static org.testng.Assert.*; import static org.testng.AssertJUnit.assertNotNull; +import com.google.common.collect.ImmutableList; import com.linkedin.common.urn.Urn; +import com.linkedin.data.template.StringArray; import com.linkedin.datahub.graphql.generated.EntityType; import com.linkedin.datahub.graphql.types.entitytype.EntityTypeMapper; import com.linkedin.metadata.models.registry.EntityRegistry; +import com.linkedin.metadata.query.filter.ConjunctiveCriterion; +import com.linkedin.metadata.query.filter.ConjunctiveCriterionArray; +import com.linkedin.metadata.query.filter.Criterion; +import com.linkedin.metadata.query.filter.CriterionArray; +import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.search.MatchedFieldArray; import com.linkedin.metadata.search.SearchEntityArray; import com.linkedin.metadata.search.SearchResult; import com.linkedin.metadata.search.SearchService; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -169,6 +178,35 @@ public void testNameMatchCustomerOrders() { assertTrue(firstResultScore > secondResultScore); } + @Test + public void testFilterOnCountField() { + assertNotNull(getSearchService()); + Filter filter = + new Filter() + .setOr( + new ConjunctiveCriterionArray( + new ConjunctiveCriterion() + .setAnd( + new CriterionArray( + ImmutableList.of( + new Criterion() + .setField("rowCount") + .setValue("") + .setValues(new StringArray(ImmutableList.of("68")))))))); + SearchResult searchResult = + searchAcrossEntities( + getSearchService(), + "*", + SEARCHABLE_LONGTAIL_ENTITIES, + filter, + Collections.singletonList(DATASET_ENTITY_NAME)); + assertFalse(searchResult.getEntities().isEmpty()); + Urn firstResultUrn = searchResult.getEntities().get(0).getEntity(); + assertEquals( + firstResultUrn.toString(), + "urn:li:dataset:(urn:li:dataPlatform:dbt,long_tail_companions.analytics.dogs_in_movies,PROD)"); + } + /* Tests that should pass but do not yet can be added below here, with the following annotation: @Test(enabled = false) diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/indexbuilder/MappingsBuilderTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/indexbuilder/MappingsBuilderTest.java index 6df31b35fecde..8d504c562c99c 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/indexbuilder/MappingsBuilderTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/indexbuilder/MappingsBuilderTest.java @@ -21,7 +21,7 @@ public void testMappingsBuilder() { Map result = MappingsBuilder.getMappings(TestEntitySpecBuilder.getSpec()); assertEquals(result.size(), 1); Map properties = (Map) result.get("properties"); - assertEquals(properties.size(), 20); + assertEquals(properties.size(), 21); assertEquals( properties.get("urn"), ImmutableMap.of( @@ -52,6 +52,7 @@ public void testMappingsBuilder() { assertEquals(properties.get("runId"), ImmutableMap.of("type", "keyword")); assertTrue(properties.containsKey("browsePaths")); assertTrue(properties.containsKey("browsePathV2")); + assertTrue(properties.containsKey("removed")); // KEYWORD Map keyPart3Field = (Map) properties.get("keyPart3"); assertEquals(keyPart3Field.get("type"), "keyword"); diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchRequestHandlerTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchRequestHandlerTest.java index daf2ac58002e0..02c9ea800f0af 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchRequestHandlerTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchRequestHandlerTest.java @@ -614,7 +614,7 @@ public void testBrowsePathQueryFilter() { Filter filter = new Filter(); filter.setOr(conjunctiveCriterionArray); - BoolQueryBuilder test = SearchRequestHandler.getFilterQuery(filter); + BoolQueryBuilder test = SearchRequestHandler.getFilterQuery(filter, new HashMap<>()); assertEquals(test.should().size(), 1); diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java index 980b82194536e..838df98fdce9c 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/utils/ESUtilsTest.java @@ -4,6 +4,7 @@ import com.linkedin.data.template.StringArray; import com.linkedin.metadata.query.filter.Condition; import com.linkedin.metadata.query.filter.Criterion; +import java.util.HashMap; import org.opensearch.index.query.QueryBuilder; import org.testng.Assert; import org.testng.annotations.Test; @@ -21,7 +22,8 @@ public void testGetQueryBuilderFromCriterionEqualsValues() { .setCondition(Condition.EQUAL) .setValues(new StringArray(ImmutableList.of("value1"))); - QueryBuilder result = ESUtils.getQueryBuilderFromCriterion(singleValueCriterion, false); + QueryBuilder result = + ESUtils.getQueryBuilderFromCriterion(singleValueCriterion, false, new HashMap<>()); String expected = "{\n" + " \"terms\" : {\n" @@ -40,7 +42,7 @@ public void testGetQueryBuilderFromCriterionEqualsValues() { .setCondition(Condition.EQUAL) .setValues(new StringArray(ImmutableList.of("value1", "value2"))); - result = ESUtils.getQueryBuilderFromCriterion(multiValueCriterion, false); + result = ESUtils.getQueryBuilderFromCriterion(multiValueCriterion, false, new HashMap<>()); expected = "{\n" + " \"terms\" : {\n" @@ -60,7 +62,7 @@ public void testGetQueryBuilderFromCriterionEqualsValues() { .setCondition(Condition.EQUAL) .setValues(new StringArray(ImmutableList.of("value1", "value2"))); - result = ESUtils.getQueryBuilderFromCriterion(timeseriesField, true); + result = ESUtils.getQueryBuilderFromCriterion(timeseriesField, true, new HashMap<>()); expected = "{\n" + " \"terms\" : {\n" @@ -80,7 +82,8 @@ public void testGetQueryBuilderFromCriterionExists() { final Criterion singleValueCriterion = new Criterion().setField("myTestField").setCondition(Condition.EXISTS); - QueryBuilder result = ESUtils.getQueryBuilderFromCriterion(singleValueCriterion, false); + QueryBuilder result = + ESUtils.getQueryBuilderFromCriterion(singleValueCriterion, false, new HashMap<>()); String expected = "{\n" + " \"bool\" : {\n" @@ -103,7 +106,7 @@ public void testGetQueryBuilderFromCriterionExists() { final Criterion timeseriesField = new Criterion().setField("myTestField").setCondition(Condition.EXISTS); - result = ESUtils.getQueryBuilderFromCriterion(timeseriesField, true); + result = ESUtils.getQueryBuilderFromCriterion(timeseriesField, true, new HashMap<>()); expected = "{\n" + " \"bool\" : {\n" @@ -128,7 +131,8 @@ public void testGetQueryBuilderFromCriterionIsNull() { final Criterion singleValueCriterion = new Criterion().setField("myTestField").setCondition(Condition.IS_NULL); - QueryBuilder result = ESUtils.getQueryBuilderFromCriterion(singleValueCriterion, false); + QueryBuilder result = + ESUtils.getQueryBuilderFromCriterion(singleValueCriterion, false, new HashMap<>()); String expected = "{\n" + " \"bool\" : {\n" @@ -151,7 +155,7 @@ public void testGetQueryBuilderFromCriterionIsNull() { final Criterion timeseriesField = new Criterion().setField("myTestField").setCondition(Condition.IS_NULL); - result = ESUtils.getQueryBuilderFromCriterion(timeseriesField, true); + result = ESUtils.getQueryBuilderFromCriterion(timeseriesField, true, new HashMap<>()); expected = "{\n" + " \"bool\" : {\n" @@ -182,7 +186,8 @@ public void testGetQueryBuilderFromCriterionFieldToExpand() { .setValues(new StringArray(ImmutableList.of("value1"))); // Ensure that the query is expanded! - QueryBuilder result = ESUtils.getQueryBuilderFromCriterion(singleValueCriterion, false); + QueryBuilder result = + ESUtils.getQueryBuilderFromCriterion(singleValueCriterion, false, new HashMap<>()); String expected = "{\n" + " \"bool\" : {\n" @@ -220,7 +225,7 @@ public void testGetQueryBuilderFromCriterionFieldToExpand() { .setValues(new StringArray(ImmutableList.of("value1", "value2"))); // Ensure that the query is expanded without keyword. - result = ESUtils.getQueryBuilderFromCriterion(timeseriesField, true); + result = ESUtils.getQueryBuilderFromCriterion(timeseriesField, true, new HashMap<>()); expected = "{\n" + " \"bool\" : {\n" @@ -262,7 +267,8 @@ public void testGetQueryBuilderFromStructPropEqualsValue() { .setCondition(Condition.EQUAL) .setValues(new StringArray(ImmutableList.of("value1"))); - QueryBuilder result = ESUtils.getQueryBuilderFromCriterion(singleValueCriterion, false); + QueryBuilder result = + ESUtils.getQueryBuilderFromCriterion(singleValueCriterion, false, new HashMap<>()); String expected = "{\n" + " \"terms\" : {\n" @@ -281,7 +287,8 @@ public void testGetQueryBuilderFromStructPropExists() { final Criterion singleValueCriterion = new Criterion().setField("structuredProperties.ab.fgh.ten").setCondition(Condition.EXISTS); - QueryBuilder result = ESUtils.getQueryBuilderFromCriterion(singleValueCriterion, false); + QueryBuilder result = + ESUtils.getQueryBuilderFromCriterion(singleValueCriterion, false, new HashMap<>()); String expected = "{\n" + " \"bool\" : {\n" @@ -304,7 +311,7 @@ public void testGetQueryBuilderFromStructPropExists() { final Criterion timeseriesField = new Criterion().setField("myTestField").setCondition(Condition.EXISTS); - result = ESUtils.getQueryBuilderFromCriterion(timeseriesField, true); + result = ESUtils.getQueryBuilderFromCriterion(timeseriesField, true, new HashMap<>()); expected = "{\n" + " \"bool\" : {\n" diff --git a/metadata-io/src/test/java/com/linkedin/metadata/timeseries/search/TimeseriesAspectServiceTestBase.java b/metadata-io/src/test/java/com/linkedin/metadata/timeseries/search/TimeseriesAspectServiceTestBase.java index 8d7701f6d174f..23ca4a4a4247e 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/timeseries/search/TimeseriesAspectServiceTestBase.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/timeseries/search/TimeseriesAspectServiceTestBase.java @@ -485,6 +485,65 @@ public void testGetAggregatedStatsLatestStatForDay1() { _testEntityProfiles.get(_startTime + 23 * TIME_INCREMENT).getStat().toString()))); } + @Test( + groups = {"getAggregatedStats"}, + dependsOnGroups = {"upsert"}) + public void testGetAggregatedStatsLatestStatForDay1WithValues() { + // Filter is only on the urn + Criterion hasUrnCriterion = + new Criterion().setField("urn").setCondition(Condition.EQUAL).setValue(TEST_URN.toString()); + Criterion startTimeCriterion = + new Criterion() + .setField(ES_FIELD_TIMESTAMP) + .setCondition(Condition.GREATER_THAN_OR_EQUAL_TO) + .setValues(new StringArray(_startTime.toString())) + .setValue(""); + Criterion endTimeCriterion = + new Criterion() + .setField(ES_FIELD_TIMESTAMP) + .setCondition(Condition.LESS_THAN_OR_EQUAL_TO) + .setValues(new StringArray(String.valueOf(_startTime + 23 * TIME_INCREMENT))) + .setValue(""); + + Filter filter = + QueryUtils.getFilterFromCriteria( + ImmutableList.of(hasUrnCriterion, startTimeCriterion, endTimeCriterion)); + + // Aggregate on latest stat value + AggregationSpec latestStatAggregationSpec = + new AggregationSpec().setAggregationType(AggregationType.LATEST).setFieldPath("stat"); + + // Grouping bucket is only timestamp filed. + GroupingBucket timestampBucket = + new GroupingBucket() + .setKey(ES_FIELD_TIMESTAMP) + .setType(GroupingBucketType.DATE_GROUPING_BUCKET) + .setTimeWindowSize(new TimeWindowSize().setMultiple(1).setUnit(CalendarInterval.DAY)); + + GenericTable resultTable = + _elasticSearchTimeseriesAspectService.getAggregatedStats( + ENTITY_NAME, + ASPECT_NAME, + new AggregationSpec[] {latestStatAggregationSpec}, + filter, + new GroupingBucket[] {timestampBucket}); + // Validate column names + assertEquals( + resultTable.getColumnNames(), + new StringArray(ES_FIELD_TIMESTAMP, "latest_" + ES_FIELD_STAT)); + // Validate column types + assertEquals(resultTable.getColumnTypes(), new StringArray("long", "long")); + // Validate rows + assertNotNull(resultTable.getRows()); + assertEquals(resultTable.getRows().size(), 1); + assertEquals( + resultTable.getRows(), + new StringArrayArray( + new StringArray( + _startTime.toString(), + _testEntityProfiles.get(_startTime + 23 * TIME_INCREMENT).getStat().toString()))); + } + @Test( groups = {"getAggregatedStats"}, dependsOnGroups = {"upsert"}) diff --git a/metadata-io/src/test/java/io/datahubproject/test/search/SearchTestUtils.java b/metadata-io/src/test/java/io/datahubproject/test/search/SearchTestUtils.java index a22a774065852..f3689f9b5d04a 100644 --- a/metadata-io/src/test/java/io/datahubproject/test/search/SearchTestUtils.java +++ b/metadata-io/src/test/java/io/datahubproject/test/search/SearchTestUtils.java @@ -15,6 +15,7 @@ import com.linkedin.datahub.graphql.types.entitytype.EntityTypeMapper; import com.linkedin.metadata.graph.LineageDirection; import com.linkedin.metadata.query.SearchFlags; +import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.search.LineageSearchResult; import com.linkedin.metadata.search.LineageSearchService; import com.linkedin.metadata.search.ScrollResult; @@ -70,6 +71,23 @@ public static SearchResult searchAcrossEntities( facets); } + public static SearchResult searchAcrossEntities( + SearchService searchService, + String query, + @Nullable List facets, + Filter filter, + List entityNames) { + return searchService.searchAcrossEntities( + entityNames, + query, + filter, + null, + 0, + 100, + new SearchFlags().setFulltext(true).setSkipCache(true), + facets); + } + public static SearchResult searchAcrossCustomEntities( SearchService searchService, String query, List searchableEntities) { return searchService.searchAcrossEntities( diff --git a/metadata-io/src/test/resources/elasticsearch/long_tail/datasetindex_v2.json.gz b/metadata-io/src/test/resources/elasticsearch/long_tail/datasetindex_v2.json.gz index dd48fe240cdf2f2047a500840288c525bbde3e22..5a412ff4b14e0f0a4e4a3e0e371e83223091369e 100644 GIT binary patch delta 149868 zcmZsiQ+!Yt2Jig-*YCx-ne%zhoafBm zdwtiMnd1z&`wX~mM3LXV)norLt^Kx-4H%F~TMjfX}!$jLiEA1^lMd3SSD6!pQYfpT3*1KE&V<3epkAqPxZuH9BnSBOHM`ZS>(H_ zn6c?lE*=2(7gH@{$ZaW{b@SgIy`%#84r9M{m-VsyA3I~wa5Q>uhB&SUTo9r>HO3JnHnk>c;u)cHHh!BC0T4 ziA43m2?BAutu4p`^0lXziD^6YR)u-Tn9)3TDWfO68TmothJn+jzk znra9D&sgg>-na^OPf+P~hTnd_j%gy6jyZbjkL}}He5>Ztuku+MGBUI13Fqr1VH<-~ zz_lzJjcd7d-i3b>aJUjtpxk-2wq&Lz*ZoymByKA_%eOQu8E8|T!r`zT3Xn0A7@7i1C}{c&HOnQRV8ap;X8CE8@lrJGo~^!7S&=s#I0(WWCPJ- zW`RiG(|*Z7caoLt_AA!H_zg5bp{%2D*oKI^3Mc-f{@n%suPk_P@UX>>Llzc z;8|gx4b9FlpqUTg{}I%uc}TB;L$h;dd_@v;W^mOhlh0HzXa9^xy(e&SWPQ+{xk}td zzFR!ureUP?Q+9*Xxf5%tGcWzO_7!eJ7dplq|4XBVuMBvJkA5OYluOmwYz9R1#r{@m zKI|&V#o<;7o7n0R!F%Y8ot5Gzg)r&6BQrflkmyNh$x7n`b3j5S{!9-X79 zC5P=a-x}js4YyOlRdA+-1sjR3yQVVJ<3Q^W?D?*?di)^0aFzJ|Sg zUsT&Q)gb}E>D(bWq}RcgPzn6{P*9=af~sW)Bz3~jr!sGeU}_%;UMiACMM|=_SkiaS z)t$f`bo7IVZ1&31m)<=OsAdPP^!6Y2Rtwp(metY0EbvTs9LL&hB-{ZP@zREwe}&$% zz5>}h`vPB<<#IJJ2pbjeI9?^0{uO#6y}JhX@(BUq>F)?X%MgBRP}t4F%c6&f)vf!o zPW`)HN6L^We|NyJtvxCbJCmKhy;qByfP?7aC}9z669;uWQExLmcXW`wV^cWa#Tx$b zoT@MvBQnHJ)+jOzRyG%!-?tr>{du&m@UMd8xyKu}3MFkWxAd~MU>hoqs%ARHOvH8G z>zsgdC-f4p(45o*4N~O5Z?Zl3c>2Q9Ks#l!Z@rL3Br2P(2Xe0@g=0H&Qtgk4F8xVr z(+E3c1j<@rY*Umc_>#4ki7pvI4MZsP#f5?T-=M`JM@0(V1qffcyj*h;H^`C>9q~I_ z@b=Gq^t3XvU}LyFi4D!zL!=tEdvMM~|D*uZ#W}XQy3X}K6zuGku&wkKSLCh17Ep~; zCE6+&h;xc!GL1ID5A{Lg3BQ00PTF$$xfUg3{0?WrTWgCEyo12CUpu?!Y)ti#C|E1dtMJMvY;sf=DO2CrrUok)UN zGDSUPq7v;)S_xswa53WH7(VgBvvZlaccK#8dXANijzLig;m84M@PMap--i&FsJ}J6 z_J*S~%jI|iOuw)& zq^zfeE{hLp2CN}|j}!8haYS)nYMOl9aRzZvXOsJEmRVpQNj%aRqq6;RZl+^rPwHFl z0Tq#4+fjB$ETzt;-KL!JiEQ~()UutnRakZKmz`D>EQ*P$J+l-flk_23q!AeYs1$`r z#yhpAKxHi_aL>!pOrBYQAUZ&q{%#xoF~{&}ra0A>6k6ettTfD?N>cIh7(VId1$k6B zDU&NaL4Io5n~0y9<~zY;I}yJOxT&w-rwR6X$R^l`Q@0c%25<7~YWvp4euyJOJg4Ju zJ8s{1-P0bNtM}{6B6-TS<$k*6xD?cK<~S`oVZAuMJ(s_ zOI#`&?`vIf~B3r7wGAl+h*8VBSKNahTyxl+jr-iKc3*T6we7bpfm%m5KKf6Ou zq?~^^s#DHNg?|QtP25cGk2=bpFS<|CSuAV(Jm92u569 z6vDT@GK!SZy+FxqnS=Hq&OA>cys1(|XMY3iOV_b-H|jAmgVbf-_B&ETkPK-YUNFuE z_FTB{=eq{b3zaN;#VzKg;P-i(^XkuB{ruui)a9e?^M1Xdm|MC!lM?3fd2(;kKz`b) z>CP1|i4fcAa_hQR68T*c)1p)1I^(B$+cnd~h)Y0?QytMxJaT4WW9U4);VtPkb*BtHj)cJ?uI4I? zQ%dC=au=e+Wz}B9pD&zfq<%jj|M+=cgN)SobR#)8E{Y-lDl-wRF*p8*{9wIMR#)yL zZj`)$b2rQrzxID4g1ogpxz}<`g_{HbJhDnobkDzawFX|+eC|Ip$E@TC)^Q)BX0Yq z&Qr5?i`omVUp=0Te-NH`Pv*OQAVf_)6qUyKV4Jx5%JiA~5jNvNb$raUQ}t_!eK6m_ zmeDnSNs=9qPj>)e-|?m6mfR!Z&no~-o^&BZpCqxQB+!P7F?Qrfh}6^D>N0ZIL%+Qfa^sdq;1- z0aZUMvQ9+%i9eO1B*b2V5V=Dq+Guvd(;?&^Qwwi{r77=yc)^5&9z{{ei3}!kj;yUw z-MWYE;WJYVcU=REjRk~!P9AQ~PE`JgYsknWUjj#%!yM@2A{^?8Z2zqTiM^904-$$j zG2*OxkW%Pg?~T70Wy!Y80i&KF8}g;Vs19iTBzL$FmpSMHt=mKCcYR!`4kgLXRcm!O zC#Ao=PBZIn%6or$Y$jY_ySQEsV;*QVjByV?$ za@Gbn)w@3&{}<3P_HBNmBljhYP{AtdnL^mKqZxH)dh-}Q#dR^3^#nSR!KozG`9fBc z$C5f9ulIXOOq)=8PHHWFWc{Tpotz3(lT{o=NtiiyCYuyHl*~g1e;?Q%g!75N#p>^q z=xeS{<;!-!et=Q?A~wxh6tsC0lBrIfJmVVFBw`Y1oh`OGv z4!hm;3B%;o(Dl1TL^sAJVt(OsWLX0U0@9j$)%oep-R(Re zADRf~)hyTGZN$Jn5R1I~pY3&c$q)_eHGtJk;MZhzMknsZg4&q9B?D)Ivz;BSxJot> z+0l9sN`^o8S9kw#S^_I~_is}w@pwK3w!N03Y}8X!&STn*x(40>f$_zoZ(z8{??mLa zsV2g?+QgJ%UtAl(rWJL9OZ@}r@E38kYIMnZ2W1NXIq?Q<;?<^1KOH--=)#Q$BH)?O z$)0!tx&D_=v`DO2D;C<7lXM~;YJJgdF&`Mk0`~N=MlSTx?;78~o5Gp;P45oL#Hhi} zscL>aM@8;rEE}u~NUVN|E`y#lL3@|hq3#ln*{H>cnE8|9fOd33X^OOaNQNFQaCp}Yu znz#w5vkcNA_Y(<8)0Qu9Q6{8Gdo2YIE8PCO+XBsifK7?QAGr;a>4~A5v5s6>bW5Sf z3Ti+#?k5k2>fEOtPzUm?AUZzclgCQ@t)5d6aV3R^nr&<&_B6xi?DIhe#=q+GNORP8 z|KZ~&7yGrMr&$d`>Z8u!&}rMB)Y^3e zieM0Tbr>91gJfNy20VaVBJ<_LHf~O5D-9rH)B@VsO{06&Fg;iC8aJXdUbF*s zYeP!mVxgx!ZWeh|TshH9M+p;kMlW>&|EjMRpGN*Zg9}1O%7Ut02(b&(C0OFpXZb;S z8<*>_O=@V`BRy?i$!#YpF?>~I&z+EnH^GqA#AGG`-3ofZQ3>e0tVrAir7~Rqr7|O+ zRHl~hVMFdtfDIJNM4O8q-3+VXDX?X!di;d#Gw~AeKJqA?z;9P2F9xVf+b@HjIiXTX0awHJwxj+ek-s9QNh2 zb+J2igEan9WUBL^y!mIIK!;U{#yQ`SH(eJ<5$SK*@U|y z7(O9eA6L);`8;Gqvru}F&x6TzU&IAOTH^H@XR#9cS4$?KE}*+y=Ws>bG?oj?E=%FU zEzcNW1>(zwjE4Vy(?>=RKbUR~rS1A4m2gh?Vyk^@+N^_aG@D@~-m5CL-$X5~VwhXI&mN+5W#finYDaxrc?J-p1*L>I|#~!^vqr;*ZeZ zXusn)dH$VhC&l|0tkqS8X4&>_f;;Lg&-MflF9P-z07`^Uz#iD@cdT9uZ-VOPyxrj9L-?qBe4C#On0|BeoYAf z6kimulhhT1F}qiRzduX&@DQW*M8*|#L>Qh}tX<7Y*5v)ph1P|%!`nJH_JSI`S$}1U z$C|`BLT%iU3>|zeVUp)l4-jUZ2*laNb9CCwVO2V#$~HWfdZViKbJC)~?nF%r;T*kl zsrnHg=w04B*;xp*ft+4tUwY~#$mw0(z1<7#PK+zpQ&Ys-hqxrfeeWtGqf$fD3#ik5 zwWfF$3e?HDclsBVYSuyyL%o|K6WB{XgF)3?)NN*mwBK|f&EKpDPz^KR@}1-Gh!uc5 zDE@yQbm~73+6MBVTi(~5nY>k5)&?Id^0EjU$e00c3&A7z9q9L(S*|rurU@+0ET*6! zG7ZXE#58u2=_E(796zWfevT&np5@$5Jy~gpv%+`eNv&)nCx&Oo!#Ac$@^#CD zH|eL9>y3taVEP?mJL8g-Ke~v8g7a{+ZAK`-wa_ zalWxdW^TCmVW&+q98zlvfV`5}ROSPbZ2d>}YwlYRVzumK-co=QXDtbxY@s~vVRcs= zV3G1_H|R-}<&85pP#^G{g(15Ky$_pgPYrVDl8?^~zUg&n@QrLtS|sFAAdZ4F${@TW z++=+L8*@ShX;SG5+Lh5(dqqJ>rfy6KPA4S~w+qDZ-~MCx77)YV_ECWtKG9k5PFmQP zrOzWV4YfI41t;<0x1vF z4A6b^FJy_)Q6b@$2>C6fHmlf?r-8egyDd2TokEvAvXmp*Pq6!-u>NZP31*)SQu+cE zH>3PLT_^b?KOnW%3VKt!-wnCEuYo^3>_my=>ps-)S@n10QY5B&#(*r%7}aQ@F%>+a z2DSC1_!MdoKM+1mC|!MdK%8z+v9+-(7nElY63xgv)xe{;xMN#TI+UMKNmAHsJ+ulnd?;)uNirw%z(K?Z4d_7LcUht@w1bm@&%rEGrwHp1oQd@<&{n}T%q3DG>qQ{Q9>)pyTWrE# zXK*p_Q8@j4p(FJ?kpBFkkLR%b)BY-wuYKB%bK)C!X(~rM1+^KHQdAsbfo(4~h8{9< z|E7L_qjUx^W=L1w)}a(vG|VIG!GZTW%cDtU=nPHJ5e>2cv8nnA4~)|BMa5xe$dM{P zVZq70^i!Pp<}wzG8WO4fLjGl}SSD%Ii~a+r`zDaKeE4KPwq-5Rz)~AKkiVSUY4bx# z62fUF-0qrqM4&eL%7e(zVF(d}-@6FhQH_Bh150kw z4!rH&z#UX*WDt5qzYbr4i!#prsWU{kjWUkUECP#XmYLYBI>x&b@tTMB90YOPLs~wc z(*T5?F{)cbFq1&=ms&YwHfVNL;i{2mjCTp=?zj6X06#cAw-Y|vr16a-1?9;IAXr5D ziPy0u>LUh!cY#W2TUaTch9IueyP7uN1Fq(ffLB5-_eo7g6IRP^G~Wxv)ti0hPGY?`)sOq;j`EQ83|Qqm^OKON)a@j(6h3K(G)P7`5#9Y)wvmDQT-yu}7(uSutj2({ z5C}CTR5_9pIMaMO6YJl3^D=*ywtOk_kA=Ymfe^J&W1pvURb|w&Du|;nw$*O*fzAh1 zKu?9ZRuZ>p6N!Hyxpj>I-qdaph7i-)@({yQV~P^lw%#JWW^(tnp1-`#u3 z=WBCfF%$Zdm$6o@) z6?eJQ32Xsw^HS5+m4KM4h27%+|fJZmYB&G*gOl7P~ZrVqc{!z)Tb zKYDOaEy%aGhoPEJdAvVjB$N}tHLFa=Q(-IGr;OE!`zP;eP~;Wq_yW9syx9+eq=4=B zT;QF!rvt@hy_c#V6skS&A1(7WJ(X_0rcm6l;XL6y-y})~eUS#Mi%uf;R z3btTX6@65dp;^g~z9~#`O|Rx>cdH%>gIS&(qUDLrk6*`^;%NepSaLFeTG<`4^`?1!&Tck=|YyUp~Y*m>V+D;H7Pmmr>;|feZ!K|QSZ(!8ZfY7tJ;Z9 z7iE8TlqIFbkiTty;R~5*OdT6Tgt=Kmf#ZacUP0|zMb{p-h`6Wo=U6t zU>SD34On-Ky!rEs7n6pi|EHt{>?`7#p!{$kf1BQHPi?FV0-hz%)ppRbjD=q;uwma~ z%XQ)PrugB7*5MP?i(#o8k{%oQ?q|-I-r1hz!q#GANI#3+uC|yjm&B)#`EMM2Aiw0& zp)QvpBF6fG$rcU;rR+_*#_lkc=tDzWz@Q;p|19)vw5cv*>z^b4@%!o(@HXC1ISOkj z>@u1kq%wWs(@X+Hc~(~b#r!d_@vi(Mm7Y(KyfZr0c)y%#CcAKzN65STGp@G&l4>P$ z33~sLz+q4P#vt`gBlUB25nW`|;)1S#>R6wAr!Wc`D-?lHfejpyGiiin4a!fOEx0H5 zQDO>ADVus9hBAspPSPHG8V+6hT*hL)qW-E9mmom_f+-rvw0aJe4q$oz*nNE6dE7ZY z-3@77o8+A?OVy!iq#`-+n9mjbJeW&m6#Te#rk*|6efvky2*(*NOm3h$VtPU)hDF3E zTYLY3@pMmKF+cEccS~Gr0`nJ7YH_q|!IPrUf)?L9R?e~8%hcb}>5j5SEKhGol+YmtVO7+eJ-N9wWb%lqNm>K!e52gV zTjp20rJfOZTUL7z%zG0_w8O874LjLL`^XCm&I#An;kG z;d$TOPOH4Z%1-OtL#%159KkoxxqWV~;=4eo7-C?op?t`IUC!`s9+ zlfjs!+R5o?r_S60og3?bF55*q_r>K;H$1q=MzaB=WhZ4l0v6ys8Sy&qbLKlkN*=w# zjmX$aS5NI|dZ(z#17E}6maCwfo9I0ao%R|9R(rfH+2ZsyZVTJm#I{FHx6-FSTH23C zt{p(7J_P|;f&R32Qrwbcm*C8`bYHFP2hV|0(8_%I(cK@5yCwNdM~r3nke-0j%j?Ae zN78*(?>OhJeY)+!b;v*WH*hc?!$o$>x|irQ{gbj6)KdYP#I#ofLp3{D@L2UB;wD{K3KQ-EFej$!m~Pd?=G` zx#5P0y*9Pr^6M*X1UPgKohC6;P26+Ac)3_Cm#1p?+73$9)O9mg7^+W5;9qKq4IjBF zwqb|kc&~CV+eS}vz1I&9>PLFoM_Y1^A_wVb%a}iU2fQ zQs*Z!M5JOev)N-7}VOeGNmPv%OWaWa$!tN3VLlV(y>y)#Hb;Yb4{91w5a74Z~Et zI}lMb$*53cU(Gm-hSXegyxRHLaaVEhe!=c}j@j;xbvZF`azO66`|1O@dV5{zZ{$|1 zbB;l~0DF2h7JS_;ovrP+m-Y(wlNuT&OYTTVB}lSYr|4VxcW2&j4z>5kvn6NFF5>fg zIjF8zQu$-Dx7aWD4vv7Y-+^9s`*Q-Ho3kenlghbyV&&=U=hdmbnf2YJi?5R+L|1>q zphiY^>Wblt=u(O!s{p{LMNd?GYG&Z+biKM_w7a?@yxcGjo_#nyy?nHCeRa4*MI-D8 zojc&Ky?;a8FwoO8(5JUQX+lQY+*o*E_uM^1MMDeQDEYSKRf2_eyu14r^X9Gb^IKa- zmu~CW+`mk?gyR9!M4vv+68F?p4_8me6aCF9ix6GiUF)ZmZ(hLPrIMx8qyAxNcUQ0G zHiK*f16@7ct3*BW)Cew8U+Um?PGFHgWbRO9$9OGY#`^{8TDVB+Lu z;GKR%f44R?bnQ&_^p~}dcPs;8#89-*_$40;^gZ9jnFYxCf*I5WU5(D|JMwf2a!Ikh39Q){#8ntLR24C8x>zKM$$;MvLJ zn!40H?A=1H(4J30$C&ceKU8u_fAiYt?(69l8(vO$GwBp=pYog`!`t0MhG##eIHhs4 zU%NK8Hg|CQwsY?z9nlLZ@_cxhlCo)ib$WPvn2;0~N)9|w)5kZbmS`+#xF0{-dHQ+Y z31z!RUW0da3v{;p_Vn@gbZzU<&dK$9Lei+U&^`g+u4Fj?ABhdiN4ijwhRK_O`eQ60)w7A*x4=pgW+&$U=5}f@HWsj9^V!%ICpA%~)VGP&uKdS{Mt;z? z8CxM^py#~XGB_lB0WUCByzvK_D_sZGS=QK2`b}8}6|kfIb=%7*U%I$Z47QNLG#9g7 zfpmE;N$#{q;XXf1@5Zi~Kkt5mvZ!ppT4cSb)#$2=&f}udqF^oRVTKpHa@ZL9kv>PD zkN_+Z%1+-|$4)EnF6DR*KHoH&>oc7yhqmu0$rF*VP91N1aqAJUAMfjZg1Zhr2+ka8 zf3ljUJZ#n6R(uA~)V~syI?4Ft;YPWar^DR1buO1Xcdk>kdz&0kS}s!BCMQxjPw}_? zGTP!VCZOM>=^E3X+YnJ|sPxXtOq(^?0x^VrH^C$tG_J|2B?VZkef8pAd z2S*U5aznmB(K0H03Dp>;M^4i!zk6vN8)m z9$a5Sl$>HlwyblI9KG1)S(4aCD1)9sH?hpU&$XA!j9rYpMHj(>8l5wbqX5Uo^y8{Q z#E1F6cWdt6Y5iyG6*ODMH}@6D0}6m0^FZk^)R@@xrsgwxZeVR-YNp#!kgeK?ySVnb z^_;=4e2__Cfnbs77+c);DU|Qq>6GB_M;>0?OpcISRvwr06wm4|tqJ^8E11Q|CU^Ry z3(TmW+1DjLtigc+i5dp)Xfi9j9ZRR7ryUh{mAEr@4E2EC>y)ix z&hg}-lxKqQd&Q^K>)hM(^U33yOw z1j5WSr9kS&))Is(;aQjpS4wWaExuvNE3C2Dr|j?BU9_d=8QyAx1Jw6*PKinLpYL#- zu(uaIeYoS^i$z)DY4(o0?Qnqb=A>inuZ;YT*Ezd5SV%*=l*x(NR;^b_ixl;3pfYe< zxiRl8t(XRuLAoVeeGI}Rb{&yb1>x!k`KY3)suH_Qqi<)#TTD)jR0hYEvaFOJMv6r# zb;AiG7kV^EP|)NbJ}yw3X~DgWG#yMn4+b%J=z?uT0>k~lxWWzRjf|Z6A4i4T zqP+}bNmIIV$_4H|ov#3IxflC+_G^bw_|&NmOYXwczCgv5o3Hv-{Bva&`HhyZ>@Mr; zJ)uHD_qk_fYAY%kj?u##B~rg5oQ||?rd?YqqC%IIs1k&RpU{-WHUcq(Gj zZJpgTp z5I$G;U#)GO-a7&I)-II{U7$j)x?7!M$$&QTWPvdAv)w{n)*eIQfWqD-3W1q3vM1Bm zpd@Q`d0KJbJhH|mGA*`|KteL)blo|EeZ?`S1+T3kEd{(0oVy*r*z8;R;s&L!w|t$q zWVYK+llp4ECDsjD==VIrxaCi*3#NgOZfa=8N1zM0c^UYGUa%OfCPIg+iHs+g4&bqF z)=Jpa+somV-501vL#>eN^Zd0jC~{HBz55921K^a$pv%xpa9 z2uOTZg#d&vc(v!+?>`>F1Eo5@=kIj0=C8HRL?O6cGLinu_FiZ)Jo4hFS#DL1g>kc+ zCk>p4P6Mi$QEktd+Yj}5m6ml!r#V*=wZt|kTS;Pa-0WQLBw3tRTPY7?xvW-OXGVw| z`x`oE#FF$JO9;1<2Ej?N%5O>a1)RjxRy+>G1_6GG+iRbd*D*9|OJt&)PRJ9Z342-m zRTRPW`@H(fA$}z8DbdNhxH}@F;Z{H4Jk5{KR;%=-g0i;(FS+FI)tJiLrLmhIAKL7d zqXv~w8j)BC5jVR?6L|xn#{RqQW=zI6@rV))q7_6H4=>3G>+J|GMV~`f*ZxGR#qNGo zg8)3{XEbV!_Z_R=;%4Wnrg{|eH>7!R@Hf-PqKK)Q=wOZaCn9O{GF&dkkFrF1hIjKd zXio0sDeK$cmxoOzx5ql(540C>%?9!@)6KzQL=K!hU_@8#FVikVn}TO638jG0O5~yf zvvQ4BURyn4eY>wxqDy^;a>qO(z~X+~21wq>ARmN#*tQo@`u@8#(QG(tX{W_q7=<+N zufHS(+<(14P$cR(r}#<4A}4Q>Y6RhZxNWp<7lxlrBOw2?<4S$2`fFfBN#07UKl!am z;q)`+1@GR$Sva0hgO+a@2PbyS zSA)e7s-R<&l|p|r2`LooX;i#!SKKJu=E#kIhHmq?HrRlPb{$>9*ZcCZrIq(%?aqIG zqF2gcO0P9DnAvFOGA6j8H<5R77I0%qU+SMsJsO)MRIL9n8e>o-)A-CoqQw=6S+ME+ z#hEoYMNz$gQ+7y!>$F376-&>_yo0Q2?*A zRFOBvNxu4J$4Tzcgdh2?7TZ_rZ9J;$Z5qBcE;m_}0Vs#Y97T$}%9sf{1$v(jkb7eX zm|$PrZ^#|73?gdL4%Y%_pA)-#BN-20WL32$Zjzum$SylB4Zo;;ix z`0@OfjiUYWK~sLKp^)qu-=Y`U--sZKMNa17sPv<1PDArEAo3TxaE$V2YlFEmrz&PH zo_7?Y=2c=%vBV$uW0vkyK>&1gpEB|_nRqy1$=vb2-)#qrBk8z#es6t+_SJmn86}8+ z{yBR_?cS(#^rhU>--^`!j7Vn5)tV5Ng9;K#EzWfp`@4b^f+MR6e67(eP z_Vg4N7_;Bx;>4O(p$$gk%WULl1$pr| zG=&B7Z9aIxkiP!}F_EM^ha&7W*kP`ybat9BDvKP}!bKwpP^Gs+uY}Oq-u;1>k5WIV zoH!IcGY@-yT(+su4r%wuhTj<@h4%a#8Im#ms+ia2lz91k?V^S$yUMw3jm+JCK{<6L zF>8Zs8Grup2rUMIlpypNhI}KC^T6Tz++s)^YGwFd-GrM1aT#R#H==v35(ZiVJmSTT zMi^}+Yh6|&;4d~o)nA(Hdu`XiY6$o)g)yapB=fBqQiWSqqHGuu_ z{@KgcNVDp%f)Cvl8PZ9loE)j1w8K0TgMeIS6+QhG6WGgwsUNVk1lJF5zsU`8uasd9 zsoix64O6r9$IPM$mBTKQ5T#|3nZ6#-R12FaCM;l=@(FwO+LoR!Of~%Rx&nsC?0W_+ z1NB{7IRyGU4~I}t8M_FaN|ZANJZ4D|f`@5(?!o5uqaOK?19z}djr*TE1o0{UbJM=LL@Q^gC$Fo-A*A|;WE$w_>gdd&X*rrz*{Ymb`N9LL751?t` z6yOc7S~7XqG&aHynRlp#e@EdY45_03i$WH1JQ&LZ3*}W*%wc#`bKC#pCdQg}Y$g0!0igAgL0eN#P%LN2ei{3)dKQWpRrs*e9Rhp7_wMOt!6gFv& zEz}n+pz9y;km+<>>4Rs(19yCf2wV61Ml{V~^5(y*#$gHsL0T2n+5RbJBvDxG+NH1~ zbrn+*AImdY1zmR*bX{aqjHWM2sP=@a=w5c9@2&o=W!Z%k_|N*ygedz03ld=+k9b9p zs|1r-Os%|)O{iPf;X#y@+xx==`9p&m4KuJlcm?CzYdmPEU6qTNyX~W*scq@lMFmip z{jrg?>*{bynDDFt^~aiAQts@{$Y-|uWoa(SG6Y!0lPqw{^cTzlHV6D%g&eRt-;P=J zQT6%QYbws;_!%luY3YT4l*UnrmMp=YZQ4>>@c!23K^*F|$&{oQ_RVsS7uD}`*Am~~ z)rEt87)N7DnfhfiWjVQ4`PcbaPHKr4rcr6m&D;a`Y}|Y;kSY};=RK~;qfe__Ime(hxtPdNx`+9C9-2N{NA;)@%|r}b8sbNp=<7WVIZ z*v1HtChqGr6b8bw7AP#fR633YjTpN$d?^Df=pW(mdoDFr15y(?X5k~}$^1(f?r1ys zwOjT!IEL7mKSRKJRg94I;2^DyOM@3>^e;^oGriBM@y- zlsj=_)V3roKtxRE1qv@i-F9rx1@kaO<(wd9vw;a5b5(X8{=CF>OKHTT7MQh30*tN> zXYZGkg%y#XxyQLXjCqe?1XvoI9B_tJ@GJpV7eZWlO}J|T)QGG@vTv05H2(D%9K8_| z;q**KHWSl9#CyfkzlKgMk?dR|jc>DX*JX@CmPn<}0slyC%Mgiq8Z3FjMhQhcrAQ7g z@^}*_(`5WU|C?3OpA~gNaO#bU6mn`6`4~))U$iA)^7Wnbvr@d=S@G%9)%xJH+_j)k zdDHp5h?BhRN2MOAY_ReamxTxX%=|(yqT(K5IQaH&BYgizAIQ(bS8PilceuGU;b>t{ zN}r<(04Od-xzx3DbOqlO-(13%?wb%zgYgWXUBm^ivNi|Sp6cfGF2 z8~<_`5B2|OK;Wy47JgN>CHMxZrfzb^B=4Z)vN;z`L@SYu#!perg4aVjhEqhOvze|m zjnUYIv^A^&Z@V8GVP%5kBSg9dmeQ|hXh`SM2q5;b#qMnnY!C~o=?2s#3-8eim zjcyY-VQL#?Muk@rK%6kMh>Bk@Kb1i`{^Rd348FBaK4Bji>dLtsSw>-`>?SZ*45-X6 zz*2}}4fQ>>(G8?++aH8d(P3`aWq^xSu?Qh)7V#EGsaE|aK(2ao&B-&(GZYr4wQe}A zG(i;VSk0(xlt8nt>Z3rwuP7#%uli*XId;$s+hS6fuD{Dr3qkDEL6Bi+(#dAI?je2w z#voc9(exd|1fN;LJD)NHV6E^A4&RrjL$b|%&FKDh*s`-P_T*A`02#wBn~mJ321V_8Ldk^{>~ej;vh z4{SdOZ;kvNtrmeu;@yQ>aX1^jSb1#z^g6eIQY+YMQb#uAo~ce(+Zp)p{(u(e|6Vtq zV5hWY12d{=Iy|3--|Sz~ii}wOs++x|P#>L$dfR3!DfDNFqm<4KVHT>WKLD*nwY=6h zt$g*ufW}o#YUk>Kv2h42AI$CbN_AO4=)^CnDBpyDLm-F-$^se zJaV8v6=Gbxhz_di>-rxHlQ&MT2e22Tk~j^FM7MjR~~w5q9OrOc%oJ zC2@VH(Sf#Bz(tMVrWcC|r?N)#;DIx_|E0x)0Grs@kbxS_3U|z}UC4==SrRFYWl26=uC=S_cbN@TA+56*6MY)^iFN^j?6-Pi z6<2!ySyC*6<-VOz)D;v9%pn19u)1Ytap*8TcllJ2^y)?8<8ofBYElPyi1tT+@#zKc zCfy}HRmo|f@j|S1md-RWF0h69y58_qnMsVu3z-7HJwyAK-fKd+wTF7|pioRRIGsZ> z&Z|R(9-Hg3Hh-1y1+@$do6meO8V8hB2Z5n-#rSln_C_$*RlCp8rO!eW$@fnR6UtgiqPF-!?LWv7Y*;gn0eH=pjx-%o~XOoBrP=b zMu--2m0!5YCSO@b;!6T?z_=@7d8_DW#8*+}b82P8@s4{D5yL~76hf}zdSF;})1}YB zhOx~Cq8J`BnJ_g-5(K|K_i0Kw$&SpX&p9s)!zsgbH4v{q|%>GA`PY`qS{eUt+2_zFp60e z$)Yl|cpSH;1fykU?khghTzlWq2$!XqyJUj)*94A5ME({>{R|fSzEy&W#uwAzI7da)1r$_PkPa{mo9Dtk4y(AzAxC{Y9RB;2 z%pyoXtDKAFuq%o%JmN*J81LkS|BtP=jB302qJ@LIySuv#}+ac_&2;sr{9LgnW9-}lSA*8P-~HNVW7OwKubpM7Rd%o@qW#wZ=8@iP*{)J_$f z{Ficw9J{-4gfQ5yr#?b8FqvWzy@ZVQfG?lTHc=>=80-6q{<$w+)Cl9!NZ_4IO7dR)# z?{bQ#!bgUtD);cxA%$p$cP$zm+NZmXS*27-5a=J8eOouC$aXst&}?i7r$msZd&ifs zQkzoSy=C9@o*3ij6XaiaDZ+t9e?79Mv;PS3Gf?Tg%+=>T?aY6TVY$YC2T2+|kGeKD zfWK-Y54^xUuZB|1-E}3HaPs^`H7F%zfbVivf6J4JQ$=F&)sSETWs=T*WX^}Dif`Gz z?l_57R&w!Q)bx)$RUZDe>;J1Yo4qBQJey(RRXqCD|5Pw`vN93Ff|8sZwmZI*4kZ^f z%3m3fnNOfrnTqJakKNoKE4e?GaKF(n6)0{7-fz$|LSZ=I^Y|1iU;Y0w*Mz{F`ei$CINZJ~0WKQJ152%rTMF!j=(gLyp(uXEOKRxQlKqevh43;rSAfJ4{gT zZ79^)JSB^FLbLDI(OITLcpAyjfy|EC0_?{;`wQ|6#6AZBoZp;g9UVk0;#4^c4hV4O zdyp@@%R^Ez@=C{-2pxE3EfY!x$JQ@7j2R+o&RA4CHg`)Bk38uRQWMB&US%Yl^#D_( z-OIV-a8|b?!ej~}Oj;nqWQqeKOtQkEb09Qk>+Za!jFqJJTuJg2x<-YgEl?GO#?))V zjN`R#>^buf%RF?Ejt7*=@=-O8Nj8K>vka5WtLx~fW;|bh`V??kd$HtuP)=FrDY6=R zq?7Z0{ah?q9FA>?Ru7XhB$Xh~ySdO8XU1a$vU|c+yKKY-#~u?w&$TiT$zmY z2O$b6{3i+xQPM$1&WmY|8~(+-bL%#|z0PF(cxfsmyPz0Z9G^AJH~D^K>u8*7 ze7mv3x~+qq*WTrx3cBUw*xIL)~*w%eFk4&06k9gOvYL>d1N^dcek&sLjjtNmdm7&AmT`XM^!2r5RN^qNM3Y`NB0+C$gyEf{JrxhL znz{-^H-J^C&{`akR5lROnB7#b%UBFT8UvWABt;mNXvtExnC2MtN|7tXG$Ir#s}wLh zE=3)*hRmC7OZDvLvjy8UuW89OV<9)+5{mKHOs>_ndN&j;tacAUb>kV07uhy+Kup8u z%L+kd9D~@`>?uYyWqiD{>a$hErS*&?K>NGkWD<}V)-%dPgODPPR;(p^no&&^nZ~b4 zqA11oX)&#gZ>{lV5nTBm0r(+x&Wy9!+f^7)<%Y-F1*h~eI1PJsHAsxsp0_DVIA7ZmcBft1_$kSIfQC~6$);bO9m=R-SSNZ)O(UNy@mE_ z5k|6`{rKxywwGy_Yl%T+I?CMKmL!J?0Gl}%+nGqruxQa@>GPlms#ta@VL9rn|7W`= ziuqXI%UZ1vmPJ0#?{5D@oux7OSo;>ub#SIf}2Cy~rbK$%HVy zx1)38-Li4eut#93RN)jAj!3{J4;InL^W6g6K{P3#Kn60q02s=<;w_Yt15u$atB(8qL4UUi&J52 zk*r|+ezFUAA|bfb-rco8{Z0XS+ptgFI<(xXaw+s-(6b72M(s1FVA942T6?<+OH6Ys zI{{-OO!nqib>5@Fi9A;$0aaoZV5=LGKPrWfn#oC4JQz3F8G@W?-l2qT6LE@3i={kD zKSR1v>lWMQLjgdivW8wMdea04Jw;-bv>7ZNT}C8ZBhumH|I*(<0-d$Rm8L|NATa@XP+BR`K)t8l*;+!)!PVVL$U&10EnP?VQB5_Lvv!_y4AzbcPX|!?| zZrrdabaamTEEg+LlFc>A;M#L!itw<|LIrKSijmQ{K3>~P&`tl z6`V?s%4Sn6oMh;Kr68PMW>8sfX@(`-|0=6q9(bZfA{uG>-g(7OObQgShR#-sUFjj| z5=q>JF=GUY#4n*P1Lm`zrq|)Xqp=I?40H^}HS2Ly>)2SmlZd*#sd?*H3Yg zpeHRaW0!BxaE#~$-IUrlvyy1s_TTz90W?h^(ez!^-+_gEYMM0rNl}&8HLu{o(0ZpT zi$Y*0PHTWU8j*CO7zmK_3o^zzx89AVH7Tv|I7^463x0^c?$qF^_6|t+T+yM^mdEn; zoU!>h6_w*Po)agJcGsBs3pOC8874kT(HJ~ylJklEDMx+vjw1MeF zowv;SniY6&q3Oo7&MR8N1A?ku09H!`Wnc1;5&4g@LjXLs`0U)aY2A!92K3CpUxqlU z6Eq}KbxVubUK)~q#3gFM!oTStHanPn@`YlIg$ODgP4TS(%9V}a(nx-Czs_A6!@ODA zh(rx1*{G2T5g<#K)BU%Q->xZ5y!~r04QU*@hBpfeI3m-zMG zyRgNSsXzrfB=tL?t$hi61xTe6QWo%CQzi7~`PqftjoV+WzZ}=@+u%6L(T*z`+aw#} zb3A|%sjX<=2ne6jR!aI7FJXeo$>Yzfz?K(@p3|)^P+s!L2;k=Q#3)x3x|9!_*H+8< zr8(9@CrH(lx`b+TRIa%?<>rp!P*F^ZPKMzomq`FM5DwDDY|tm!vVTrvt0md$zl_2x zTcG8m)UtkM&lq~&jr_qR3X|b|@z?Rk)1#U~>rt+5RMyxgx{#Zr0a52`RMloci`+!G zglBhkd){n9ZAA5d%Bg#5axD;n4`ea1%SQI;(MBaQzgnV@3=)CkReXO;BPUuAjs#$P z3;mxb@kQUE2^f^y*$nagUs?YDr^@3@F9p8R;+Zl0eaz1>v5`&?II5eUgTZmfG4mvmunGYcpHCE^9lS|D6*g8Cz{kY7zxdSvV(X6v9~ zhh&In+3!o!G;_fS7TgVrtaravjdaNkrb>|5RVnR=1is(9EzdE>pw-!6{uJRW7Zce~ zjvz}L`1)8LEN?_LR2%J3cFe1&7B^lqjySD%kUnzclwnVuv6AhxZ`4%Ubtk7Av=f^jogWN^xr}2M>i_7rF0*bs z0Do9cVLFl`RPg<;j`FgTXDC6{esr+ijVDo_qS!~|OoxeLXyZ69Xwr9Kl)iVX(CzfC zjy~D13fuXy?rkn4(mz?2IC9O8gU5p9o+Fs2Jj&dn0&i$Xo&=-CWR-kIsYKG9^mr{= zOT!VZXhdhryBRvoUcc8~>lU5|Z!hR?gf>|-!gdeZSbRlEAzNSC98+8< z6)(LIWh!o3lVK1Oi8OiM!+<%Hh=xFEgiYh=@)kGHF^BP^ZR4exIP>(FQ5TkIQL0c# zyXQM}Bu!IG9`CR*O_<$%7olT-BaZ2tjB+yc)5u1@>6}QISQka*<)R&HmBVO*pOEBt4ngY%YOvbHtM`0uNW8quDYj=^gg5Ij zP}Jv}(vk)$#i81Zt7bC#P$Op3peAnq9EnQZU~wn<&~*-F(({4eWUr4n6-OiD(+P+} zq>7BnU|p^rcgqCGC7j{AO3W*lTe`zK0I)+MR7ff#TN2U-nCtFFQa7AJxomFlR2h#C z3UYU`ZOQ;4XUZ?)^PS$>6bbrQR+=KIKa!bY(B_Jhd#A9KM^*)#qP5%*qwaI7@&Wic zxi7no3dkm_MY6u$%bpyp{{54s6TvC@?a{;tX=VF$HojqCuV3HVweb#p6wZ&+f)r2~ zRJGluKdm1UJ>Te-p=K_PUn0{$oXR)8HXivehNKI3&3A5&j=(t4e?-%nZzYxcn=)k+ zHn$~u<4SkiVAS;Zde+ajLH09r^0}fdYbnb(PvT$Pifo5dZ+`Gg!W@T}Tt5N1*U5=Og_`m3~N=!Koa>^YdK2%%UJYcYGt&{|qXL~GzKr!xAJ z;y!8VG>@b`#BN5L;jH;C@Sl)$VQ8AWN78q3mjMeK{sZzPD^gtcL~Nn=Hzn)T%!sDe_KZSorkYg<>*JSbIpjMp{c>}10rQJPL zebWAjzV;(7154(N=$`Rq4>osNl-N!L=^t?)*8X!4($^h5kuo*f=f5X6e~TDjr|cMb z5NS{bYu`t3K7r>51B|t$Uq^pN)uZ{r2p8eaMy-W1#@JFU&LM zn$%BLu@C1y=co&4iKFzByje4TA&^e>!(e`89x?P9*{mQOKD8O^@ zmkR{XSHKZ}x!9>BQ^K_TqnX={tn$oX_^M<`DP&z1BI!D3F2=p1 zwEl_>7*3O2|9T`C`dph@``XC$teB5A{6~-RzXL&&sD+QsN#YUJ4Z?~9`rY8#1>?pl zJaaMupJ#0H8F-c&K)VZ>*&-DWwB#`;Vh=ZGYmQ7FVv-FjuOt&Qm2PvyHJ`y|xi>Hk zaX&9u2knd$eh)@^t2Vy*r8`TlenN$Z@opVaI&MXjj%$;lCM(#z8=CFDffAOvj8)mD zQ4frTFyK&<>@%_K4A$Qw;b1zGE2K8@ zq&)(Wm2rcK3eJriSp7TrD4U_E21@}QhHj}|;?#q~xytB`WW?L$?2+8|WqPLu1N3&j z5-j{rT3{ULZ8(C)<3XL@t6%nR-N7v)Je@PJtiTv!irwp5=)fvAE}ZBF-s*x+QW2}P z%x@3}TnKpvh3qRm(3$}=LSR$mNp=ENi=E5I%n z5=DU#Y;YhGuW(Or$z4eOodQBRV8$Dx=}=}+v~=nJwc6rSMYrlrp8?G788I*hV=;IYmkRVGFVN22}f)IC^Bvtgs?|V8_g8x@CQp`XnhDi&lOdTU?7A zjoQoPVGD&^dWtmswEenB>g@nR%hzC)ft`M6AHx-B&)GsE@1MgBpIPTGlJaUUg_px8 zOVe|_O*N>Igg21uW5~qa#vAV!CbnU@U4W+Z%jJVlaJ7tCA7X>h2%s1{^i%=nKj)X| zfcRPJWfYNQR|6#sx>-9lu%4rxUIp{G9lR z7BD1f<_P-%$HAG3F6L|pK%_i{&3D@Vxlqt+popB2JXRukM*eBI5!LjsL_7^oS zq(42iOUYSOupDr;;|RZNk^~(bTVX`#DB992rTCowufs<`=RvZ)N-%rG@#D)yJ0^2_ z6dOpN^pBW5c0UY+VJG)HW*}`O^E2zY2(C=R`_j%IeQ%?q9K7Z=Lt-t?w2KZ#L*)f4 z)nLey(&b8KQ0fZ2o;gJljOOJP;)xYbJ*&%dhgC|p*Eh2@)YrJz9k`rO>rlT6&fCvj z4LRJ1(y|w++<{Z7vYBR87mvTFLVw;UdQQ8#NO{wv0$`2@@xWV+CJQFBQKxv|sasIM zWt5I9IL$?ZYBSO*>xRHq<_bB!`?teh)HZ^sgt-0UL|N5g+RV_7L0te5`0a-9aoG=$F zEfh9O$;yi$YoK)Gny*BvKF^N|{t&rBdPXh8m0BpSo+-w{jP;IZcFmJ{5>X#g*Sw6t zBnQWl4e&_S!TChd(U7CLDepJbl+r{BY*`7h5!}=1h8N*i(%Awj{!H$tflv( zYQB4uepbznU#L|!(8_*mc7qS$Llm~&V*Hu%gha`8y)Ao??np@!XUwg|pOck;6Xo{g z&B)^!lYfV}8?KTfri|lKQp}!GR9=VI`*k}jXYhc`oNXG_>8fQb>VXit4dY#kGP%me zB3XyIVE~u8clVv|Ka9?oIHBprKaMyl;R!5n*;4>d`cB-C^$+_rS2@==#YI>Dl@0dT zX6>pyp#A$&XLrwFW~dU_IK1v_#n8&T?f8ymm%3Y&r1P<4zfHrKf{)yS-Qv^vc0bXh zP=M`sxmlM(8b))IX68nyMiL2$sTp(T0_qJ4u`O2Cmq{I~iZ@uFSVZ~ypYe6oS9q8@%I|wz$VkjgIRgo*#Bj~I z0?NF*As4ddHHuftCMI^a5iqcfV1c-w^dPejbpP_tpZg>}h;j$?<<*+?96oMBs%-r+ zX`j3V{yrC5og^f>gCL!A{Y+Za?au0hk`d$!TR`r@7uU1DEgY8u=KHdw=qa`8`A^KCs9Kzy|E?xt*kuDDs8PDpPu)#H%r5y3OeLenMVLc-q_I z&4!l}C&ojTQu&6jk80+jzhaJU#DAVxV1GS8 zGg?~Hj;g$8_pw>51SD6BZ=zuNW+vV-$P5ir&F*H&E8)!98Gj-Bv6)6loOFdt1WP`< zqpSc+W++De)RpricFvO*6*4U#4wQG2;Dd!Yt3d_&PRnmP&#w2n+53`OF}f`DAqP7i z_@CkeRkxYSWh_2J(cMue|1Rx)P^?>`a}1BQ4ljv$zu=CRvk1EoF294M2tzGb9HDJ&R4clagD(QY_d-Zoif{y0Fv-3*+6Yq9Q6s6Sa%vwK8Ra~V`+ zwK) zF&`9?ad!BiZNRHgEFLc<>2l)S6=w}Dx_H!jc4a2X*z2G;_*jeFc*_-|#x?8$i8h`B zpjnugbtgA3|0+S7$;Y<5i}zfNIhOBg%wWTup{<@vnv;=yev&yD(^YZcPk)YA!wYit zc!y22VNad49d-96hBpYC0H>Tlm4qPP1oav(FHRQvM~zja##!+0{2UFxre!?BP9v$Z zoky{@mrh_sg7=K$-cK2XTxy{6F792;nV{9t zNikaVoLU_jHumGopo`tLp)!pk_|KezrfgkD&3Xx3!A~cnVf4T(cJcG-ChLf#M~}a> zwT?Yd)bG~)3uhc-)nt4XKojG-%-Tjt(yqwY$#3QawZ984spN@A^(U=`Gq){H3^}wr z=z;5-OdY3h&kTboHU31$IE$PfXFUjmMVe1J0PpemB6Zhv>mqA&XwiF5s37Wi5y{#+ zXuX)fWjQ1*gGKpHIof*h_>i>MfOU~Ih=-tUbG!@iVUm%rmtpQIn?q$8mE>KhoTeP_ z;?HDD$Ak$?km;84 z`7G=i!A#YXYVYhq&a;os8F@ax+N5d4mHvVRFTSqhm#1zlIqO>{;bU&4+K1?QLv3W+ zv&ASDe&za*p^HCN);q`9JAeJ?tmum#Ohz9tuhAAd_SWq1o!2Ht_s*aDif>dJbT;QeA5g?JCkk}Gyl&0^9SHxL#*pI*v zF|v51+6jS*V9^M6cDEiLIQ;J)=md#K1)IXH$C+qlbs9<}DQNZdB}ccSf6%RKuBCLW z#X;ASu*uZA=iqcDkZ=jtabT}Kd_kDaee>v@v!XyIho>8cSBFbqijdB%BH)7jttb)? zyX#|rP9EA|3|VJ6o&IgenQ(P=QZ&H0J%S;X_YJ0Izs+eI?wkqCE5wIlU}_;yUhghVD0rgU8M+CdkVSlNvF5sRQHVQEw&}V*O%|DReB=zTg@c&V zj~^V0)SspeB*B0@{`P+8XGL>oOL?C&;d(Z2DW0}f%;x1FZ$WF+;6kN9QN(SY%lmcB z&m87-dX$?59K0%L4R+2MSSMpU?=K$-E(EJ*pMzo35mDiECV)c;tSlnqJj5@*`j{vP7v< z#~tr&xMcXrl0PBgz`NYHk>s8Pa$FwM($8cxAy^XbD$@E!xvS0@XCQnen{Cvhk&T&3 z!vI`v>mPE5iRWLibIc)rl&VUjqktI#@?=*0Xf@_4NtNp@ou=-F9tn{YcLG0`8h^S- zID9tfBE5`D4X- z#>guUXZ))kp&OszP~7A%zQ}s`A#TBQ7`{+9I?)w=v8J#!@4FJcy zhwvDRo^mLeU82;PV0BIs3Uc;Gkeg#U@m;{BU|;i@&`b;)BuZz)wJ1Tup1n?C;qs@Llyc#b&?RY@yN@5#CE$XPGD|h!_b1tXy zz(;mlqm*Z!{B{*t>vZKu((Iuuz*O7hhT#CZ@}qKyD^iZw;+^sN7Y+{ck$fqND|ERh zvWMNunc&;7H@`0D22H$FXV z({6K7zq~J-KOMaoeYH?_fcQr+>&jPXbw*S^X^1rq7`gRbIp?&C# z9AvfJ|LlDt-jpBF33SH@4jA;OiZZc|kC^PpbdK>gOUCpKZ@K){r?|ZXgDP(S6pQv% z;6ZoJP?Nx7A8K=XsX>WfpkwfA?SYl)H=3^!-@txuxKsA6(V=tBOLzj}7@Y}<`}-dL z#L%36As%T{lfvVw-DgxFjW1*P?M+_I+ltR2aCwi?6J?^4?GAX`w0HK3jP}R$9&adzikv z%zUrH=TGmiwR`|*G3O_rMZvwgeL#AkV}3-Ijo0}zS>^>yW)$Ua{-M#;MTbvY97g(q z$ydOec52)WA2GF~9=-Y-ur#K`iQNQ*R{R+`)$+VW@%>nb&EbkOR8M-zZ^I^k*`n&E z9I$h2`S>4eI2HWd8@Rj5uMm0%4{shDM>>7DRp>1Vza*e0zo!VfLPaI|xe^?N$3}|@ zISNBFM>e3u=L3=vR4Whr6n#x+EOWThpvWQ%Bf6>L=2Jq2K!*H-rc7-N!xjFK z+DWG_zEhT&YP@Cua2*&wTEioWYA0KL!25Bf*UgT-c6KL!*j%hGU1GJ#4b447)t?ev zTQsgfGyD3eGBFulYwd5F3)QM#Jr)Z{si>(&7e!~)*BmPGhxYGJ|8%BLDX${6_i%*aR5m|2&)?rrEB7 z>?h&n!}~90;8j@zk#`u|!*OEkyMHGzNjg^LS2OB684JipRPOAjS4PDrApy|T1^ zw%Dq3jJp;-lKIjWP3YV*A-ry!oj>{+Q|wcU>5|KuNj#G4nI!7MheMgW*P2?s)N*gY zTNZZ#p@cP4MRE>{2#DU=MnLqji3uGP_36$tSlW_z{>q+%_393!p!&}SS!Zhrf5K_d zkIJejOF6TFhVOUGUgiYFm1w19@@}ir z2G-|W-x=zc5Ue5U?43wQYq0>T@}E)!$Q{lZo&5rm6rh2XQNf#7v*@m{XWf}2l-Z82|j8x?8^sgIKd{3J@? ze*=oF>O7aioudxNK-CEtc`u~zi?FsTgtbM^AgnD4VQmC*HD?)i6mbaW{rsFC*}4xODat)1#S$ewar=)=Oaqi5athZC&1S$*3YWX# zK(O3T{m$X#h177VbUuf)3&Uw9JFpM57{g#`q@P1;WoXH{ctt%( zlLj=J539sS0h;*|V)hf<&BSk%soDK_5JX@K9+KM}e5@()X;;nflQcIx(*XxDXkDT=X3 zJtX0rY>ObHh?)Sm7SV+atA`E4_mLAY6>=zpal<$3Xn-#;3JUS|+Fs8C;X z+hzyK^3D^#PXnfI4UVqaTMpa9Xx4;*so0ljK)(OxMo3)IR}4?hT6;7SI?2ze+Fsi8 z8KhzxIHKIn@c&0qJ#uG)|0$9Y}Y$_8e#3gw3W zcKgx8ud$w9>mcjT>@i^SHHP4;*z4@t<@`j%43BestQ^#A8i@iBQ7?xC=;59!c`U~? zlC!Z9gLBP~ju^FWJ||8D3IG@Nnp<%M>P7fjZn|25$h)!i`td^}R$z0=$QLC!oxC0% zZ7!G0FK6)cveCHyZHGIv;hU?}8 zBlSGesEkvREll4Ic=$~uwRm%-mcKcgf-04gZ z21eSE=QMlu(4~vT+;dHc!)YV`Ql0SC_w%eYHPDwVzEz}s%klk-*G z(z|qr?tq^Z-dFLj-D6IYv4Ag06?Ys}Pcs*#>7)fwhS@cTL z8XZd5`MDgXzcmJuqNmW?ID+3fc&G)!lUGbLIjf~M$V-CNbWer3>&`dZMfm6;ONT{B zbeKD6du!22@V|p-RHv)-18tx_eBRol494~S>ry_SO@3!+s%p5J<-BxwF7*Y{U-X_M zj!TbyA{^zdq5aJZ3|?H39W?76hSs*9w4`g)R%#6idlZ-G?Ky@*wbqyqj*^o#SAhK> zJ$6<+cC5rOVGE^-3n*f~B<4&L{rP3_M#3K*Sw zwy%xm?XEsTz5Zy`#3Su7^Y^5sj!s@a9M?C5%HPJLccF@dnBJ+8Z+a67GM8mz$R`Mc zz;Yue1jORERz`!z))6`}a#mnlfn_sSBk72y=4pOMzu1-WNC**YwHtjWd`lJppX&Hq zxh`I>T5k$#{UPD+CWD#5o+$sZ(wSJ)J7Ovaj-vssbP#mC`6gI6!1e}L@>H}TzkKb= z4I^32ACWWURVpjkl#rYnVB0RfhpXAD9;TW3ob#TuJaNOjkViaH{V8MkoiWfR1eW+6 zaIxP18G|XG42_t#!#2H*H@a{%nm<1SG?hOwMH2tDkDPbCt&}vUHhe?{<~xVS)!OSd z?u$aftr$05Wc0`ioxerA#rM0h6{W35+q=HAT*qKFE9jVN#V^;KN#H8585E#O6v=03 zW_bR~C{jL7o9+V@KVwPk7UY$iHFW7lFI8N%?QfX_4D%NTk$eG)8i>8eos~mnZZ-%s zFaIC7>nLSu@6j+K;sl;LB2L&L;sg|jliL{seWLan((80hkn(5v{Xs`9MB!ooS}nB? z1@LuPqx2V1VV3vBBcYf ztNolL;;s(XH|k#V={ie#K=+w~pe|5FAn2Ab~74}cfe1Fi4yc~aEW$&yT^0-W= zWR102%a?3M?*4}Pj|FaQl9|(dIq%NX2y-u++xn=%T!f1arda9P=ISvblE9f9brsk?)0m4XBE9WQ;Lzw~mg*(j)9=YN>vYb}IIy znlG9C0oc$A28W$4;XV{tHhtDH)fH0g zmZXQ#M>aeEUgRf2Hq4)B=F_uu_n_aCK6AFJ!e?*dj@p4$_;+aKYAIExUP^_Ns}l)n zoN&TYsa;U4Mo%!%@@^-1=XP+zSV5zW|L!Qt%)A$)1tj{m*}UFNo}pVgo1$YG%N*E0 zn*ESGrnAgQ#gS*~QG~jg1`w0h3|ld#y`eeY4=ToEoJGFmU2zZVUx?O%2*28%_Jnhu zzm00W&X-(jf@c!CnpF~e^bsdltk}?(Jqf(@)^i}a>YIcjS`>~mJ ztmh^eo0&P(G>fiZYoQLVOx(vjGuMmqW;Vm8JVGf)?Kq6X9HG0AfYE_0SL$yO=m)tb zpks8ZJwE@{lz?CvcH3^}*F~Y>7NB3Ate!ce%Q71$jm)m$dFuItH15SNB1FC7`6z|O zC%<5@j*Nl4VPQ^4)&Usyu&guKqHSL66OJy0E{FV6{9kQ(;EAOzVfVIp`?C>Um5t)E3|%*O{u2hLh-0Rk@-#IWl(wX<%N&U*MCXW;hsmIX^&lo ze*Q*78W!UYyU+HSmFBb0D=7ynm7UpXPo-xNI=`_6W-ar*wepf|ZgG*dId;;zqb;E= z*|fBwUBsYwgZ$>v*0IIy_*z{Mskjt#eaaK^Gx2SnIxDm%*A%B?ExhBKRlu{<>1dix5mdt|APTa%Hsz@9Q8VEzWA|u(xBu}#oIFU7Ba=nd){gM^0Yp%__H}T3(6bzLabc@G+M3?*vG$DsipJ zfm~&Kkf$989PThjWojR`$r2a{JGEWMV=aRFCMWvn%dbrhaGXbsjb#Ok;Olj!;B}D4&6+4z)sK>jq{F?L4a;;QP;m$6 zfZ9`pzjzFTLQndWrbitRJ^%d7d9}rN3N-_LDwf9>ONd^2{uRoccHxq)K0aAtsjW6U z?>qMkTZ9~7k2#iQSlsC7;(vxuQLj##oZ6@g_gD0ShkdF&y-6du#+Do2BBz{4)sS3@;< zY9OS&w@47)wF_i*>`uZHJ1U@T|;+r-uG4 zGO)LZTZ1d1QP6Gwd4mH~h8E(ZRH`F4wG+``?m?(YnlC56Tx(D<{Yu2LT~Q74&FT(XcqKk!9Vc>Cffq*V#h4(@u>AqyAo;XWMD>=tTIXB3FinEcd)(?+g@Z^07_R zpap`RX7k~L`FI=Y87%bXmie4}qX>O?uWfKQOia9Q%L8iJ|6yQprwBf@*VG`e3BUi^ z7dNz5|8Cc6(PhZ$%N!(6Cu!!>R6e-q$+wey$ZB;Yp9cQjIsAtyy#QUs_FGN?*MURT zY1QA#-V4G@=06g+3|?^JdV9-1e#9a+Q4A#ZyDj>z1--yWWx27>{G0IAc17rcOeY`w zTvY^Hbc8@Jh2O@G1-O0GYYQ>8PRF|Tde!S0ZRIDwJPG?wj03rZ&_we?-EqDJKd?<* zoOwEK<}a<*$(-ieIrvXG*a5 z>c!jq6&!4V8<*o`vz(xvQIy2i*IvDzX-C6V1;kpWFLSa^$E%r{j>lU_7~;WKE)DUw z%3elLw>f7QWo6}AP(Wsel{1Z@nW3zlsNXs|4n!HIf=@ILx6kn+0C zMD`a3S81EqW7)&(UuX#9j&|n?%l-ztIasf{>yR_{Gi7ej*H}&3w`%WX{D!$d?7rMX z59kDP11%nYZ3gyn)jr=i;_IvypKSkL>i?!ytNrw<6qHIymc3xt%17h>!d9QN6cf>W zKh&z9^ZD1G(U*iUzsoF!4Xt=kwKdvFiO?#Fm~nU%UeEbv+F(yWO;mN|#P^O^m~w5; zGj`ZP(C>+-#9NXhjwwCdBNBY|gDV;#=;x|BsQY-1 z@|x(_nKnwf5X`_N_~I((TyAjd!2EXHd+KZ_AwTwjn6_P%b&JxYS{Ks;+2(tJ|ALeY zU8h$L*=EgjiQ}Tsw}Lk>B_3pHU-VG_adFDx<^nT9i@tjP$$T5-6&B=4QGIib1+D&C zZFM3Bu1tSc$cfbs^ZGo`4$Wx0KP69my#MfhSZ*~>W^mzwl;YC+cIpdpg^)N|5yoTb zVB5)Ts^k!*Ta!h2HII(pyG0Vqw49GmQ`uuiqmQQr86=F=2hoV~fk=VG#HnAAjgVBI z%o=)29ooFGbk;CVdbQtUZr^M`5p=6awp#d1OiYNv z!Zd_u{2Tta`R~QP@bCA3KmP>_;&3i&k7cv;rs~qm{;>b#Q2d7H0S)qlNf&dSvgAgU zSSBMOcrFhAT`|)Nfz0OwF^)IhHo_Qs6w!<_ZAN^aAqidr#a2!@$j|>61Gr7;UrSHm zHV#d9qb;L7_TaoGKE~4~fXa|ZOyg|5mjRjcQtB>8G2IKllh|=8A+A>cd)OuXs2ZD8 zn}+jNbgjCw?6i)U>P<95w1vl$B%3PJ!m%n((ypOPv|j(5t{O)axzb#y%;R0qD9vuy z&`!}mcF3#s9|ofR!JM--)h(2B?!RuhuAo83rI`??3$bGOv9auX0CcN&aPnr5e(?3a zOOLL~ida3Og4Pp*x4$Mb-kGUAme<1DF#Y*EmpwG(4W+uvDBbOepxbnE%<4KCCV_43 zcugB`$}+O}+H{S1lgcUfl}W9{;Cz zg_CTFuRL~7%^ZswAgJC_mwSq}+~_nu&Pgme4zbd5%${LOHGJm1sdlL|`Ip5s;sJ^C zM04w0!*aIoqF1vb+aHx~Wzc?=$ql_#lUAFvk-e4?PD>eg*(;|Y0sIctcl#7~Iz7^E zK0&Wb@m9YRCuBo=u2Q}G*H0;#8J$-B(EG(FmLCb=&9NSFX>w?1mGizU;@&F5NZ$j8 zqB(!`G1kAkFeG|#TsWm7qDi5jl$iJeSr+qNb)C${a0)v;~c=ESxpwmC^A zw)5tG?ysKus^0fc_dchpyQ|KRefC=W>a~)@9VK}&iYMCZg>#&KZgy;i8)8=&*b{`f zN8Yy)O*R!}!Tm%o4MTd@z3?KW=wigh8*U zEk8bwd<3sgz zLE5>C;TTAiF793+M#>C&_<4Zo8h#D~78RD|diGv4jImJ%DxqT|%6teAGtw^XE1glf zqVY!H55*W|8*yUQs({OnG|7`UNa1)x*~5?fJA+N7ldd>8MhQ#g7tSahQ#o4*FUM$r zmzWnhVP_$lKA0Xws}T~ooa+ZMUb`3VyeP#p8Z*IJq8W~1Za_L}CFMlWz@zSJKk4;z zwp^G8t(BF$5FtoYlehtQVHZeSv{d(uATQvfpYl~MS2B<88LTjN^-U)a@FAH4NuWoC zvnXZ_TZF4YIZZ|LJ~%xqURred84lK%h3!mxtP0@VDR9NZ@|R}*=w-s*&mOnGce>;T zOt6tXY4x|_V&Rwr%}?KwN0@u}vq!e(kq)L!;m7-$a>Lwy^P>ZUhfk}J)@V`*n<6I+ zdpdu1HJM5%fW2(H5Hgs0CjSi2iY6u53tetH#*Yb`Qz8HKnSHtgf6LB}=r?q>=SIvkb>4S&s%NJP@}7JjZVm>^+r3GYGP% z`LS`-2`UUJQbIww#zC)2zp`Hu*GwIp|V%FDtpki(~+24TJb(DC$$TXNgz?}K*l z#m93&QcVSp-vK?K*>RXP02}{ms6tFdRCe*f-5*r^HY40c`CtG!@m`qmFR1|9(D5^ z0vdy7i`0mndBwQ}kHyUB#ZF}MDI1Y)al||RX5M&cQHxg#aRj_~= z=B!bS8m57CkgI_PW8~&TxXGXBkFj_SeMWd!4QXd%g`yAmRD!|7fz~VU4#Fj1;A~u3+)~ydEgc<@#|bWngPA z2cEe1QO6Wt>uOMC-<-|d8_ZN%-Vxxl~o6TD2H4{QF*laWI*X@+OLk2DjC$TPFk1i z`QjKZ%g-c7md|pM;(UBDY7LDyS#UP_iKjd9<>h+cs?x48$)Wyryi$Ridr;d7$f=1O zG-Grpa!?*yh)e!G;?cq3D=_M!`%mv=vd;y95LL)ZPu`RpOEtYn(#0kerP_e{umQkZ&z(G)LR7qc!pw5my z@gdQI`QCxcfm2litPw7)$E4pSBS=;YkD?Q(r%f_nugfr^*9^Ak>cdd=RbRHzE8!y5P%xPT=W26v_tl?!aD;5-GzKUmNF>OV8OxB{?&-X0C_kN zZzf`%nh4a|Q5%|6dROH;p+4X(_v{;KcmG&kdqvG|>I24<+Wlw<*9*m~xYMKyOn4Vr zN0+IdKSx5$8R_fj787rGaUf|B$Hf(QIJwM&?AZ=OX0x9O@e+X?zUP9e3&ODR1M_Q| z&t!MzS!O2(k;ZCN@aD;1bv50sxZO)=K^%yyWPSGaCvvc6;7>x>K{CL}aED}{w_ovy zRQ6fx@xk(2uq8}kQNs^FCOCtdx6+zigD_IUat{nl#Gf$i_gr>Xsqvb9bd0Kfn`M%*!7$``wI0#e{g;3E6+SIcd`NwI%%(Up0Wkg0IATeW>rj|H>$YY(v%wb^u z2_v?t*W22v@{Pe4>01in92y+ZaYpH<(Wt+nY;P5>N|xFVbwiFMqjH=Vsg6x3i28fl z{rzg#Xd+DDL(FYErn(_nTg}Fc4INiGiT-GvSWGt7G5BUEH61`5HQE;D*j;`>We!$~ za4wyR_@>H#Xl%7H6*^-F@p$D@n*OS-eRep$NoC%(!tY5qN*h4gNQAPugtWZvz zV5oLIw2Dn$QyB<;T!3%p>I+b5$(EJaChcm00vov?>B3WtT|_yQeRW<^e&7Wu zqiSy>F;%EF57uKy?oD;X_R;(NLp*)yAp59+x9jl&Lr-w}?#up6%3G8kHA#9qx`A~o zc>&%=S`8SQ5Tvi4sfnM9AH{oojU~QLcN-F%p8wz2?ux4Ul|F2NlDCO+z z=)%tr+dqF>*l6wK>*L~t+yy@qa?In7ttU4&8#(cB=a{4uK89lc>h1W2q`h4rTQ9y5 zv$WS0B_}rE(ZA$ zb&2KO=lfY_z?0hpTSs3U^Jf&Im#53~9$$MmUprrV4xw(RyNA=A6G}E=eC5u;UPdQh zwrFavt0PKQV0$8|;v4T zh`zCdV^DZ*-k$C>78wP-EZrV&kRb;Y**;}ZKvzE)=$ln__wn&`dEYy9!2;EGXXXbS zT?F_&Jx~H*7zLO^X6{r%;+Ah$j#~YgLHz$`#Z|vBNY<>%)>c3BXR_+Bu-Q|!YuV43?*g#(9Dtp ztGf#TmsmE(oG)~6azqzptj(MIGaEE^a)5LCMAaFf)f2|>C#D9b3`gi(mg2xkm_c!E zjNIR-lZTrpJ?Go{;_Bk8iPO!^u?eW$E*tdd;UW6(t;plIo3k5ut5C)qNlL}{wVu)6tQiNxEnF(v2s8}Ap;wT%NbI5Iw7awT`M zn3{sy@8jnX#}9jFd&j4Ts|PopPS^=BYWx-${!{7_si`26kra$E6vR!Pm61O!xD=ht z|31y$GG*xK>gfsJrU%$jqWVoze~atv1X{`d{!N`pst*n-ou#I_MsW@i?sE4)SRwt^ zG=b{GoejyDrz3)mexS=l40A5-n>1bOQOkO)h{(i*6v_WaOAUtOoYK8^ogtCp3HHhE zD^Lf`V98yX@F_oP&bjE7DSjZseoja>%6`hYI`7P2NWZ`pT7?W0Ogv4 zhdPDtF4za;W0h#3R)hgw=gT82-(v(TBeaHuCh}?0KD|XEm|`*CPImP7s7T5m?+A;o za=dy>*>)P>rNpcyKTo=7W}0A{#mF~Ck7eG1NiIv^r7!ud?yavN2(78pi4{xMYGXCA z$@bWPJZl#ZK;_~%E;BiNuXy@>1#HEy6L~YSn7Zt<1O2waK6?oV-p5%h? z%eCD2oog}Kq+V=6iC!@)zfrE!@glk^zd@(-lQzBu6-qTZH<@ZZT+Xf1z6sS0$Cgqp z!HV)C$TLxcw?3>ho2#tZwH)wbS`G3#yQhsp^Y`(S}~KtdSh&NA$Zv>xh$ej zHnG_@KHC%5klhzfvPGs1Ldg;vG~Y%98hTP0D~s+y)^01-^vs@Y5jw$=%9a7rqkjh#Lz3&<(cq3*%I0lV@T2k9|5Z5&XCqNSk8U|RS>~PC zca(?C*aNthpbv5iU_NOt&4kr6#n|r#NeHcRx#Hw;jKr^8AtkdjWg$Uqp@as8CR1^Y zy8XrS$12G!#!;U1la^&?Fbqv-)yL0fGofUrWN;Z=undg&0Oc7pJo*R`c1WS?$Uucp zP1OY&L^!JeI^S^^xIn`9#E(#1;oQz3KwqS0oo;ycz^^W>lcqjyG^`CIV@8gG zxzl9G95+t>9Lx=4nfE~6pevNkY_90hqyOTryFWwhv5eoWNjYWSwRJR1o?H8^U%pkw zk4krhsVLPYFk~bO&74ZjsU|8HMcA1&HjGsnN>wa1 z*<}1w-Re95>}Px717z-{l~$)@DI|q6P%sR#s?#dAF-*kIF0Bw;hG0+TTP`e@AR|ld z4RB!C;Q)}qJ`i`tTviUDSImS2JpJ*3Vi~ye0__*Sx_{M^lVTIlw+p7j@RZw;!22cQBNPer%BYyo=j21{r{ zN5MKkVsX{4#`hD)8-rA!l&bITFea8+p7TfLJZT9g+Zhw(ua7+G;BzNd4%0%(7tuw2 z{`x6J#;`IzPQd4{CApKx4e#h{yZ{L}gR;d~t5k}!6QxZ@eyt8(nPr%h_eaiQE*v}c z3vw!QEF;?}5LRFEobtICeNuhP*{q)WdS=`Nc|i8Em(L2OZKzu(Z_osse=P3E}K zW`6LwYDE+2A-{x)Q&R1(-V3Bpai8a!viGcdf0&K{6=$@6>Ek}OJi%I0zOuHE&hE7U z-V=MWk<`AW34?!oFv*}yM)im#oCw4oz9G&wNSkjF{Jj0Z6iMOswiSYSJQ-?oS z=lu7OG}|`tlZ(#P<~Uaedu$v(-oPhP5cAzy*5V1@#4tR8=B1esuJn4QfuWDwD~OSI|Wbal!tnRVPioxV7DJ| z4=HQb9|4&{-UGQ-jVGLUyX^5pT@=a`UXNNHJy!vOko3n1B%Rnb_1uTKk@z|Wk$>~ce z6i|{kmRs)NNa`ya5?S}F@3DMAcnpy&nLX39j5Nls7wM*n`nkfWZ+c~-gzmiZ0@Jg+ zO<1UWH&5^Lk>*>pHFb}$Iz6Ez3{Tir@4eYG25nT*4sw5AaIG@Yl!yKR`@9ap6{gKy zqY&C&G_8`3{Xa2!o(`@FHM3F#HXUzDxt~$H?TUAQ#U+!a2)!-*d`f-KOOPlYjE@v3 zpJJ|eQcP}&l^GjBO(PJ6Bv;9#i@@5o(+a<}NW{bZO5-WBAyh)j4VQC#P~e-N-riST z$Vy_uyvFpv%YYgBTm0Y!C@Z<`DZA~dxi#o*j@7@<=U)`bZ^EdnV|Y2Z{aaS&D_KJ) zMa_?vzk;@q+ncL^)?)BYW{@69R>?0@uB=nA>Sikc>@T_rmb!-nH3y^`3ZQR7V`o5a zY0;BvTqw{&j!3hbY@Q=a_n7ykAoVam3ozaDn%;sbSF54?;3fe;aAIX(CDN2Y7zr|_ z7gAZ8XOv3w%wIkk3cMbwvUnkyMScU@(4-$SGO)vx3t*jB#;GE?K8MGz9SaP7y|^Xv z+1W0b#)(A2z!td85Wm*NqYotWDD#d^MtNT$(+-#!?p}Df%^>x-InMxWrw2S%Izd*NW z`|_ck$;IE^X5~MXcnyliUgH&Ln)XfY?_dUTZP#|XO6DQYqt>+3NWu;QF$N^5vFAjcFtD? zTGY+&sny8@>9nZ^9r=^T`huIAHFN2wGDhfWl;0b5R*r}4Q+@JpzCm~$+UmRQ7}q_L zfiko-P53fuHObn<`Tvlb{m{m)Xhn9P4aWe0}DeoD3yDRvQ1~sINx){;VMh+I8u67^oWy74!V~f3TeQr8yjlX`{O-s{)U2zfPQx@$9 z-Dv}9aorg5B)o;Eo6sY1vBMX*&~HWG@b&DEuRKZd_&LyxTQu$PHHV1#_TdNhgI$=6=kjK20wROw7 zIGbPR{1}s!`W5nM9uzF?6NLR#_WtDr1hG8wzToh$qNzoc~CY_-m!Cwvq|V z7dmky6ZSa8|C7l#DW$h{l2e`KsaUQQ{+&wMlw_Ll1ee9d_=}fZ4n{Ubrh)0$ad&|8 z1}fKgLXt(D2d#?;V1TL7F51IM%@KFB;RJgdPi=Mf4g#Iu6W2gayT1hkWm9$jJ`xYB ztD67(6`BjtEOMU!KX3X4pewzYF^>YyGxneZ*fm3=UKT7$D?zl#c;(4#KP80dnK1m? zBnxZz4EP=8xczeu1GmW0C~ofIf8SKl+a0XgFKfk-;OK_2#3T|qW&TU z6QT+H(i;`$Yl@E_9mBySZ!PKk8NACGvS%#wOSzc+UqlFB$~cu)VAbI)ZEK2@st|po zT5mI-IJdTS{#mKMgJ3SQxecl4#}bC@K6e1-rifI-vLXV1Z!(cJ+^|6fJ$7Jai3NO4 zdjoUY>6$i*0*v*lXQ9SWftw{@*NqqIU)%vrN6-jVIY+q+WhX-PjzYpXPX>>}Wj8ME ztE9AIdpxh|n}}hX#ozK;-R+&-J?dN@e4=N9*)_+iTiVze$CNS2UFilEkuOm*(njS* z;;97Y>(xyB+vh;cszDQAIQkt)v$R4VA?P-f7PZ&AOO19Dl!wR9niT{D5Q5?K=|5P| zxkBpr{t!$0@L+DjRE2abl2<@}g;<>YQ@;kqHgw2B^1=7i0P-_yiPiMv9I^l{l_4@U z#>!vZ3w`VhZtu4$P(%OyFo78iS%|ZlRZg_5D_c2XF4wn^B1wN_L(XBSLpiH#ODZpO zgo1ADk!&+{5OARTA+l)bt`j@STx+F0M)TLX7-{xoTuGETPk>O@|Gu@uRA(-Qst-H zmFG^OzZst?ddKyni+RP5c}D%Da`kyZ`i(M8wB#%^oMCExgxCo_A4inddju~ znET>PwZH}%xGm^qj;!A}2NJ-gXzfW9gzGH`hKT}h5=Jkvh}&=nipI7xMMU75ztva_8THrqcz|==4=lw&A({wA71V)t zFc-MvjPOvs-?B)p;L6a&98cfjrMM0JF*F>NYu6nxNmD??k*KnNT4hBS-f+)7m>6*18hdnQ~Z2v!`5x||3XEWUAW z$!~ZP+KxuXJ5%I(OSN(!D(LjMTwwULKIgc2dKmKE+ZIH_n)+zwF*QoGxSvf_w|^3% zjn#+FbG!7#?6~}M`*8- zw9DAH#PD8&>Z#S|aUc0ji14*-7Li(xEXF4tO|~y##fD6$d}cYG&_D%BnD8c)qg*GI zplT?omp3vmTrns6Hb*ML`BtZR7%^1(d9_suCj9V={EckpQw-^!3iDeb-=#<)VdJD) zt5h4%VNvn%3Q8#0OMj00Lh=GK%$hgi#pf{{2*sP)+ZF8dZDU9cO?@ETJM@CbL)36Ya*RV=2b`1|vn}IH#O!L&CkiB*?55eA{-f47PsW;aj!6ow3xKejJl|u0?5V92$;QKXxw88DE;sNad z^$y80_K>_0E}5@fo^WM-qCO6bMNL7=1p<~O#Z)&U+1Lv?mVA{C_}<;AEn#||uFq`H z@uN4FX14wj_%Z_FbzrfoQM*$EX&tSCqqrNoSD`ziy2^PwRF_0G%@NAz)EM{$ut`I&iKix%%B1)BkJ9x<@+&-chN)9^lJ z=Hl;A>y>IvE+bLF1@zuW*j$Oi53V-s{+#q~8T)rH<8ZbGCORdoNyd-;3nsDAf7y^6<_W7CQ*2x5z|>*inu_t2-jpiv_zatf zH>2KoQ(qDij&wt-yvU?6Koa&;#FmxKI~N`(mpc&*M1Q(hq4T}D$77mAAMd_PDK^6 ztC{8iyG4OpX@hS8c=?r?$B+$VpsI~T|LnB=Qld$`{hBe$7qSw5+nxt8R{T)+G3w9` z?pJIw>CePH=W4(3mpn9=*pq8I2My)QtmijPMk0p{3FG;fYiq`KH{_x);y`I~e{&nKKfJJ)sG+%w zZ(CoBp6pt-BfCu5xaaxT?n~-X%Dy3SWM|F!KNH;rVjC$0%l~uEBUS1Bn)68OlaFzn z=8FdM+40=Mhs$t`Ric?IhFiX?8b=k)`jSQ0#$?S(S4E}dR@W{RYgSZ&t7C=(b3ICi zZ)WWjzb5L4-(F(4AdI$NFvIUe!+51sWaMZBk<7_!V^nWqa{FeZ-sA*Gr|#dvvuE8i zVUZzG7v0z=PE`ySaTpA(JS%7o|a-+bl5~mrk<+ zF%{iDtZehBKdj<^u?uhcXm}J*zv7N^1a9^GJ4jICz~}7+Sj^ifFEIJ|fusCKiqN0@ z=X>dnTPh0e-V~u~3s8yi(S#E0fgfC?AWDylr?gq3(@c*)+^|jO)JMJpM$Qo{%V)lR z;h$6v`QK zb0+lR|C!CD)q_rA@&`pdqSZzdx223BkQG@_S>2GWy5_e`o1T=Gn{f-i zTLQs&-%y_j%K~}%Cr;1t{~veVld3!&>dq`3DOx(9d5@-515ZjT^WbYy z4cmp@lo~gs6{V6e=*C6vsv0ltoLnGXjS(pQOQTYP3=4*B1}+3WHv;||M>eCSl3505uEbI3VI3>{cB)qG%`NJ zZICy4lMvEg(3xxkm{nG4m}XQ;n;JEXnv3Rvb z#iG`vUNSm{&>JjSLrZo42>rb6nh>+WwHnv^4at#CuB9-UjCM(alAwLIZn;!ATHaUQ zwA*rB_Hd+`MQ}OXEdOTu!Vy(rcTDV6Yw z?#24>zNIqQ^phE7ahEFJuR3^_-s`*?rgRoyRKnlSZ?SJ1vG35ypOm!Qqz@^HDq9N# zGe@Kdp?pS?vdf>RXDwN(n$fB{=@vEi$2h9B7G$1m^9$z7mz?|YS}8^A64SyTG(*|`a0c$ADPYFK=_V1x^VAcuOz|n0f9eZr96me z>>*z_+U&})4$i*GXcs}HA zMRjg^(fG4=Hu0n$wQ?P`2QfdK{<~>R;_=;d?Hu$OBclEGz)q9{JDk*!%y)lKZUEO3 z*A!}fgo^}OGQ!vOd>Y!n1?RmZx8Fu!@$(b^FYm)6y1>r z%p_x+R?Iv(*Ye#gd`lB3WeBk3oZ7Zr?hp-vbloeZ*bBnnFf_%3^nK2?Bkz&h{akuz zkVoZwvg}9Loc1NRmsbrB$PQ-q6Wv(hU|5dq_}5Gv9Mvnz)&t!e08I3fKLv(|0}vp83q*fL=ztcbHVqL1dGe^`+%t<|(nSt}i0D`O z*@NFlyEP9-CA^}4B^m!%$@Oc{N{a&%2kt9Q1j;vU47S#ft{v^N%a_SgwZTG9mhlDq zniBd^Z>)nu)|6Le6w0wQBvVvMN6aq!jb2(BMMG#Tw-e>i9yy#sGAzP(2&kzXg`5P& zIVOQyvq=C(=*=P9W_1rSGQtgne6pgPf)4GT?%QO~?N4GpcvyS>Jx^HLUiCd`*w-;H z+=qdRYw2Bek%VSEg(!|^1$X?x*Jy9*Vt=gJeetdGo$6&O)FB2MvBJ6;jZ@A_OoH6; z6zw$fzWwwoV-5hb)ZhzW5*)_DF@}$34uQllmb^=2i<=5O-8^CDVOtr<cHlbrcfVoFg?veMTAqc>i0`OD6-Rj5icrm6~nGTRw3Yf8fMIf|01 zqex>a{_l1b6Fh9hxgOvoWJz~OlO|{77H!x8SYtH#G_!Z9cupS_h&I|6{xiFno=oem z1Ro`YlMh?P3HE~aLCNVeM6@P=acj5Oifv?zo*HW+QR zfAVmzw6Q^Pv+UP_xo!?i!xFxV4N?sUj22iOGd3VM=hY_EuzXkP-AdeBEuYR6s6sYW z&zv^d%C=>xE_-fB%kZ`Rs9nIj!NB!d&qnq5SHf;gRLLB-xD3_cDQCVt2-g`vMkdYo&UQ&SBP8^ zDQqKA3b_$gE-g4EE|Ji>^jqyZs?Pw9ugnM5a^=9}f98V3_eJTdhZzSy{v#UDQ7W9#0L*u(TX3rHY zew!!)4sj&h!qDj1f#G9Aum}Q*f?A8^l(KrrL;jAcgLkN|ub%}E7c3?kWMtUS*toZz zd1nR7;p`e@WCeIUKg9TRqeU>LE~e}*LBKCfn}x+AybQe(G;)uuc5&pkQEtEb{LXBr zrM_2!KEu6BS4ikT`zN>Fl4|*3)su+c09M)o>aQ9#_y@>AM5x7kY*-dn-;U1JsV_-M56Z>7Hq$7l1yrp%2 zFSGz%kAodOcooaEoXY$>x}ob&GHdQz;`KNKTTBAetT;;rDfD%(y4 z^bHfhUaZsvmKK<)yVq=tkM*ZSvi)!{4iSMWGYx$*_Nfr^C3d{z z_TQaq>UD=43l59LYxOF_lA6XvRm{puSk%_^^Ir$a_=cyq>b1UG@FO;>M$sro&Ge6x zf(n)f;lz=wl?{#7?;yN)xgQ%8byXR3%q3$M98Pg?1_{Kz$Ic94++R9KiT_InLBZ|* zuMPtKD};9c$A}u+_`aW2!+UVKS;uSkEO$@%E?fxY~iuI<%&mIMwF!#nnod(G35RG#>9waX#CM zi2IGOb|TlnyM8q#3JCJ_&UCyJ1mWB*2pJlKaf}=o5&@uHFed=Pyq<4)fg}-Vt-;zGkDP}!l;j=Plq3|W8u17dP$G04jn>2)JEsJ%+ZX{;KT{%#t&z} zP!-{aagCl42lcj>`i15w*Ajj{or{4FtNKBPWO`n0b&r5fF$;C}p3e$z8CR| zU2Je7SJ&zUmA#WCpy+SW1dlgS-(=|}oXY=Kmi|YT3O3wAXH0Uwg=IcA5$&=9*?zlZ zWcTL2oTH6aG5Wj6ltPn={3P)?u#S}BR^?JIq2B_gnR!KjG9_$Z|KiGsC+QqeIetdv zExqPICxNG^xU@=HEbp&i*XifM%w#$*RT0yglVoWP5#L8nWs3-FD2f*)qQDp38$r;^ ztLvc%6AwL>Z4yRCr< z@T_Dc+K{jeq^CFxVl8avA zQVy}yb8U!qf3iHs`B@gRx(tbe6jv|-*3$wL!PmS6Np^2p;xXPZ*QR!yj%MCRDO8%; zNWYO66i2L4Jj=FP8q4^E4TaFAws@u6=ZuIcq!k6~fc@S)_ftSN!*QM&_1yDfU`DUF&f;H^`Cp{-K(%hy!86j? z4!aI`4^a=1;p8CyQG0;PcU<2DDoDaUDneuftS@mhFmT;dpiB_cv=X{Md&~6|1|!K= zlj6;tE|7-jZqS5@`4%UTeZOZtOzoU=DbW1@%L9!^2+WK>zYB={0Mw6U%2J{}6G{gM z==66S-!22fdcGleq=C>$X!5JZpgS5=aVVsS$xc@VW`C`kVPGRz988o+Q&5Y&-EMA3 zU>O-HBq&W#2z0Z+ouynnMdc|O8OH7pUJhQ0!%)P2LF=w*XhIreXbKf%I*1?nSMVfW ze09g$0f1{WlHp~w_~(U70FT@=D#%FBU`yixhds9*EO@$j{`G)bE1pVp#5H*|y}SK4>-Bl_%AQBIY|kwl@Q)zPL%m+19?A?X@L_$ijk|?zgNu z8K10@f)WjMC{YjXE5rdoffrXSFGoB^jQUJ70+Bhi)PXC+SSya>sxpkhcDbgV6|c{S z^ZN9Nqx5jD-0K|Qm!}u(E~6q&aSCZ*r9A*Sa@bgmboKC^?Ckny+uGo`)P(%5=w+(d zgDK(D^KqU>>;=RV!llGe{Q4bf@1c4}W=T(PL=*dU;qbL?*Y|HE1NdALuuGJLf|Hq86k5l?lWJ2) z^84pf&ZXKnMQM&eFk<^Shq{;Fko@cn9zPl7?gBborqUAlA18{eo7Sj}kul38%wEjJ zzI_k@Waea`=&!#GD9FIc;hOZXT8M^cQvW-hzgo> z_r31rj)5_N7ig0)pM{^!WL2R6wFjh_9VtNV1{nb)9z_w6FDQI@r?lWy0$ImMI0r42 zviD=|2~hHx_<1+@`B7TxKfJ#C`9Vqb%jCUQcxCZZ5nHcJYs@>zRQ?qS9;?@bV#c0` zeoFe%KS@uxq$=VfFl&;9>VjTonD$A8y?MMSVaQ74ZjY~N&>{Wr40s{{rQ?+^C&OHbY)$A}+-MV+HR-;x!LMuhuu-i<);hGMq&QYtcyy{)^zQ%<)RS9oUksIyi1m<{T@A zp9`G~^ctaiL%uCXD3J?7ocL`&GFD<8%E8C!%!3%&LGbDpLT|m+!@hlLPGRU=!rN?o|DmHi-qWdfCh>^hBml(2AK~&z(e~EN)q};cn z@cu|MlxF3v(cZ6YUd^+Hy-s%jJz-1xBl8RHIOf=#vfbU-LV35V-x#LxX#ufqs-U0Q zi0Y}7!l8>6o_OW# z#n z^F3q4NgMHvhw8y?#3+JRK-`ve;L4ep5gz4RnCuC%2Nq!+G~A2Y=p*4T*NCLY+ZdMh zGiUf~D#3Mt?2r<=jLAnK{epO2@nHn&An)W+ciQ}EUCur`8Y};|bwDGix z_ctl`MA)7foXzP5ZdL2}5jVe6?SR(}Svb7RE4$1UN>TbNZ}3yY*$;uhK6vuIc!VOD z{Wh{Ul@PpO>sNJ=}A?hbEXMlw-#6E*F`p5a5qGQ>eokosqWob zuQ2|Ap}QHG>D9I}d67&S&M-0j5Lwi7^f2tybRgA7K{48n;&pA~z^&&4)+OksKe`V6 zv$nZ1EXY|?1ItOnq-+CCM^LjYeNRz&XTsoZJ?#%I3i&whC;Mwz%_7>c9x~F8?~HaK zGYRYh586%*xmoHeNI9jwFJ_#73t4U!+t}2Ac?hWzU-=SnEnR34^gWk$N=ns@9U=5d+hS1 zZ^81$%2l6!Yl3QBFz0qZ=i3s+ z&e<8~?jy<32K&C-Inqmx@R@ zIw!^i8}SF)C+3n7_sHi4_t*b`7aWlu$QRt7TLQ0m>kYLUmjeQ!k*hZKwPxY=nHTx4 z(HGiZE$lOdn8qtkN|?kZRx99Pbm3mXN<-~({$x(BR{62--@$rgnwvc zAi5OE+W-81w~ErVsZ2FhyFSeh8*AB`FT3L94}n#$-d0RD_o7tQB*9SwvZ3MmkLwG} zf4!I5;8T_U>Z2o{<`|bw^=G2pvJl}`((Se{8d%J5#`MzG?SgFA64?-V)tso2<6SMc z8mE7#^W-dj(afgtT4&ELI`*K&r%W+VJ48-WLz&)B_~nlQS^R{@_=XL$Lhrcow4+>S zKSKX~UtJ0EHkd?i53St82QQJy(3JzZ9?pkb0}~TTPIiaT>16%tK+slDrxcb#v59 zTt0I(p{(pt>{>%d`i)jqQ{x+i`03zEErB)G5DkVF>{rCU920yt5PEK!Z-+P^jrD#D z#TVf>hZIV`k7+9!aYt$^discEy9f^SB7e`Pe z$3=ajy*Hcr1bBfnaHu6lk!r|nWf-?l^;gLv3;_@QG2$fZ9jnH3hSwFpWW&Q%kVvP29%fZoSE@3?&^&Aav6uh zbVg#s3}*+w|Cog1s(TP{_6@1rUt`ug(G~2a%{e^=YUZp|XX;+mb{DAk*+ptb;{P4^ zi~k?8-YGh>sB6=XE4FRhwkx)ss@T>O+qP}nwrv{~JL$aNJGw{ze;=)rz4zG~bFb@~ z>z@0Hw4R}+6cfLKZ8lZ*IMtww_R&CtA60q#sa8CIj!E}9V6O8GDiX0U`b6TvSGBti z;7M*63Qbm z{xI7*Ktq7d3U(BZ-SD!vVGjMe#|4M|=63i&NKoCyIUjhSoqY)BpUscQtY252_$64e z_-?4YdSl=*__j1E_G_2@o+gq4&hz85Xp1N$oHk18i@x^7p?#MJw&3Hdx^Gq?A9(?- zOG}nOg~6@f#Pju*3+^a?27u)ef~XN9=>IJ)0lcQSNm%;^g=%WNuzBR)*nG;h#k%6(nbNtOzaLGSac-u-#O1a`(3bzbKk@M0C8PNg{-9KJb zC9g*p3cfgPOE3BBG&+rnBt5Rl8?^inj$(#}SHRLhfx(~gisDaWi-dPi>A<>aqbw&}=%m|E~E%0KNWfpou zTM{0p3~b9L&O*|*$!Qid>ccWw+v^dwyr58`C$g5`h;3$^yB`^v+v7i>FNil1cNr?W zI>+UwtAZ!GNo_Ouo2GlkTdm`w1PFfa0MdlzeQ3-sLJ6kQ>Mwhg#dJ&x$(YD*^yipW zhVFhs>sSu=hj)Jqyt4`$`0W{H=OTFq#!Rk1lMl}7&`0C&w%7Yd#D5^rJGCatkorQx zYcO&d-g${Lx^W(p)uSWDcd%WBpHzaPJp9yxpzO=X60YS*` zQBLgDy+^B&KpZ^KF$G z*vEq&MqG4m(nRar$b6{B{FPq&XY)`P+QV=bE1+f-xID#RTTG0oS*%UgDRcCYN zOLBWS#ZPyPT&Faue~AQ`rU{`80USq||DS4^WNW`R&WV8$^ zqiRi!v9xyqC#!V?5Z$eJU}Sz}FNO00fhhMXS#=qJVYZh-?&e+YNSCDj1rUWLyK{KT zmFb{tl^{2-@T;_#FGQoR+>|EJ-8HY4h1;I;RTP$8tHz5G&_Lnw59cN>PBT&FGqE4`%M|ApV@Y(+4PYlg{q4sg1&^NA{ki_~?Ff98r^((V{5eG)o)(yQ3i=gU?dW-5wD zN|iuv8s&{A{~7LmvgTD1XptU1pez+1=kiY!n};*T3&z7vK)?P}3@m2~xo26gd)r4m z??Icdru2YiU>y?Qn<$auPW_sdi#Ry;YZk3SSR}UDu{jD;cdiEg4nV`_H+U|Wz01VO zKX?AXPwV~>uBB*eCTrV!3MrRY(msq9P~c)P*V>)d7ch)&IHJOX`_NzZmjpEA_q9(5 zTuV_5LL^hsd!y3&GD}Y8X1-F3d$W#UJ}pQN*=}So-JKe-`KW^3(lm~=Vcf{QFMf{# z;H(I7mnD<*ZesF;0ML6U$S72IGJPmv>!Jw5Yk_?MI~j{fZ35r0G*sYPqJw0qr#Mg< zvb{=78&bDl2(J}J6-BxBe916GBr}&0UTSRbp^REd0h%@aFYV1A=z%10m<2;_YDIFd zy(<0|?rHdEn94R93mMLJNZV|+Q~&&Mlt|#J;5xr}TtUr|E}-=0+-)e0xjV8@AJg8s zdEF1BQQ4_<-85c0eHrQRt99(%GjJe?Fab8c-wu@`S-3Mk6F~2Y5ECqc-!*PO3at;x zP-7|+wYC1yxlhT$(?;F(f_EQ~+9+rcLs;bD%l|+7fCJKB~p8 z?vfVU#%2Rl@%0WnyPIlkute>?_cXKMgBojG@T477AiT2gs+p_f$;@ub%(rg5JmrdLAff^zFEB^>=dtq{q9ATNC#60coG?mD!l{p1jDaN9YUJ zrO%9d)K6H^Tb$Ib$_RPfQ&ps#sRTkk%TPLI3Shj8Kscq@ikW!M4jAV;PCFxg9Z?0( zPoa|kV|K^llBx;x@^XY({&2@(DEjlz81bEmB&xZmN>-q?_@bsB^V4T+I^e91BS2-S z5B03(%ZCJg;6QOZ z%xoF%tmr!0@@#is1iu@Tct?!iA%4%@Uw6#ShuKGNR+dx%g|{_I_hkk!<4xK;VzF zO*c><%2&BRfWu6olSyWEL4B&VS$R#&5%T#xp1B@*2Dqko6)>8zm&SX4c3puic!aD`&}f!= zzuJ<}dL!wA8JR(^8>Y+W)c|)(n1brz4nZ*N?=(TC_*knZoLcQ2?#|Z+Ot_R3k)3;T zYA_v!BA`bB6u)z8+pjHGk5GSO?yh0Xxhv-^Sjdl?T>G}3z^(5IllGhvIrpt$giEs^ zY%q^i@etWVxg`I?!~VGNyBqI%DM`(sx`pLOg!r9VqtsF6E)Vk+E2j`XbJW@!+<~{Q zyiVD+OXhy*$8-DL@LVkmusIm@Kh-x}OgW3D^cW1GtRwkjyyY|bQn~SDuPT4DiGl(f z8(7|2g}j7n>Y5exdBcHX+f90II;I*KW}eb3N*o8oj1nf=uhF6t7RS}iAox-M;!rZe z#BbvIT;z;!0ERjn)TD!f`KNZA?WTGN;x+wIZLOD5O+BCAE@Va&U`R;;)T@UytO-EsFSR7NV>6NNezXsZ z)*i!}-~3kWQ(@z_H__jq`HiwCN9KHnWefiFG%sM6Tl(q{WgZ+Crud2R1D}r+7bcbL zmn_f}O1;_`pOQ;%z)u-nVvMS=_oLy?5evSKN8ldA`02`y9F*b{KpVUk^B0K3y=S&Q z$mo{Ycp+^8si4&EmDujF>f0P&?UAtN_-iU^68<>diz35~f|ZFVFP%Aler+h#qB)|j zJp+OqI)=Cem*YAw#TgrOhx9gle`?@?p(5O04Q5s6hUkPP=aJj;}q$crl zQk-|>l8Wad1R^U>$TM}HWhm=}Nq^XV34pr#Z2LP%>T)>aJqSY*F^3u2!MU&t;6c(>+#8Q4noGmJaL@SHEZ_ zuk4VcFK+|oD4mOm?1jPZbqpGBK=lMSI{52ublzE&xksh250U(yXGzCzIz{9VlM{AYHdw}{=&=}!q06a`@oMOXkr6i>n6 zEud4%h^G%#0hzhZ7~x;>G;Qh-|I_kF_WW4lWO3b9Uv8g4pH5owEI9VC&t;UjvcMlD zZdnw8G)10A+*HX1)L^yR6%xwJJ8+869CithY2tC>-4DHJX26#{_H7fFcdZfer1D! zv~gw^ifbmPGCQ{~=<((tyt09;U>J;JSfKK+m7UQjW$X?=xComSRq0<4%V5xp=Jb6EVO^X<#%r5-N1FC7`T>w^E z9HC1tJUn{V8wmmgZx(4UBBs12c6;5G2MhL#&3PPaFV_A(aljf6K9RakDh;Wf4vree zW4x41{OZnJxh8`;Sty`bN>uO?x~+??f=7;L}C6H96x!o%^RQA1E^Q~eUf zhtEd+Xa<(Tn*@eW_f_qvke6;m9U!`*4yY8`5V|<~HSI6g=VOv}q!&5u@Vu;f?aJ1) zb;92C%hITp9Kv!RN(+V0IoVSut4T0tig!O7#EV{$&jdhJUYXI$$F>O(Sfb{Y5#s-O zD9ywS6u*?h*K`mPlLK&nScv2JPEG_r{BvEEIAAW=)#GZiVp?UOmp-PlTxgy;|~+!ZD|sHn2%@NSj>`C|TY(vIO@dbSVgy#8Rf z#Va4UFKEMTY5KzlNXUW_0EwrvE{{_^eQ&z@cpG0Vrao1aC7t^1rNfrQ@ISBeM2wuI z2ea^eh;)PM|-)rzT-kx{9Zlc+IUgqk*Lz-q7zy2M3cUWjRetSJ1x0ExDz;#ZGOtWY@ z?Y#UmukD3^v8wIO?gC#f>xi}>O_X~W3Zp*>)tYN4*Db86kvn9u11N+ANaL#PJF12itF0af*nQFy1m4)X3b{=OwGu{Oz{J9KCMtW2 z&G=b-Jy3k`Ep~e5rVCv4eLt5S|N0p>{qRiOV(4{6+hiC97*j$o=A(Q(sb1XT{{?eE zUxY8?N%=UIpn85CbhWhw`h0y&cKP46)|KGtI^de%32+^dUSkpTxx_nJkMH60R`~cq zX1%=y%5if|X7%5-)}7$tBlqMNpTOyBxf-U|!+WXwbAR;y7HG`=R*4P{)Ag|9)R+hNH1R_vCr$9>baU2H%YraKj&T z;O@$qO<;5l0se(Y^>4YhPpIOiYRQveIHtX^9pgdaynv!=k-_}lXI8WG9d&wtYme4v zwG*NJ`|RvXXo{fl*wcEO$MLC`cf0!JlcN0ORb(J3<5@9$>@97*1NEvWZQR@1pH3r3 z^bU8x!$1nWg32s^g#hqw=lK2#n3~XcX`rHK=-Xwwj668Rz@<&-W5%LQ_-m79wWRUB zL8F(08t{d}uKqG1onl_NXvQm;Ez1?n0}FTio{5wGSfMbf)i@pZgrEL3n0+KVggRhuZsYV6V6NY|P98AF zgozh0ck7Lf*Cl7#FUKHJlY*LEnqi!A^L?2tLE-nVMX&`aZ|#SBV?D6J!O=1|)6@N? zZ);PK!eP7pfm|1_&hh>6)I9VLe&67yE2iIoSSLDcmToVmoQHwBrB^5noSwL%w^|+1 zgKrsBvP194PaY?GXOFfF&|SZf){^_qlyz{u5aQ|xA;wKPFBIoPB1XN~f17k%)Y^p5 zUIai=e{hAny(weevMD;VQBJpsn749tN04?@NDr9DO*~dsNxzOuIwsrl5I7xw7u$60 zsm&_hs9(5jR}`R}5;BMX&y(`9=Is=#=6ds0cLCLj`h(tgFdG2#b~>J^YMhJz`AjbB zucBAfq~kuGsSvW<$a!9^L(@jRIgB+BV5spq$SB`7n$ z;1ir6ME0WhH9^Lmp$|}I{xnFP+bEX5BYK*Sj>U<9&JX}!NWjx}5e64=-1Bb0=?IY8 zfW(uFeLXMC1Dq|s)(eko;4EAg_PtT*5P}ODtl|tO03z8{8ljszHZawPfx%XWvK`ej zQ^e@I1G`ms*@0r7r9JVS)u5Hf0dwaan&HP5f0B$k>c`t@90fC9DVdQLRT6Fc2oJF;yy%C}Ib z#c`jGl7AJkin2c;VV%8!vPY10>*zNRQPu&rgs83Fu50yzv#T4|*A4k((|8zn)x@W* zH2N(0H~tuGtNy)YZS@)=j3*7?`UtBln3y9)AXI{+q)8)9{NHra`Kf0qddw zmonfJ|H*p2H2g%II@_=AtnnJ(>nZxG)ez`?lg&33O0XKruu2pImue7*VyFllT{chS zo0WcqC6&4bw@tUJU$jM$L;vr2#8WD; z4cY>C%gKV=rhp6*fLJ8;%CPUg*n*D%pf$L7q4o~4l44dZ(!@2x4Obbn0wFZ#UTLXM zBl|0V3zFX{!4FZ72^8=^Glzys6-cX-QAsxtYLnI??}cbmo|d8ZVOOG&#f)V(eEJcs z0#r-7sw)|EWUa`2-_lMZcoc@Y2zv}+Hy|A*!coEW2~`o5@$PO)5xE-Z+FHwjbYMdv zfZy2k?f#I~`du)5dpO=>&{Pn*5ym9XLM(^q@AULEf8FgJ=(qBJrdMGq8TbWFJzmxe>H4YNIGw)ENN#p|-0BA>h0@ZI$*7AiUTdoIw!cyn*SqE_qsOb4z z2~;&=T~MqUI#zNx%_fJsdlt^n;kfX!*`pq5(DtH^j@hrYcxFmv zm_Z=)?l|YGdZIr@l6s&679a&IdmQmvpolD>B^=aK%gTjo(WFOzpRc1S@IB%22{^u* z(BIk7L(Pc1K$PXTs|zVN6dB?7YSjC@SbFCOeMVC!s1!1W%-#tWSM|s2V@wTy^%_xHoliV?;p0&!QhaY3Zak=b-oUQ#I$iMy}Z72V4+aJ~Z4g zC$KS$2aN(G3-T7&XO9PX-rdWsHZK3Ifmw#9Qg6D@6anv8 zShMDGTSt?3x0@~ulD%w)?OP9X81;Hwi@o@Hrb0JQOkzBY=&7a!Kvnd6n7GYXd(6n; z;v~G+o7PF4DuUaC-*Oe%0jfg0#s102KcI~3D??gPStV0zc8Uc`w{0R;jLwiUDD5uu zdX7p1gR7`WtM&YyIL(ev{OkwC@B=3k!0%G=sgeds5JVeV0|9IPg4-mmH2;!l)SDFp zF~XyjTV^(*st+-1B85^tJdFG{;Ri&bxpv)D(u~tmw~?3H>Fyma{ji zWcOpFucXG&iA1js6PXZ?*I>z8p&XG?dl6w>$xso*3M zN=@l)RxOj4wYy{@%+DpnM7eGqGM`54%sf~IRfsDQUUB>tcvyYse>nbWi3BqY=bSr~ z9L&q9Ra$qzT7WBpaADL&|EeS9h^eGBqM-2li!5vC{+M$;TA?8<>zI+b6J$=`zl{AgG~N zD~y^!qeWNslM4aLGj8N)9XGw!>TA;lnoMul_}ro)m;g0K|GHvPV+C5FK?9N>QkGAY zmX%MgfrmC}#|5KV3Iv`T#EBtesxqK@!x`LPx}#Nr513S_sAbB}gXrL@%!AkrwJ!Ar zFcf>jRP2n~m&pL`F;$~*Low@UH4^_m{i{uWYlp#Chs~yCNagCc4y;&Di-4M0S&EQ@ z5nR}+`eKt&e!j5HBE__P%+f2V;?%oMvJ6YwfCfz%G_u6HzikTeIjKPn;;XR(?6Ek6k&dBGU+U(?p3mD?JJ)0^35P%12EzR=xh-|F|f=Dp?G(5I<$0-8cB zY9=!EZydcR=1nlKr5v{(ty_4LW+Z8Wk57KtZNr&#J|R2jB8Hm)z=}o8LoAM|RO4Xbaz94iHXF2J_kS z(IU&$qghT}V#+0@NxNRrwbhXV-ay4Pca+mAvRc_KAbS~g4P1kbW48*zK@ z{|uh=N^zE_bTGv!-c&W!0=$$HQ~9a(frsds5l5}U=xQk9mcTT!w%FvK(b>b`*`cu! z3VED@fHv|u2?@d0!CckBki!U+^_5NLL}%4kuT&kv<`x$#6XaxlA&=?ICgA&t`%{5% zpY=14IL&&gOf4@Bb1mt(_Pn2V&a^i_O0R5dZ?H9Hl9$B;Nugn50b*6sCd~)+Qlhk& zSbNUAB%|80d9+ummv!c?d8OZKP2Yuv`& z4-AqxvfYl_J3`WgM&5Ud%%%ojM;&6ID-ZqTCzGlY`j`%wd+SB;5@nU@HN;&5uf~H7 zeykTeQzK?sjQDy41ImL;9ruV@cf=gx)WTyb)z-B*Yn04y?ysRF|6E#W8uGZUsLb+^ zo>z~q{C+9bK)gk0#o3=e5+6oRimjnq_k)=r_9Ub=5&brG@S8k&RZkZ;#SepsC1_pr zy%M`P$%@(4K%iY_d#r4TV;7~M-}kl-U<#@tENrGY17cUs1mMPg74A>W=NUWjkaE3; zeHRfe(ku_F#2M2TsFE4eB_TcY>+gB9ApW8S45Ro|< z!qYVQ)|INI2Amm&kq^S*Oex~tC*FW3di9AGkrm+G+(?F^D(9d(1@Sk0#?L#AKe8LH zQu}TIwOIOFH?4PHXoS$eosSp5m8S1^z|U6Er{@|m3f!5&v4fkeYHJTLyl&P;x;J#q z1Hb*niI@soGj`Gow?}n!apcK~V&>B1SZP({Gi34p4dD1k4ZCY<*^6xHJ2M{jSpJ%f z4KyP+b_bAlm@8NP*|CGbwfHE5tUY_i;Yd56nR@5xQ>u3K*`z zhQe5@<0E&EYQM{90j#8rXbb$l`%1mxO1_xX5rW+Um%z1vN_%u{YpDXC%Omw%HKr`j^PMkiV;7gT9k!g5 zbc{zJbe@pnt6L`hMQB&=Ixncv&?yeGIYs{T>~ zbPIkdn^As=mYUbbo5G-}Y9~Nv79?I6=)Xc);*n<9Pnrsed2!tDv zOHfAJDRtQ1euDEh;Th8iGI3i&wW=q*6wAgw+8#&EKvT`LiOy|SnZ_w}ArAZ=+acu1QY@hENKN~4*HO%(V8k=!7L~k{!E!!&LR+^1KwRcP zHk3fUrpDh-`2%#F;x+}a^S>zDKwgv~aabT01>cU~c9Wjuab|nEsczp_xTmhZyLfSD zIC*?EFnG}n9|R|ee|P@^)+u-aQ)=Dre-*QDnCW7Mo?MDxryO-dcR-5wYrBtsO*j3Q z51?oHlf<|>pjS1PY7Wazc|@cJP$bV6n<7zW`*4NE)yM8R^Iy}88;o=cS2osFO+FoG4#)#R0>|EWLZIlWt(12k zyGi#Q3$HT!{!ZG_nYGndkF^FFJ)zPA4F4&Bbwrm5 zP&$Sg5Tp>AfSVu5#)%Cwpy>3hx^Xe~_wKJF~EqZRR+zhb4Us8Fv?Y^I~nI|KZxrDWm_7iv0MmiadV%QIP=6(f{Kj4aNVXB1`^9MYf6msL1)i|4T(itNmXp zQf2G^t0MC`41QFk<^Q81?^)}5Kr`OJ#2>&lraD^bf9DmxKm&_sMS#r4$N)T^yehSsCj89{Y#yk&E1t{6ey zf@rZnzph2NXFfkM<-JG4{3gJUoQ=-d26Bk3WdeueBwl&-F?5Z|Z*Ptg^W`oTRXssp zxstqo%fI(v3~w8kWvY4w^e)}-G}%X)TaTcz#S%^3#vZ>s>|=R4tud!5x&c&#CYiF8 z-^TeL(b{0YkU2D(=nED*OU7=v5eYDRkqOFLbCY#{f1ki3?8FiFWpI$nLMmT}W%Vat zh5EJ+Njen}!vd8`t-8+&M5&|7TLY=PuvNNlN%&oa>;asZv%r*HxzpUTfA_m#NBZlMnbEjY_AW=bQq}L|kP7Yc zpm=iMQ#2FE6hE>^xh`A)<<=q&N%1a>O24?d0AvHR+WA;{;M7z^gf-B3bhf4N!| z&5;%46mNhre1Ii&3b~H+tGC6C9pb6Dn$JPJVKwTEGg|Q>c-o2eHoiIX3hzPDD@GO{Hj7fu(TNsr8vna<$5u7JwNV{;h__P&EYMdg| zk9ZQm;~+8Ux92Qg_xHU((ZX1{87F}d@0SU8(P$AC@b)mPID}9^HK;LE!#ISD576Zu zuQ(thCsKlB2Ww4=yO_}*6+3H?(Q=>{hsTGps1DTNzxvU{7nFSeM?dbW|L8}Q$UtR^ zzU+2;X=9GsggI8Q?DuH6(10qbpe>o)M+a5^Y6Iw& z9NNd~--v@QopL@UMN?)d{&zcFwA9v&U9USWw)_CM|Lp_mk`q)=0B9Y-`ly>rW(E)` z-5J5#OipVmH5#jD(@w3ojnuErjhR)}fNAX4YLwK$^;d!nJf7GA)=ynBQ#kB8#1F-Xo? z%2f4j1WhbPgcP$>0E%$jXRl|Z#z&v3HJx}#@^mSmHBEWQ^wQTyh!a%SRzdhc=ugYF7 z67+9hMZPXguDYV9>{^wZMFmtTc2HJ()4LY5Ds-oo*(U#(1E>=3N;q0L__8p?b)r0U zQ(gU;z;=cYaQ~cWQ!oCiarI@>4h^7-50C5w?;;W79yz2?;L%T!qGup?zG)=)IVY0? zQgRTUCxzPUSukPGXs3r8$Cvz;2FzIKWc5?fSxexB2hjA7+S5n}|0vy~gbY`8KMvQo6@JEAZ+RB|z z-s%4+2J~onbKR;g*-^~yV76QGztaJU&a+R)4SDcm49~jfKnc@?>83JP7&}bw!`P$@ zG1Y!pgE${mUyN$5q*uUeuwcy0W2H2hrrC37qmL!-|A2iN7DIf7S**Jk+t+9@E%!EJ zww-cGfb)JQX-#0>?GE=0#Cw@u{q8^lBLB8#*sX(kn)usWUlQkdS@6j1f1!TGt$lf- zX#g_}b6V~QE$V|xxZ6HuLugT8J2!w6C*rLs^8NVI`Vr1`>XwI^XWGhA)3Z^Rurw&S z>-gu;s4BHLzrw)^(m6+1$REL1y>rlN0gz%TLVWVVke=wek{32#jj9ncubn~=GH)Hm zQ4s%)f#2zJep70WZgfZ0<`{(njzIZZ&%+Tl^XWO%n-r15*G(msrI$>tPXq`k12((~SR>Na?02V+#9Mq2kdY4Wm~3`&EDWg2Pt->+S0)AFyiD zdXwR`YlNUW5FG9OV%GKclPPDr`s&8w3U2(=uquPQHvAna zqy^|v-E#3Jx^qVR{+YE}?$W~BnHSx0a&SC^mv;dR%Xr?y;PR^>mT`aY8pc^pt&~S& zUZpNed42J{DI@}^C`xc)re}3e6~HUX=+q9LGcsS6d9S~_UpDXha#yy;a{*%UP<4Z> z6cUnhxLhIrXcILMgh0$dWrk#kehJl1t3SO+dfEmlM(LPcl>80_l+;)I{*s8s zxqU@??EKjZYfCwP+!c#7nI;aW{4QcB_B5YUdsoEqoWm}mw)mA`&O-DX#rmfT^eIE`W*q{dSXL{Ev=3CI|k1VL0JOZD>#Ab8OXW!kRbg?B3g8 zDn&+;6h&?sEo|lAhKXmYkN_lp6yeBrT}md4#$~U2oAdTE1(HN_KOjx zVGJZt3p66_`WvWTMzAT^@4;r4XgVp8)f=_6n$+umxzG+CuM=lQED8_dLA)d5k9X#< zorF+Csf<2nNR5b{5q7zh12!X?UR@NCSMM?gfiCc| ztnx4rtOE28pZa279wYi;+mXoSK7DqjUei_j(2Nu@e@>A|u{pT?PG*2XhfCw*t=y=` zNc-1n^mD3c^=?p;QRK)9w%De>qyJ!sJXwFv~E@F z*gR=nkpF%uDulua&(c58?vr-;jU3ZF8VA`IkP)(aER9LQNuxSM=AL;1R+Qw`ra1~@ z`h}(P6#x_4EPp9P_8;&aQ9_s8XyRC0FdN$P72rj*TvM~tAM%jKX^<`!_iA-Dgr%c> z%C`!;5&WGwy!O=n%(S*WHruayq_gw}yc{eT;@(=7@$F^XH|aBq&V|bNMG3NdM27;X z+#oGAtXxWKxsRFyH_rwpaTnKHBgeXnsxFD&cAUr;WWu^d`8N`wT}N*Ji(v-3-m!W} ztLE)GsTO;}x}>96L@*_WHh|8oewewu8*kbxJqd;UHbC<=k~K|DS#O6W!ES%&6eF)Q z3*_8xhKM#iY1IX@s1%#z`m@w~?wt@oqR<5rJ40q(@0&o#Bw*67 zQZ1|;zT9JZDF&!BE=&`oY_Yq{q$ zJHG$D33nqlw~j>UV0bZ7L}{3a>L6_BDxS25;OX))tgw>w(qOL~F-y4^yX$8_U>P6bU zH9qWb%Pj~dC;)%IeiS9WI^uU?T6p6Cc!{{P+L`%NX$_>903J)3=@v@3@>(#JvT7Q7 zWw|8ZI}Hpyr>GE$`5=JuwBKU*JOLaheS>Q5A^H9jzBN2Fg^=&C6L{Tcu!b<0LP0sAEM`WSBdn0+$Wr zVip)pHNektE%h|(W$}AqzMoyFoevAh%ymg|xQ*bMG{UO-`ttBf9d~K2~P$A@24F`UdS%jE?>=%(| zVAm9*Ns>^&vIc0(R4}f;w|NRjVDSW~uEK9$p+3NewbR@^BJ)$Di++G!8?~~u!~u(t z*;76;E;!W{D5=6b_UevWq)BB}k5EssSDlJ9Z07!f}nUqNuPe%O*>I zqI++o`|9P9GDrEODwostrjG!m|t7l`H^y3rc(zeJ?wSp{hc3r4?s?;`I@la;P^mUmU zfmIhcSAg49g-}#rk0tPzdW(HwKP@a@$Z~L!nQWWYxdmISQlx$Z695i?I<4R|jNtxh zw$g#s+~$kinHo0WRp9=V6wy+XbnjoJOwQDGfJ?g~_nw*~FUoUbsC zwZJqree|=*w|Y@&g;#AcS}cl_kSza(L;5)0byTXmM_YSz20#N{(YKA)$vE7~Gnqhpmx0PfBukCE$lB`)cX1{)Z?xZU2`j zr;pPB{f8(cH2&kdJ7)Bn`iI4UNMR`y;ntof7c%KNV0(U96z3Gv?i?R%Zb9~X<$hXp zM+`c_r5R#bcy}puR8HaTgw%1obC25TVr5R5Q+zGdSiEo8)3i(#i+?6Sw`@kI9MC1!2}~||8=F?`7lp@P~h@UEOv?~&=SkJG$`4m$&r2O8MX85n-hkE zMnHF!jXX*8vi`gv0h@9oG><+R4iHa4B5T|L449QScUPqvfk_j6Kw`B_9mnLSjzNZ# zWG-H#cme!`tR?!P6(Z%dJ~P}=>V(6cRuov3Ez@9BY6el_h6NwWldEX? z3Uk%%5U3!U;M5zMnKnfrXBC@z!FQBvP~1rI?1)+xwLMyP$R3qX25dWWx+4O2T<|^t zmTKD)&sdjM5r-h~_ zOy0db?hwern5;P+-{n4MG))j%P1NdI`Ariuxnc|czj-q9|KQ|B9{`o(Z$rV@+@qhq zjJ6(pIn# z1f_mG(&^)UD!Tg7HtB<^)UKnGnR1GDJ{4uJqfrQT3EMdwRGGas{t^ZFO)^0DX# zBpj(pl&8wF?Q$1^+I|?0()cm7Q^{Ug@NFTRksg#>cWrMh<1(GPfl2i1HaUoJH2d{=~XxZ4I*k6V5FU~yK^*%=d z8h<<<j=QSMfj*} zi_0!-pX~M26);|nfkmYRSI~-m-;#f?X<|nAx)|>TP%fX6%pAI35V+JMeHj>Ksu^PM zKC`Tgl6OF<4X+3HI{chKpA3uq^jt)DR1+617QBI_>q}&c>w~&I94NcI!e@8N(DiD1 zoUFl8mXu|QP#&uCjWP1atqE7M zqqN#u44`{Iu}^=0xOCYxG4z@BRrgl`D5yp~9-8q&{l@9{(etG6#omL)JhB~761}%8 z=(rtW!#|*|AJ+)xRuF&Sn~N^IDP&I@O$vOc#}mqdu0+4*o$PI7Bx57v^r!!hL144f zeT%s92(~Iz#VU!QtiGqQ$FwOpxK?JOBb0Au!ZE&%fG50-?n2fY6UQ=QX0ht`Z+H-g zCjbS-({KT2>Z%EIUNb?ox97QgG#_Xb!62(#ChOwCaQWbYy>#it+CpM+bIzMFkKD&# z^hh?!O#EV6+?wjiTM&^p!*-G6$SREFZU~VI!3etsnjfERaoJT-18HWPA4)W?*~b4L zkNZOVCB#Z`lkJUW0Cu^hKAjGOtFOoedjyDBzCpGc}g*pZDYC)^s|E;Kx1td+p}`@7L~0Fw@|$FeH=Hzt`^a$PNH> z?$-O)xf|z+*mC@Zq-hdWclqX%Eq?^b>h+yi7*y+lW2j5$+eup3%x%i@x{!+~W~oD<}5@IZ00(@xh_IaPHY3HuO$;8#G3_uXaBj~(0|YiWbp z7^2&iMqW2VKx0w;dglGt8en+i1!DLo9_kQ@BdLg1HHx?*qt9~!r${KJ4U7-jrMm>S zyYS10YjNG6qi{8Qvmtx6WK*wF|BcC z+M!8~L6b`uRmbv4U_tv844Mt#@A;#>JmLPrFkIGzv&~w{AUxyO8{gx=sbd*He&&RI zDV#rNyBy-|FeJOEb3DrQnb)lYrD_-^#*+D0n|_RC)Dm+HXg(sm4h-CR?gkLn%j?*% zbuUX7;Bh*8Y8nDDrvnJh1`%`gG&%l6xIFgsLqjAct^q@(UYvYGBxIrUec!j=dF#wX zEzY!gJRWd9Jndwhu0GXgX#LjdljJUZ5Jx0@8EK2Z8vFFBy^*{20qIZU?8N79Bb_&@ zDfT@BD)q`GE_g`!odyp+$oZX|2jiwX0xF*qq}hqS3&@cHuOf2UISW)H{mC_2RH3me z$LM<3)9NAL->0g4qJN?F5-4HvOh(AiKiLBeVY*x~S=cq90>qi$KF-1!AXFldZaP>X zBt|XJr7Ui05LAzC&2aZ>k^`(D-Q{Xx+)2{l`K7PWu-O8CBf<0<7=S*ikdZ{`tOdTo z^F|m-04k=r0sKTpBS0tmC*nQ6+Y21kL_atwdJl=1NxEq> z2wnEb3z<}h$t+o+t$OIZ{E#@@mCKvx3T@Nkzyw4p)ZU8@_*n_8^_g99W}LntR+PJdJN? z(?BUrx=b<`grOSx(a>QWRhY1YT1hqnNKU!?`B0|4Vy_=gQ=kF+#zjRw`~m}XH0t-Z z&nD!LGzQye6oC=0>R%Y=Yvst(IA3LQ`HJ&>w)5VHszn3pj_t*tZY(ObA_ubOCyF2K8?@#K>J+QTmo0j&A>s7rH;kO|(Q=Pk$ugGH3JF*rShlSfSDexUx(^HB z;b3g%h~{GsCUa}VjTY`dHA$a>Q2l=6P@(7KKZSnO8MyGV_%&DBG1Xk(S>NVFPs}uH zaA3X3V0Qq$xl`cy;j_T4>ohwe%YE$fuig-U>6W{HGlaspFd%4bPfEEUAy1}j=?-o& zBMhdL_cb&m8UzuT`gc(Ny8AVf(8}5C<2LYO8q-3% z7(@>0ZQg}^8s+SvxL_Ly!))XjBdY!+1dbIKJ#)Ii+EhUCwf(F^#NZdYEv5#@roX71 z7Pqel11|+2x!^66U*DDCI7Rs;_TmIreeL;Vj5=WlD{l?kUO;z?S5u3yY5PET+~a!`D|CYps!$@h93rikI7 zyMP_RAK|}`t>~atz(w213ldIzm^P&R{KP;FCvhIlq~KUyx&6M6%lY(DRluM(V{W9F z1@@Oi#y<$bbj04gKOi9ppHHFYx4!5Y_K-6$uq%_S@?y-c!&4`7A*Gr6)R78)5r6OB zn$b34RW#atGPhhjRcBL~n32);mE?1MGjom5C+9@@%*xam66S}=*JBRt`AGyNI0ZAD zU@EyBA#C$7p?E1rp_ll2Fq)Iu#@K8%mj)6_0Bx@&3hf_;Frb%Lda*Z=@Lmxb_?^@a zw*%(F{O;Ejq$HBA4Ra*j5E8LWwks_XSON+d+$HQTbR~JHGNuIg!UE=#Q8;PVa1CN$ z73_H(fkBTie;D+V|p9IiEo}P?(s7N+H+*emVU`|6>{kp?>!BUh9n~V z=mkOx{~6^@q=cGC!ZZ~y?O?}hKsEhB|5|X4>BmD^-NE-EPeURE_bfTVfI+|xeDY>L-`G~uUueS`-1$p2DZ@hQJ~jJt*(-^D@b%$g z!x^bJ%+Gj}DAgi4%g?b?|o$7x9Lx@w~mWyA&oa87fD0IAK( zWh->eJ*m!?($!rKocb>0&><(h1+JO%&T(${$X)?LS&~l%Rzx&NpI6s<)8`d|jcm;y zcknlx;$@#=Dg)w;p(l9e^vfi@Z^bmjTdT^O^Se0`aBr{9!0=0|oO;l?yc6Rv$U!9G z@&`Nil)ZNyZD*Hpz1vG%^8?Vcws)ENmnc+_&t?DI}QXDV*^?%C65u&*IYiFwU?c2UPWrgNOBP z*I0{9=r-_%GhqcAihDy}T-WDAry3r`aB5v;F{v$OpW<)5@0d2|03~lFF4JK;zeV!e zx$TaM+h1*U8tyqS$&go|Y^)dimr%BkJieDUB%5^_`H(?MNRQ;qgxq|pY>H7grBuI6 z9Y(Jd%rNysx(uIhPHu>2*FLsC*;#r&IX#|ierH4tlz7|{KLSyV7}=Q8=)wS~Vi5-g z9*e8oh&!))XqYr;;I%(=Se{0V1WwWrRXs1mH!k`Jks;7Xlc&QKOXUq>IQ<)9>@&t% zxto_j(ePS@Fe91j5oe>eS|frW1|bMyg!zHYc?gg>4+S#kL3s0ET)CRjF)`9Do;*9O z;_O}8C(4bkv~>`}FsbOfp9C?CDnSu;_-LSw=SMEQp@qj&)AN;tP^)?=ONmj3m^$l9 zY4d91AFz6I=EbWK6QtUP`h1K6T_lS}|#VJoTdZ?O{+=ex9cS3W1)SNiZvFDmu9=XDaTnr`Q>G*9^^&D&#U7)J<=lpJZVjfj`(VD}_LYLDNDB9ohbAn1cT+|2T z%xCnfg@c^=qn&>_^9D3;&OCHaIPza-e(6BwvjLgZ4SyHJX~3H_FZ(9V7e7abIt}h~ zZp_jufuwoao4?Y0&C{DSp9zxY(SMGSNj??u&{ zVjjDSQoy2xVBw3iM=Yk8XlodgqQ2E8Lx;G7VH>6RjG@1dAy3H`=fk9q(-3a~#%eV@ z8$&DoWYYfEkl5slhER4^g?3cF0h0k$GIxF&{WP`q-|kIMBlWZwea)y9g6;R)Be26i zhO3beyF+bBm!K6ni6v`A)tr2N_p+ebjmgbhLV&zuE3Jp3CfXuV6Z?B3SEPe2cY<|3 zUEa*kv|3m452!PEW&@7@cIK~A|2p#lAZLCIsa>EZ0sGCF7X>-s^yr&6_Alc-ce^SO|`z2%_7^B_t zdW$nY@5A8{N+Tk=dF_0T>#3*Of^Zvf3w*VY=H?Izpa zP8pr$by6q9$P?bN%^Un!PrU?xI3j&+d))J$D?)B7Y-P?~>J%K8JeeZ8*qV?TXTcSZ zu?5Df`=ezs3po%XNYoB-$C$KJypNm%yvjfC33c|=Fo#7#4$H?6&(Y_gKRYI|l=yWL z-5^Nz%4t--qd_GNWd+`?m=Os_=MJ}x2kVXhLcd>?bxF?~w4*8QmxCO@U><%TI)_p9 zR>-K1zCXp}Ze#bEg02ZC3&GpREGTR$rk{xW`i&c!41v98pXjrmL*h>F+cle%u9|QHaODtbAoSOVlo|81Ipic5R2Pl?^Bo~CC42C zuBx04tI@7rn&%!)n5b|+yhohOA^si*Rl=hMV7T<9zYNC_ zBd`9G6WK$eUU=$EM9W`{%(w<|maBSUBc~jPWz}xVKxw&{_c}^ONEf{Vm~X31SPhsI ziv!PvNx1Aa-{>B%jD-Pa^eH}JO7N~!VUU*zrA2ZTXop*uJMSSksv3!~VK3*mh93{7qh{jJ?al{(maV#w97&;@FDoh4WKPP3PB ztLa`QVAfO`#nw`TdH3ZSj`aaPST*#CoCn_uvul!>Idq)7u&4-h(3Z=|GMW0bCr(cV zjn9|YBF?A%QgIt~l|Fxn|IGH3)YB_w_MRnnZ1cLC!^EU*TE&mo9=7NJrrMGAI6nOv zcnxl4QsyDJgYipRb3K^HpIZD0Ye6E4zd#y(bpD&+X1&^&K4*`lbC3l{RY@Kcyu!+Q z^W}v=zC7k%U*7IL$d_L!nQbG{VoB*n@ewDAZ`?KY zY;N8D!k;#>XAJz|iLt}(L3y%03QRu@#OPDR_^?cc!-W>qWzp)EKvwcBjSkhuR#m_T zfYOu37K#D(RS!=*!^3l+KZ@jm-#88PbM%OvY6Hsnh{FC1&(buOJDp3~>O&%gj0ly>5%}tK5_HXc-|EA> z3U5;M0(-pxE<0{1Gt^=xBKwc$d4lkKpJ58iWGWxl9G3$gmLDQ9E&HMXNV2D~FfBrs zd6ey43qSwQy&qE|5`r=Y*Tfpv&8QDGpWOw_>d#LM)4^{@Xx(C|^M(jd2-Nn5{!w-Z z6!YF(3g$;JF09GlIvm%kr1_nXS)^M@((?zni8gDK0u_V|-3&v)XGSUNz;B1s$4RaX z&AKTJ!+#bCbprHtk}L}5s8qL@AGnyKhvDcA3IoWc3SRmHv?3MI*U^!x1&~+fU0U&w zy|}KW@YieY?YBE)i6SD^80ARNxyzAde|ij1P)Q%<_#T;W*lC^3scYFekxMQyGp%|1 zJict&PJo&{f~^3?O54-@a|(8h#NcUYXVxa7%k-X(DiC(yBpPm)q&r0xO-lLn0x!WC zG;`9)3Vy#{Ac$n!)q_#`LknUkY`8qxqrksAr1hK{#coHz0Oh+;;ww?`G+xjrO`MRJ zZyMIgp)t38__d^0OwS!!%ZiJaxh`(z8yF1eP9Mf-Xl>qtRuDF}P9qJc7323-KJffj z7)aj1B@AXX7#)L3 zXwcE4^3+$Oi_~VA;&s-;}z@qawTPr7jz^Dp~7n-mo2dM%H&Gy z4KU|V>{Xm(U;Uz`I@2F@bwsa^v;DkZcSKSPlf?m7_JO+gA#vjly4kGAuzjrx6BwSvt6W_BCEOxzo6NQ+rCq~GI@-Vz z7kJcCs`?wFx8KX>Ugzpz`*^7BB}YZRf9Qf3Iey##9`u0$D(@DOimKXPLn}6Wb&fhU zGtJ+oCo*X|Fw(Z~;N;jxCAQWvmUwYWo_VY1?)(JiM9F}nxtygn!O!J3uZ&$9Fh&N< zFfc~z_xO+^VODH#y6tSi0(oCKq$7SUMwmhMefI?b)C zs}E}1+%FF{zt06sd0ZaxMbGv;hkE02HE_`IF)`#E!0XLnL3nN#t@DW2T&!vve9MSl z*O{P^^;iVtcv!f9!48>Mros&EKh~ty7nX%CFMABo9xcxZN^;%igRNW^AHb-2njcfe zW6+-c%}JGv5!>2i&wZ3U1%)RsFHT!A*qJRlQDDnB&Gv<5QMEnBC)X0<1v|nrUqWRZ zaQsyDC8&q=w?Lrl0N1#wF~HyRSa;75x3(w>D3jcXVI%;@R*LXcRy?NS+}spj4*3*| z^z%YE3Mq8UoHkmZ^l2k{Qz79!)>v`6eLdj*ErH-M@tve|=v+Y7XrU4oO@IU;`$48M zY&ZhG+EAcl(X>hBh%#o8Uwx4v9c-_)j7n8rKF90%$)#QDaz58JKX3FO<6ozj;!Z3Y zK-K}&@llQq$z3@a*A-dEKC$-`n%>KG$tj)KBYdK!D9U2ziATQ695(bqw!pXdScl zSTUsed7~GWW7{56t|S;v)OPeMq3vrc(d+}pGU6?JopyLz=L`N(#p(RZK|!^o_f1FI zh8D`I)@puDpCXmDbC5GEf1$H#-0?|K)!5mhLJ5YUskL`!#Xi#cNkU^kMZnDrLpV>U7uO8}j*W9`E<=4dD!whgzMC8~5Ku zk8<35S&GjEMT*ae69q5n57V!o8@gXkmNpLQqVu8k%D$0Q78LKo#=Tgnd6a~3?cL`m z&nHLLOq~jx#6(^Wh>BJdYW6iK_uuB$B$5!k=nC89MRjy)QGvGRhbMn|BR;vs(I$lG5G_`gGnruDvli#s_8D?A2H5A640|I2$#G z#u0h1GXAT0CKtPtU}QNEwm3{!3-BqLJR;7-G29;&bW=HwCj_*;zuIP-Oz2zF_%;EW z17r_PrmAJ%OlNXX?2gv_x+G`$R*aa_kn!;a-MOS?188v^cM8aF7LuPMV&y7q_ny`l zr8@#8wyzsQjs(IJ|9q{_Z6x=MVmMkz^`YF_}i$cFr%WYN}Y4}4!vJG`n9naa5Pg~rw7%s3m_;NLNVwvSkQ7I zLIueP>#EmIXr2IwAGb;bL&m$%R93W-aA^-;i6eYj)g}Hes?hDXe3OyBD8+ylRVy9W zqJO9iuu>fs96ydwmb(Zqyu<$0<8+nyop3hmM1F1igGJ?j2%_=Vs&57`CWwN3QCGMo z;QBeazR<}Om|I9f4P_JUXYeb!#YjLbSd+UCtp2_2C?IgHLT|7!kI{ZXHiB(T#BhnV z=?eq)Wx(UJ!%zCzOCFuAab+{gE@%JD+guX((w}>NPe}r$#MwN0%W=1HiabvZ)5O?^ zu$uDHwe)7uWo|Ly)rBPHHwEWkqznY#EYnQvJ zGt9Q6T%snKPiuT_DyK!io_qqwPKkLvrv=EFjXKf&_bImSLq|=Og-tO{^dOqT6C9}3 zrC;z?l+lRA12pvcuXI=yNOcPO6KCOgGxfm zpp;-Pz3gE@lxU0VharKmjaHQZI$|!u7upXmgfkeX^^`aoSiWK46C^_IJBXwHAwaXu zpNFCL)#az(gS2o&{W+9SzpgjtZci)9+P&`XaLQNOOz6I0Y8q@*o00q%TIG+?DyjC} zXsBJbR1YTZ%(o4C?no%h!zK|4sk8F+?p6DlQQxt$eqj50MaslrO|F#*T}n^{^_f55 zzc04jn*IPoC(G=Dx2IS!s*Epj&?ZHg&K_Da^E;B`FS_goGxr+vHQFNouc@^El+p^Z z{r@keG!)iXMGq3U9flxwStfgxEHl0?pk8E?cel6eNO`R+sMzsrRQSuM_~IvPt8>|T z?h4j&nOqte04w$^{64jGFnHK7t}+AqcMQT78~ytO zxU@OVMO@wt8k_`QMWv2eFv&TCQ)b^ddfKr+_~C~gpAgJE_8iP)8pFW=(r%I{fZYJo z@KHE-6I*!&dkZlQ)xXH-d<_i9OMqgci; z_6?K2D#fA3ifryb+;tzVLJ>Y2m zHb?AgfvPV9WV|5)j3Tm7prJbx?LIJri0|e0`vxN4P=2g~3{OT1*SJRtjdJ5KO=g=y zY=(OFEz;??%^2P`u7@Sgknl8-G+MhU{+W-Jij{Ts`i6kkV}&80^_Wxk|6Gr$XmdD_ zeCeq+AO+GEy1TWD&lufwS={Ve4X_co2+A#Xxv2Q{HJEtAA>>^f#4`<(cYI%FrTwea z*Y=$kLpQPXf5mJ5Dr07rrmv4@&>box$+$o04Ir)h$ssJ#=!0$Pjq#)Q4>Ej5nT(Yq zr4@c^T=Ac1{!vH&$%EEP=AafO^&H{tkBwmFk4}KfpJfrht|f?r9%E7MYMv?O!ubCN ze+icb^wbq=q%6MVTQQ9*EQy@ID{Zi5NS(@GT749ZU**Hg{Z;&)L+4{hH2Pk-^1$-b zt|uaPk7#{=*50|~yQeYOq&aK!>B{qi;iUm2=FJJ(S_t<~+nWPk0Z2-6B}h%c^B2uW zLOOV8hn`J1A||`6lr2I-HI4B!NBFsi8CTP9GfH{*dQ&WovPdSLZeXnon%*KZiKFv_ z`$>15H<%y*{MGRd0FQp<^M9hd)e3E&aPQ9O8$u}TUP-BpSN{@Y6#GY0Y*}38G22zI zSKbdnNH8IH07AjP=PB~|1Q)>4fp!UiaHR^zs8wrYMBm0&^$eh`yg@*CAzuniiB6&J zXw1;0k*~Pdh{fSAMW5F&*%QPD!>XWUTXy4dU1O>E`riq#3C3RlT$Gz;wmg>i7XX+3 z0s&y%M8Cl=G&opXRsROSpso)H0DlCOK&vtt&wmqOlP!`>zCbggSK{Oj6y48$&0=FZ z0lnl>k5r?Mp-=ThH*t?m=qJ{~cAD>(;L{wAOvrU=A4c={{9UF(AiiFPtX&Y{zEY+V z6{8+V#JNwXG}qrWUEG6&1XmXMZyP3@yLxLbII27;Jx->kFkqw2e!!2plROkmk%ps) zjw`War4fGRrJCfe^Z}l!k!)ozN`&v%OU+AwCh+=n(Oq_ZK6Tt2LENNEgMUntw?w)A zhJuGMN26=W`;CJRQnTmx6p7)VE)a(auzyn0u_ly7W|Qa#scBVG~;{rJ=1~ zB6!l-tqL#I`B?pKJL9gfkQr}2F8-p0CdCojcMb~gj^ry}uGT+5n8pIjzAv%?e?cp# ze4(H%8pk_w_~b3576b~ZQLBH|CMqGLi3N4kU_C({H8WH_%?Zkaup9PUFHlDfh(ZU2 z)J8xdwL(}>NKI2j7-<6icmn-DAvK12P)O|-6jHlTA%=~cE$0wJCvaMo%*-{Q@=I>& zmqjOxB@$k6HWVz_IXtl@=D5?EtN*;e=tDP9ZH#-0CdyDDmgr^5 z=>GIwZZ)T2CUJX*FJLF@;+L&?DW~bAa9vS$dx5%EWA?y#s@t?)OcPDE388cAts7K zkP3yDoCfUT=f8bN)VG6(Q|FDyHeWnT*BGn4b-D#8=-6D6%PI+58ff4WcJm)YzkoyqCK21Vy%Z#sg$MhT!{=VrgM{u0``e4>-jVT}-j>_^r0?zqXmz8wAF zRSJuJh@U_-14`BCW1LzN{Gb7#N$b*NVJnk*w|z00us$UI)Q?G_8dy+#VnQZu{-S`z z)5{8ks~{ivs`I}t>m7whx5FZ{?ZAMfH_r_`GWmWF_hYxqZ@!{zhdRj2HbqT$JH4q=JJ^B|P zmS3=qekZ0Iy?<5s#)qkE{=9G|LqIOvBvR7{kUD2chM zmT*S)yN(8>vkB7)fOY&+m~hJ$((Ah{EW7IV406ni=nRStD)-+R+>Rsvfa07#_PMG| z8cI@Fi2m@RY}7uHa|tW*l&Z5eT(e2-Wz_XShD6LiwN~>Xt59I6&%T*a? z)ztr!RyzTu)dWg=85o}kR^QTUK^4BAb&h88ED_ntn19UkHb*_~DAweD&qvQ?4@!O- zr)BvH%lG>$kM7bQHXj+om3W^XhZ(f)1-gL*fN<33chsk6h_=%|F~qR?-Ck z)w=7nSxh5$gpH&AMoMk~^S4~B()*2E7u2ms#3~CuN)6vHPK(ts6}0cvQb-Qhh98Tu zKIJNgWGm~+hGltBrdx5qWNXw}ecdO8b`IoIXDPI9Os)&xAo4F7E#qh8g%?Td$tn7Y zDD5{s+5XH*lKSu*QojY;O#RLJ7fPE6@D~){G{1r3eIRxV_Gv#joCeK;d#A$$?_p<;HWM+WOXH8bxnp{3@g72JjJj;;?t`Q8r5dY1(qg%XriF00hrjoxVD$~ zJfczZ5vhJ{Ojb9}_uKL#YeRqcFHk)5U!ZvWFDU*#`41>&l>8ScF7x>Z6#Id9W9$2v z@Z5tyahcg0C|=-y51PWAzD?oUm9y3eD@c~0BF9u(WJV?McIY(`^W=b-Z&9E8;|X#j z1ja}ZJDuEawb$hOVBz8V{IB-D*HLy_D9o-+tyUF$tdademKmyOlLVIA+mZ6*7s?lL z*T`_>^~Z{lXplTC)ly_NbV0|5qQWD4N%Xi^ntq%Pdgm$sQ?L(EaHO3WMKNEQS27Xv_noKe7^dt2N%y?^ za4|QCj|=~P=Ly>h^v)CX${ocm%70if|H>OHHUqKZtr*C(3C_d_YF2L2bWNVPq}A># z8@7Uq`gBvy9JV6!H2dPlbEQvSb>QR+;^2jHR7#$LvE_+hJAqo|gb1v(nd#nz?9GzO zhk5_?d_3?tQS5LZ;8r+y_r>Q&f$~;Q$F~0kZ`D=uMnp3wmB?mk?)z1%l}w?zM33j{ z2(AMPBH<-2rF50khO3I#7|SE+ltI!yF1Wbowiy1o#95b7Wk@`9K~(q*CW$XbeXqdH zYHfo(<-p@JE#SW^ehZdoaV!h&cq%$nJHk`F6}88!x8f)S+9an=a$>3pp1|HVuSuQ6 zN=30shsc7#7SM+vh*QO!Kki~D^)XxD#feyP9_zA^EK!CK1ce`7&Li_tvm)o(ioNWg z!1y9Cv-0o^{e*bnJV%X^TeZzX$vCNhufKsDqz;f}15;o0eot+mr89LQgE@%220rcc zq_A2h9UVU6=7k9wjMEAg`qvA7jP7z7?rCgVk+qX-3mu|Nb5=3`apxs6A{a!AYNNR<9;71<>-v&>*(JuJLqzyrv zfrKrg9xzxq#b!B%g!_(RZhw!k?$ad^5&NfRMklvt`2!cMsM^JAKap4u$$h88=;f%x zPYo-v*wvG|!C@(zCdV}sv5PC;rVrb=wpOnl+?cL;JZ>k`2gzb;GzKNj%{h z6E0x7^++>m7u0lmID3Budo5f+H{&Usgra8W2kju@=hf6h@TjK0Oxh?0>+YePQ6E~hv|gMLHA3g z@+oiw&tEgrsbqz+|MhrQ#KtL8-9x@=6?{NB0pB`LL7Oa!k3=w8K$}sd$E8HzXeKTT zY5RbE$+@b|_fOc0dIEj25;pB8{MO%SZH3cuCe7f#ZEG{zGK=xO?I1$)kiu;SvV}dw z)N`AV8gOh7%JS4pl!@6po|LR^ll8O%T?&A?lg`@%WUW|pX`FHN9uqx_C=<_0+~_vB zu<>I?u_X5O+a`fRIdgDQ8FlSziJGcNZS;K^bn%Ny9~g6; zm8kqDLnF|-vsV2hF3n7H!Y+Q&%13i~oby4mvpQ0Q+KVU*H2bJMzlDk$HWa!D-=hGX zT+|SFGK57dW=Ooz!iM60*v%x)(8=`6(9kw1thm#BWm(j&I$r?p+~cenGmajr z3(-PrYUc-)>v*V*k|hjSND(??QV|Z3oIGdnkBb&v)&c5ju(D}H({jz$ce7E=;SvQ4 zg+nqpi(cQyXPJac2yYfwObWP2K4AcihYd{*jt0wwmEg!#`oy$K90HN)Dy=geuWmG>vGC$klaYFBLnW@Qi;L+35)56=3d>XD*8$Mat8zpM!`V|2x`vIH2X zlGvKK&s&L#S8Q4+HiMhq&fd! ztA_)boDnZXC^fjY>htrIczQjM_vt#Nuync(YY4@^;eKW=AlOFdtnJkruECJn<@>cE zK{k4zK#`yoQaJ9E`8%=(-Y<(C1q&EXemA@h{+@GLie0NlB%;$1L`ir|b}^6p1wy9+ zeZ8Ok)^DgeO^J>CWI9TqAQsZf`GqKbWx(QuU}QRQUUQs;JaLO5n^tfE-b`W*Sj^D< z&_>S~&g}fHkVBsW!GRk{a=eS@pGhBA6_OSRSp_qGjIoUdT*I;w;YIuam4%Ig_IJJR zW)>{mAE{|}o$afZTXqpIi82J`-7CW7NXOW2H}_Bb-HiEN7T)c$*b`BoMxGZ4qXpfm ze?&U(Hc2Lb_)XEmb9t5zQ1LvF5sWscQ|7hyO;RR)eD>}d-HBMf+|56TdXjpH*+R{Q z8uYsu0H*-nvx~W$i{7I`2V;YcNaQRd2*EOLg+EUhH6RDU;5^b5%1fV6G=zp zGI8%SpshPSo4$MfNI+wob5}154|bZ^pLqJp)B2nA330P^=_I5TK!LJ{@51|SP=O+^ zP}C3?#IIZHF=7}XQE_l}`|b|zi56_FJ4k93waZO0PKe^9Ij>j1n`xnnJ&9z^g^u)w zkPS!ZH8G`fxcfuH@;1S%dB@XG0lV1Ni7JKXeLoR5Za`xN?}>n1u$T`Q(tVkF)XsT3 zl#g?5Zl{}J8q5n8VAsm5LK-GZXVSaEOdWzg;d}v@t-^HNUzD_H;gv#7Tf2y{Oz}6O zowpNrkYx*%(SUvxALNwzcYcEJ-jClfV}!&GsiFGxp%3N`mAPk%6iv$#DbZ%Bb_t`2 zVnOeg3l%wm7dg!)e^>wDq$B!40Y9K+_`5MfeCZKFlG%?`psb`)Po|KLv8vdg7C~1v zYDgT*T^+rVr+V)KBCdxrR&1C9>qET+icx71^<|UvoehzxR*MZ~N6VO8Z(&9a|7i94 zI5-O7Iz1x9N5pkvW>WNrk#VhfpD1;8V?n!0-QqtUIU%r@^hr|Tx})lMgtF9Jj9fax z(2_-_bfJP>fbSA#Xt{`zovNN)Du;%>#i;~wA6`AmkOK@bR~2}6i`xYDl;t&cbTkD^ zL-JI@+v=OjM7#_6o2u*MyC-km;6UJxLex0(RFdrQp zH1Nv_^2Z*yqqTcH6$!$~O?TMk)e!%zMt|@+VKQqw04{v&^zw*nbIaLmSBxmug14(B z^?=8e#?-(t?>B6<7)oF3E!L5~(x6wXZ4yBB9=*oi2<7abc) zEU@|{S!%HK4gDSNcYYY}`dy6qh|r=q&-BRF@!rAvW`G25&|jc15EA|a^P(}T3c?q4 z6(?dEkUTJo5ajFVlSf%W&@KNmk=})ivdVVq{goW5jfvgvWV2y(;K!FFNEFqtr+Q z&}VzA>aWEUx8>D#hS^?HxQ!ePHE-3~I1FabReK;WSnhz|*Pb~abl-JHS5Rv>#@Zki zY%8T^bC+duBljn&A!Y3{Q+_%KQn29ES>fg@fGwhHy{6N$`8k>fKk5CH(gZSq{xngY zfG^ilS*T-q6&vs<2;FcPEziOZC3lY$~A5@wVK`Jh_ZZa?Y}>umatjowO}-sc#Q zQ~ig}Hw#syc@KRw_ox(N8-|1A>X1WRkRCZbos9brF z)(O7V`&?VIzE1lA?1Af~4au#%{X}p=R~xuD>)O)Mq<8(uZnHXuo3UZw@8WE7w7x5e z;VniUcIQJ#VQnOR3q7pE#2Cun2>&-3d}w8%cJIha%q9gYWyv7z&wW zLtp}*J=ddMI0-?sSoErZv~^0E zAKC{u;+cVP%LjfO!IsYU;7Dd7xOhU$W)Ko)zF@k-xqhDD{DIM3z*lrM#Djv8NI|F= z@ws4`a6=>E1=t3-P_Rsh)SFB=>t8fX_-7b;2RgDRBFxZjPTFLFUG+%IpB5cjNm0z} zkafbSo+kv++T>?MahAa>y{Wsp(}B8?TnXlpvG;uRt(>|%0} zgV%;Q{+9l1~Am#q9)Pp62N*F+lgqDXpdHxJ511x}J;-bRUR+HA^x-ADo zacZl6P+p*5)D9)96LU9^itnsku$J*QD_;?`w)LIbs6<47p}@e6rV!tsG$YDd5>Q^Z zO{YHNVnZ;do48~pr7~L*FIl5H9?b@iXoOg?_$3dM^}NeQcokhYLDA|0o9zYb zgQr>#^~Gz#W=^M7-4t#D>w#!6Y3!^D7T~)exh{CvNsw8E-oq}mjC;8NmT{ZC_nTik zMMu^eBfN#2-%Eg9ex`vP5|JKhY|C#C@sP)!a{@0k1b$F+cthI8B)815o2_yb%eSw- zce&Q*urDJJptX=!nS@Z zFPc#u2ku@kmS+;Hm_Hk{wWUlzGTFvI&hP?bzqaSLiESUhSKO;M2Sq$G>#Q;3^La13 zaBP0h6ezK*DN~uFG-KYuV6r%C9AfpWht+@;I0smWBQ{~zDw+BXY<{`7-hXN3v>bCc z4}zRxc$|??nXYTZtWQa^pk%3J1T+2p{E5rb7--dJ437kd4$^uAB)j1)XywPvMgb3jq(og@NqnLop4Fo?B6G9=``iCR)H_D^{XNmbNgAs`8yk&n zGjcxN28;xz-wv)zaY~vf-Pk#UB-gV!cw`Z+c>&)yqv-fO@*a~l3&G*L*f78n? zx+h`Xw0~X9h#6z$JexP7**|tA!5XH$alkaWO8^!QYS12e$5oPJ(39^Y!&TnoYewH> z1t{&!?umi1-}f%S?k@AG=U>kDd5rbjJ`B%?w9Fm0dKe)a&B@{qhTW^=6}=yG-6<7b zYOcWwCH^?xw9$xspT3nQd@1wT#iO`Pmc^EKFP1iIJn#Ou;DS7tD`8Jd3mGtfbd7(G zWdUO*w^YT1qS_qAa=@i{e3Y)^_vBgy6C2?zODMkL0u8OGsW2WrGhsjO_0*X4M(>K} zLq0!C{?{RBMxOcBm_qNfs}KQzg>|}`elyol%h6?4L8VKBfuHw6f{+biThFqJ=Rl?X zZDC1Vwf#+@-S8qcnJK&6g2V=ew+e;@1Sqg(P%as1;=W-;X;`=#?esqiPSKT!nf5KD zJe|sNX04N*5w;{%Eel^JQP>dnKFRLT`&y62X53rKP3eWa-^#M3{LvO0imS%;AIwpSl|3Qx3y|K5v%sRaRP)uiE#hrotO>>zgWBs~hei|Eh1O7s z=5;M5(3nXO$mLwg;q_P@p^7QK2vZiAUoOR>fi_8^b`P`1qFU&i5i53q@Q zv*sT*Ys3RFcPfU{AwG#kXt^#4H5V!*^By=(?>iNXtpkD!rl2&``y6F^p#QYbdH{P! z)z;UMwBkXBVp1$noacZb=$B^pwU|;)Q__jlzsm9+giW6I>=RWt(maEXJaoLI?>PUC zl|wH6$zrN$J z_O@#01-yL@@Z>Rna31;Q0c@#4tk(H(P6PuPw9K;~Gi!@mL9ZAv0;BFb<`l*HXdbzk z7+Dc{Ogr5v4~W$-{MgH+CkGF%Ml`d%Tq}kJNQSN_X!#f}U9yxqnQi4W_jN(yebkvu z+dFUWj`N$RgiuKri8G;V%{EbDdtF)S{ZE&#J=N-4tZDH)qQAjp0c{|~CavGOou%uf zX-pWwrTgOzDeG%~YMPD}Df2&!6_b6{C@uG)uYEVR8jU7oM{J17tFo@9R7ah~5eTdh zehTa`Qsyii+t$rjEASmllo-Pkg5d2gnXzF}ENhzRmUf$zn-)X)`f~FXnVzQ4zjm?k zXS8h}+Ll!lkk%B#0khpvuE)5aK~@vr1k=GEn$jDEfTM!ItLA?BH-~BMuY(Z}yA~yZ zZ<_2vvhQU6XBrOPTk36PoROn-94YJB==P(aPv&cC}2rN6v1Rw9U; zIm;2!=ofcYiF#M-0N;alcNMj}&j-~uJ=7v>rn+Ki6G92HV4l804$_91|3`Z<(kd1l^e}tc)eodF zo*iJZT5Ud&iO#5@v9XTz9Kr4;(WEx8{mq>XBB3s?NeA%LBs*cwMyss~B^F+zK|Vcgn6piqOs)AsZT>**a7KZQi* zBh;GsnNxOg_OfI7zCk59fsad+T& z$ChwjwYhEPTFrIP_M%aOkY$?am^~+eOL{X}sUc?rY_UyCSPa!`=TA9ok+a+!7sXR7 zID)v1pcYf_Dze*#l6JFTfBzMJPUn%%m{vXZvZwK(9#$>ACnwo0tWYN zRyEfo`X&5HYb*2LQVGM!?C)>iuNptT0bz~F?VgVUr6XYDS6b|cOJR z-9?-c0KF&P=+Lq>f@_!izZJdh<(hHt+=i5o#TIBf&%!E>%Z1@?ErhKJTq~c4rCicl z7j<{jtF`v z2OVuXX%DPLmRVflJMN(nglGOcyX=sjp-jmqgvoAsBbpL&-h@goJWG2EKK8eEFU;aU zFbi`v1m{1a!JA+{WA~JC5nIP=Jnvq@x2r$NR!wJvFbjt+X|Ww@7kM)9ks|bRR3TI* z1SX2RUTf$tkzjEY|9WlWVY5I(zR5l@uwRHR4L&r8Rie_4X?#E#QtPf=Q8~hnJ|Syg zdezJ?-;}Nv%x!FbqJbCXRhpbdEpv-*Eg4aKUsx=oy-~x5n z{O`n`2}$N|1}Y{ro^a>6uNm!^_?p}%fF*q8t`_jMH?%W$A;^-{1!lYp=KED_x$O4% zX=;BS2$Rn362{ZU&QHH|<0uMFV>_1vw`Rh&e6j46(8u>CdskQwn#g-}4wIRD7fSA3 zPE{q2p?}*B^;iu%>#^x5=on;twmm!}jzfzb^|INoX*!-8X*bcoTYhYa4vU?+0?|Ol z&+!yEy>z~tH77K7sqM5%k(TxFYdNMEg#{N{dpl=D9hM>wDRhbi=I$JDtcXw$15SUM zDrca8`LPH<)=<@$pO(%#fz8OzVe~T{<4lnHrO1ug*jU)pU@C+-= zZ0vZ*WFYHDww6|w%*~vCQ!SKVlw%O*>gp>8J1U-G;a^a3mHp?|EsLpJ($;I$d?da~ zH9j|dVj{EP^y2#SPQCtQ22@yzU~&7HzJM9<-S}d% zji))8jofqkl49@9)>qgR8&@*Oo?C2Le}`Uj@V!NhP#n+ z(s2l_dPv99#%;Uj7Uq9iU>fEBQuGymnU4L|rMMzHI%V@~>m=sRnY6)x`&41C((B}R z&S65{;fu;X)5C+u6*-9u;NP1rXAdtXzS8(p4VqP5T=4Xa1-RLl2?)3N{=s`g@pZtRZTI=HQy@2sQIum^2>(j~hc$D^Nz>EQ@d96Y=qMl`?U%^{xmp3z9| z>G*iie)Z&TqXps;*|V!DFfueG!hi>#|_tNpm4mQp&R{JKVxZR2uj&N0j$i z1uGhUSQh@fe+1Tm{^x{sUAO>)l7QHsf2R15c}rSwm`7H*`?$GCQ{zvn|sY%f;t z`?~s=gR9CQPpj&o2AZY=BQft)(2*{c8|b^m#Pc=%^bIUPlVn?LrA&{E%R#g$xa7q2 z57sJmT>8;j3X7+4c$+kVw_IKzq0zq><(tD@uVB<4`UY^VdaDnPzIe3wW#OQ)a$vQA+-M>bp+FQ3SI&rCi1*l_ct%uc{ayayN9sV$joCLeSnA|6O+V+7Sr|}knc5Pqm=??8)N3UqzQCw+t*up*k zqVq%otDB?re^&XoWy)>4_e~a>H)}~t-c~ef_fom!*b!{VTv0)#GBl!HFDq#g&3d>x zST9f<*b_?>E zlWzO`%uj^b2~1DxE6gwtSR!L``;0Epq|B$J-wa-lk_#=EU?%fyvMF6meFkaoj}#Yiu~{Rni(&Qm`tuC6OQvxFEGp+o%0 z1FzNf%982tlxb|yIcY0aPx_tK1F%BCMU7WYc8(iDfPYb7CPHO9dV-Prp%#qO$>08| z4jxqRs-=Fmz`yeW<504t6{I9TIY<^3=XEAJ3wyq=gf!S#{Hffv^|axj2bW5O;Myqm zIr=SyI5J9q=R50d6YsI`bQy48oQ=Mn-<027{+xbUBs8TOzsbj|&ztymY(o74#JA6X zr4MUXTYsHBY%UxA=;BBl{!nG-ndE0Xk*~!+Ua0?Fl#8w^vZ%~e-Fw$FmOui^{dXF& zyz4NuGT7xLi<>L>cbvqUPA7+Jes6zcc=u}a1zFVsRpb2@rJJDjX!Azbp} zwvh7lgn#mIn^Poc+ zf^lU}uPJQt-Mlay{PlXM_?=y=glUxozfNYgW^7jP>%BfnCyEKF(KTKQ0Ag)!FEZ0$ zYnknbDL*7IWoYIJ9~iY)^^0W)viv+vByFQ~tQXpd>+M&$aFD6(x-?KN(eZx%L3<{N zYio#}MO(y0H$SL$leV1X=OMQTVhsLzIe-Y)3g06(2pgpF?=4yPK=-~bH<+bXj=-ej z173O&O3i~sfUDEe%t{Ne{V3$AJwNZEly9AC2II-AjQ_W^{{ri{jW5zNbNRk|);3-C zmFLx$_S~HCA7!&bNB4OJsg20`J69BP@r(x2RP4h1^<>#-H5&?A*5LwVN=w1hhbYT& z_j)@EUDf;|N9R5C0~{~W>55DBOb}#yl$NVrh3pk(ena$gp}sR<>P=`dY!#LylC%x6 z%c{2m-}7B=Qu4<1XZq8W)lA5bu;IM#PhHc!auyWibE;nK8}@0bwa>JjdJbi56KPl2 zjnou>kCN_pL*UKO${MuR)(@^D;}4)4>fGY49SfGEj9EY*5Mxp{L8GGK7?q zQ5WYkDr`11J0jBYLgzYk?I`-=W}I}KSPJ1JR6jD8xVV4|^ze}~5Am)ol~&-e;+k@l z?H2RbmbW2)ZiKcS`l4?^i@HQy74ui;u`p)woi{2U2NWzwh>26ZpY}4IGR#lF7MO1i z+sR$beQu6%%sYjL$DM+;Dh^!@cj-Gj$Uip9Nz=~(*!e+a4a(Yk%9{%+!NMf}ZqxeZ znl5uxSU92wbg*?u;Tg%$EJjwiaP0&u?LPCmL?Va(BHh7_1==EJav>s(cqVbueK*8j z4G-cT$V&Q)dC-Q{+zitSFG<@_7gTt4qRyU3*1!aaZm~wPv*u3gFsiyXLKWJMNJlCl z0xdL8Z%FMeV7lX=M$UBoPu@Pi{f#I>E$yFb=lBT$yhP+<>4~7(AN{&osO<)>l&&}q z?K7$gKE<06=y(CV+zgCq81Dzoe>~)!3^y{c>mB`48FvXC`H1e9IaV-J`5f1jL~b0P z<(SgbbPyUgx+K@DNa9r6`Q$?tFDNr@bmDj%bWWPmXN6RK-6PW7qUNk9S9~U@Zq>NG zTNh6O)|b29yt!&uk2gW@=~JmYT_uc{zXEvdE8ZRfiP5uWNLqJIYc>l!0S7h6f`CGZ z^kA=$%|lxzhBo^VQhR0BQfV_!zi8*&|A-+zO^mcX%kgzSofy}#jBOB zZ3Rl0gX{^VTx3#?M6p9TGC;%sxF9J&r*3Ec5!IbOuvFsuQXnVc$oMT?*h8be@egh$ zGUvv(Q+)fGQt9cU(*km|Ru&Lj`cF;B|HlOpJ^KH-AaUXUQrI~Q~6256B=q;$i_PmAtO&S{~4VWSN36Rhz>IA2&Tp*g$eTrP4 zbHKZ~zO8neBqx=xA2)hlc6tb77;C;lt*O;&Bx@Ed6VL`1Fw8kPw<9S0Kzi)7`AV&% z>6AN}sMgCXN&Ih4jTCo}h8#|8PUn zJHb=F4FWzo76CeDJe@)Fxhm7S}!%O0sX{}(4s`brKF9xQG(*kBPEp6 z?-&JD0$&2P@_Ra!p#waM*}m=DNQ1tHH2iG9sSI8UTwS%2IP)l;kw3XDKB1EYQLSfj zVxedfBcPTkkf2o}JkF@1pOZI~ajT!)=9sKl_XEw*Yu2pX6Jk25ZII_geuQVz(-y>8 z&gq#Oz7jGm6`Lo2C;aSPd!Shde7s8p{@sQy`9uFehLK54Y9OZ)vA8>NFk}ooIb%J| zV>SENo;Q0g`9<_xN@vCDx!)wFCo}K|G&1{(6uuS1iT5n7%N9Md@$%!$IT)`x-b}1~gtDbV*hJhV02Y7JKQs+XoQXl~P);f|#Yyvy)5i^*#L-yfqJC33AO}%wG`|t2E-{Df*qHDX8u}qZ4@vl z7p+(+iNX^FDxZ<)69iYGm%a(z$jQqbh1*Fx5fx1U)~QMumECilli;aL0NI6#DNA_V z1n!i_(LlY$_yCm2=)G-9%iK&l59O~vOrsTIl*P+P z|B(v$kd43OGG1^P^mxf5rCeP1Tw#@)>=N?^1l14UW`)SkJ}(Y=G|7K1v2SJ^#g$>Y zZ;u!=jv`?0TA3}}kcBfd03;QZl2|!-wV|%R!+T}aqKkkhBpmhmzh5=!g<0OnwJo~K z(aiJFinA^_VuhgKFih$cSr96Ua`-DWpj4?u(2uy z9Jf7)Z`EV^7}1^wepS;-vvIUm+36*|DXz|!cCl0?hol8pVW;u%1UjVABVD_CcVTOTPrUjsL8PM~yHPgd6t%^DY4t z0LI6Je?z7${d_2bznLX!!-M!#O`4|5XE(1%m@ajsHJE+gcaOvNP1j&+vrta`964a^ z1385{yigv3pGJ-|Wz7h?P)#evif^}c$81+jI+yiagNJv;0r*Q7YvYKE(Ib2I%wKx` zMIIZCkQx(q5b{Z;>jQu9YtC*ZJjP3TJH3nAOzKhMl*=GGBBmu?W z$N9%~lC6+B1FEV~BR-BSh;0G7td1}CDe3sg6F1Dqy`1gxWIop#*C|7XRI2#ySyVQC zS9tx7o6G3<2L}9VoHLRBh>dD+jf;mcY3Z3*ECXln-@~X71|(!F+*NH0`=n#^3ydRi z9MrkcmS%2`!!k29uT{_#53v1uJt1NvMnv}ve$fYC0M!xwgR&WkO3U{4g;j~`=_Q4e zqQi_1OkzmxEsp~-=La$46|dR@1Y~h&k{ZN{!Sqbg;(s~Kjq6DFUGk4ndS(v_j4`ON zHTVg{_~cA^S%#QY$;i;q;50~te&88K)f01&3WZDC)1mLDSz1{WqSf8nMrR@}E^IIx znlKw&0DY*zrxy0zG$d%K8cZTt%ffcTK6})w`dC`+Oc<{T-r12A7T3D}s>5u@f(mY% zCX8S&P&sfr?P_C&2ljSGmhzY}7IjJ8kUV zV?dz2I%gw>I?Uw=5FL}}x*I;+=CrJ>C5qt?&GdIV#DcUV@-5c6BS20#OCED?_0=$cJ+u15CW*o?YY`H}hq zNS);*>xYsCWiOS+aQDkje3)6ohyZ`)PvX_ui0J7da5pk{yg0((*D1oP>4@5j_S;Xk z?yu61JhB$*(VsqK2Q7=LN#B6fn>mVe!tpcC7*mSv3{Dd4*hq;5ECn;_9pC9AXFt}JJfQZ5qB%!Y~4DrivDy6yO*B7>ezm4C7iO% z&Dp|JM-qI4&vBVKh<3z-v4C$|drwQrs3hEn!CHqyp!SGQr;U9g&+V;5 zw=OT`A?Xn2?4a-Tt|Aj}+0-t=J6vAAiA=BWyI9j9FzqXEF5vxhJ7QwQQpcsmP6&M^ zA<*0hj$drLPQ#T0#5P{W|Jiq@sQYO{23;F*#=|Fb<}XT-^jhyAV*+)hjgo&~#g`5UKn zqZ-a`HKYK{6=Vc^>1kq=K^G_NyL-a2$8?S5?gd5QN#aHpvzq&5&t9<_5$E4lZB6`U zXkE9B)GVp#EJ=gqB?M4{agZ;5e@>o^Emb=g^+$Y!bT0zZodM|*FV?7G6Rfw2$HS)e zPYNh0mDZgPzjNG?%SpHM&P+n9fNpl7z_y8w-tpoB?XQjL#CwD$<2R%n219*g8JTcm$hV#*3B#ifcqVetms?Iu zOK{@8q&>Ss55S21gI(H#L1ag=xBKd9Gia zR*mrUO(UposE=@DPGiUX5m}C}&?`1u44T7YKICTG4T0(3mpkSb^b=Hlvo|$>zqTi6 zw3U^~2tZv@7>QSC+Dz*A{p@O-279O~(rY=t_mPc9g{Yn>Du0Qw+D+>F2$p}d6A@P# z3pqE4Us1GkXEa$YrmJ*o06Cg`F`6xD>*nSDxuW^caj8nZR*c4A-OpZe!hRNdQ}e{3 zcBd-Kz#b%48vYGeF()3gU#GV%z3Rz)xUQ{Y7l1v5pGr#sJ zo0&j^3cAYYJlE+&(Y;zQem0r#5)5RaY8m0)SS%Rq6QE16Fy5qDzjCr>me%mpQrTqE zp!s+iMEK9?J8KJlRa4^wM_rY<$i^p_%X}|bP9d$%pVFVO0C5Zp=`B~7X2$z)15$-kI``8J6>WL_b~tYys(FW^_Ksxus4phCeGtcCE} zWXZ*}Q)Ydo`^ePwe|)wsFRLPo`3Tm4n9DluCa`KZ%FN58o_X-%c>i|tUGf8Q?}Kvj zh|1+R^{O8n9_O&GjCSVRQ$OWOr;F2}36PRru)tpIb@Fg* zb}xRYwwu0>J??H=OjI`xPw=QO2ojeZRHt@+ktTTUjl9uS4Ez6ksl)3IjkU2HfzW%s;?-3vC%AWhwF=~P)Y<`;US zxT<_A>i+h~+l|F0PwVF5O7}CfyIomQmFBLj{B^-!Riu z&8Dz^0sEPF#xc~@Tk^?8QRKXjnQc{^#a3}ha0*o(&V_SaVM>Bw$0gTO$f@!xrlE{) z=_o?GZHZMz_K-0yfomN#8{(@)&%%a*71q}AEq_60rb;_6px6R6rK<}MJYyoXhuVGTgEq>k~79BvNfC`9)n zXOM?=gX;X^-Jc4D(>;~^FB6M{HD{s@h&)NmgN##r<5+yjT*u*`t~0fE13WvOZ8{cm zTf<*;(DGkTaQBo}QGr;1?$UkZMwd1c=AIm71=c#yg(?@{Q_YLB9@pMwaCo^(^|wj2 zGVH|Sgpg(HpM=GxR7CT)fr-NuQ;haC3EKjW*_ZlTm)*BRSJT^_f?^t;HV#XbO|wOO zPS*^HNZVogB(Q8*P9N@m1xIa{U`b=tC0LMK&;5YgFRJv&<(ERB3jUHVPseqpWh!<6 zbdNn2x5;3bg+7EhHJQRok3Z}UVpH1Bc=ygeUm9_8SD(@IN>6W`O8mT0_ia0n3ve&iGkuN z-iL%SUB*dzOq_+3`GO-zadmMq*I#CFq4QdKhdVXs)r69%XZ4B1E!i4;{f4p?O8BxF z`gN;l8jWQUSiRoj@2obP4*A$0;cWs6fMKf@o*GF-1pq_|%QWqT)co+wOrc?Os`HNv zXVsaO84CF$@Wd?THJv4i$!|XKT8@jINNq5H7^te1kN2p%c8v^j#yQ=j^ho&>Vq9_{xjy_Ir+#)}RW zYcowhxGQdqrcI$L$(u@n_Kd4+E-<*lHUtbXw!Gw`#nUhP$a%Aq` z@SYx^=ZQgs`L@n0Hz)@IFXG zDCtFP@s^{?m;S;#8M$`+j*(uDBk{^T=`$d`sP2FnzmSB-de9uh*n7ZZSd$ZHXu$?5 zB8`xZs9YLj$0ZIzVF_BzD0zc47-WYo+^?d?E>CRY4%7`;+dz)Km$}QPU2h|p4or^}XO5w|TK!R; zt=~I`DjajT$YlFX=48SaH{-9(Id8X%ChS>RL9k+wr2f^uzj^G$Avm2rH%9|Qh|a}( z-lmy-(0HxX{+~^o($Z(s7P^c*#l*5^9>i05$!a0OmdA}zwhk5(qCnB#*hbRlUliI! zU4zenM!MRVK;Fv<=F9 zu0EC3`s`~4TMQ|LL%Vqq9D#E}PRFR9wjg)X8q}p)-!r(&r5jm)OTSnD>*>WERSHdB zvt~@0!peqKkslG;C_-k{49@$+yaGr9oUwdRXJ6#d@>c8aIaW1;!A#FnOtUhHyJI&; zZ1*^>dD1qAYYC{ba0dVBF~Q8#54X54X<6VEAJjo}J>bL4*)x6~KX5OB42XeZW)q}dg7nX!L?4JiH=P4#o#E~dNNJ1sBS#NlGS`9VUV1up1*pq`N)Ozh|h$vCge}eN0 z#?9(R;{X1aQ(pid4wRv$c40o;j{QPCbEPDgCB&LQS!m9%8=hD~osywI%!&;g5Hm;) zURp|jhpdg+;UY*)g=jLir;$2gzHd!UC0TX;L9XqIXcBsYdNaP}ucCg`(4DIJH6G9A_NuV6OO(>BSY~d}wHg7qcS5N19zdWNM2z zEX*;de~Y87i7HcYao|6v8=`2~IAvDUk;NNXEC-6rRa)UPa;fFsUfLAPI{b6kYbXr+ z29_wSOrzWvvu`OcYE$X<++F$*nh~=abi|By zC0D<1bCU4T8NK*(5j1Dw)P2lv2h;Lj4_ap{nfI;Qa$5R-WCHp_yM&=vYrpYA7EyOY zugVQx8JooGc%9zd5#wCTVl71%dI+~wRzc0~0xiI)j3($_eH?A2{@9D2cf5#UimSl6 z7J9+n8FvT&N|&zVWrvU6UOE7T!l9xuA4X!uaiWRIj?kOaQgH}&yFFPr@P^L+Cl|5B z@S+8^zuvX)CL#T@?^ZWw-Q8s1Q2x7e_173)@vPnVD@(z5qw(R+Ju0H?5nR8dUrPIU zj2pnNjFkL?=J=l-kQsI45*+`o7}vmOsBW0J2ze``F2x@YzrK$=IqvuvnRXF&VRWJ9 zv+$QdFRQIi@XQd6AAp0RSsf5B{|J{En&P=f^3o&6L}dB=!|Z4J zu)qn`?GCf#!lc=@Qk(6ox2Biolp{zTr38{K*Tes94@KIBdWrb_Yu};=yX=@z-je;O zEvA7Cnm)a;LzK$VGuP*zc1gn7;#qGskx4;trJb@f9w!;+KYM1lZZ3R|L=^5M7vUSCy-gf{zwf z<`jTUa#_WJetP6k_kRnwVgAmNJGi_pk;!b~OnOIN}VA4iz6D-9abb_yU$eCKqsq8#Qg^b;=7e zamh$G z+2A91x86re8)^V6PU+X~=^HJvxzV2wm&}GX!|tFHHCsvoIYZ%K(*Ek9%bcZX@w&oZ zhf8s^WUj5|V+pD8rby$FX}y-wN5{`9NEeiP z+t8owBiwN$)YMkVHF~p2yg6XZ9(WSacD_cMgnFDGyO00Z6l?>n$B?lxZ!SjePwLTc z_aHC;5)9N<-1~HAKw1%+7mhXZu9Esu^iz2jWwLrXl)-p?N_qOTdIL%SU=d*#hL`0-Oj)v=3< zyNwKS6aZ&VAt8meb~Rdqu8omBRVWT>5ZlZq$I28Q<@|C<4Ut_QWWmFuzpe<8T;t4-eHBgp(DOA^7UCPr+6~rmnau z1{J|WUDt?1#;fsezB2lH?#~LmTjYb1TT}it^8%7^mrad`t?#Uzp4f*D=lC(MR73-@ zizBDAmn%pIZf(*y2^$Q~DsJRp`ohp*(l_zEzj+248#oC}_@ryPBi&{H(DP6d?%UZD7Wa@Jeu(<5Tfojnzp4+7K()VhqgqENdrt#B1TKl6>YP^SmJEgqjZ z32AJ#s1b$D?r(Q$-VIt&nF+YK&teJW)qPe=M!Dhf$T5oah|f%v#abmPo}z|ukQEve zF*CQ_T6DOx$2J!`FJXf<_HCCXt2chpZW^!sNW!_mL#RwEm++>&*w0BuS(yv-S2M39 z+{tG=OZg1XTNLPPbvyMP2N-smTON^g$<*+EwCD1`|6QN9zrn)RJWi(Z-2>K5TBjsk zR6ieh4k%^p&)=KlM<|v$k|fkI<3473(hta_pEJ0NF~#5}I@&aOMI|49rSEUQ#LV{H z`u(FdpJ0jSF>x7wBds={k6;rah`;vR{x%bHG)4Rzdws}E=> zk3cOV$tmu(nbRp%@y``naqV#Sv~AzNUfAWC-A#) zYrq*HQIfk0)GuZ?fSuK+k{1Ev?)Q?vDZZ`+P7qf_{DWfvU$;NRng#HWeW2IB=JB5s zNC#YbmhgiUsE0Oi5p@iwCp?}vrVy{|9e{qdn2N3K5rp)P$lp+0x2`MAR zU53*2a|mt?XdTO;i#Nj{XG6pudH1ZZ+~U!SJ26=HZZAM|PQ9tC_Vo45M8&alBj$Uz zg`8(!cU$m&Ldkwa0A{#?%Bm-t>&Ke9Nd+)&iD=CMw5B5$KuvvV&)QFWbeqX3J9ln+ zGhuJv{8LIOG-cAx#yf^T%rA_-k)h+Z6P>}ppce>OR21kOvaSHxH>RP#RjiqPNi zty~2+=KeksiA+%eE5lleqr5g?v|N2 zQa(@^Z@|9>06+|a%%(Ete-f%U0qvO_g2z}ZuQ>Hcbg1dvHJD{skzO;&{6V2aq&9DS z>jear+yN#QMfY+1kA_3xF>vwli0aLzvXc#mXXvP(Y%S9t=jf=eo>7mR9h+vKgULl7 zQo6mLZM$>j;@RSB0o*hUeDfoAW9ye3>-$yPigkBR!@Q>z{F5F}|NePOS^10{&MRS7 zhu>}ZUA;7Io}%6O%bkDa8%iugpGc5im{>9aBT8-Ft^yL>j(cVd#xfyB0Nz zMhM|RX3w_+lVV&;`k6?VF{>jKCVd&be$^~_{ZK62VXp7r)OC|;O!Iz0(6sqS?0=yE z4CRAz;7jA+uWlH7CvU_uW^O-R&V7&enZ+vM{-Mhb(h+K<$$|>7?fB5=4^EA zGkU2lWruK(J0+3slCd3Ye(T-ahx}nz;@#S{gP;zOqWSU-{4j@75&Bl>5ZCO-Cms&w zEH$m01xIm!a;5`quwSJ9m`8Zbl{1;Y(Uht}?--IwRbpt_VTe_ZdA~LxHV>qHlE85Q z$x)vjPMai2#b@5ky{ux7Z6xvnsS;+ZR5!mCONH_UME$}czC&3s4x$w#M%R)Q>D)KW zZb*^Oe#)R-)CkZJt|?VocQiZy{MNVCJ6H?MOm=nyiQrOCqbwgQZjECRU`D0o|4r*h z$mg|M?ery4M}PCqe;bb>s==31`N}EAKnC-F@B`F*kKhX&+Ls^+fF!$ijkBuA@t+t` zKsl+PymihBW$Mc1A9{yHxg!*DN0h^(3Y(xIPM0C3?@y?#N&KZJ*sBLYd*fcWBYaHX zYsrnX~pfLC+nxA=R$k1-LCj9!6R(&H)e&aiamDIMC}6co9zYbnSl8SU!P z#`qv8_k@fmKh=7&53H0=mdH{&j4IYo=6E=*6=eg86?s49p3f+xN#Vf=IQyOuA<7xc zvL4Ve`=Pxn?ptO@VH_#8QTMWWQl<7xE-$;+#wl1{_KO3= zCxFyZ=ALE)*lA0tv9_2nkYgY4{{Vsf$_JeK^?9gEv|%ZsqbYLWDgEGkAosG9p>;_T zIlpp^WM}i(SB^+O!*rTK75=NHNA7!*szQ-nnpFHT-ZEqBXyU+uGshqJIr2X??z`Im z2On!)F5pirt6>)kJ}Bxu0Cf^N)muIwo==$Q`G&v>eA5^sGd1YDqWQwKz0y^ZlHz4* z`W0j}pep(cuc?wGBFwigCq;nC&j4a@z}zj-eR)@~7xSg@mn7t;uxUH&%ci2*_mF#r z654aUAEX{7I4o7u@}hQHG*x}DbTRqN5Fz0TN;yRWG73I2_L0?73_)hdhDQ1)4>$QV zs<`_nz%e$2j+Bn5x(Nd%K~|CL{=~Fh8UA-QYCMcD-yx_Wm0?xXV3A{~$q+ZfAjjn$ z>@fbp4J%^8@$F(Jz<1X-vm~|B+<>dhoRzoK5w|e5M58?z=ed=%xp8i(ggu!4&7)Tr z59kU@qdXN+a(!;RVGHy~ROb!Unv~~?#L}&a2DW^ROvXb^P1iE`NxcKMYxO~6Q7!YG z49Pnfe?<%jbpt-C2guzQfv&!6ZVf*cnwu82tHo0}mBmpH1ewefOrd_o!84O~z3}O0 zzAc%DL7znn)|b&iDU3R@vWc`dgJ)zUC)4bvy8rh5Sog2o3x4+fn^>!tG9@j2bIm(| z+E4l~z2MmM$|a?;Os_YqN-I%54LKIG6P1O0X=C%HMCxj|GMH?F^LreWRTUSFGz_d< z#~Kb#F0&p`0Ps>XLlzcH>xjWr{oqlD1TMe{$S7`5lR#x9OD}urKW5@ zjYH`KqjDW$-goK4@Z}NYvC1mvyqORhJWS} zsum$md0fRhUf`uAg#PZOl1|BNNNf+*`aUj8cm7V+-5Z$wVaS4jWOsvR={ zZu6MmctXvgjqbY|30~qCmZTiOg0kE~z|W2^Nh+gGWP>(2uVF?+=S>uHMq&p#OWTu? zc)WZMhj`v0iCUkVfTej3`;&vC;m1^$>%gnE(i<4c9+a*;Sym2@tOj{PuS{AZMqMB# zOT;J@Xg#p*FMVO?slPkV{%ro zE*qzj%#Vo1Rh4(h*%KJG54Y!9s#eVEMI||NU7XS*+)?`LKh)^S#MQsiH7=b|^X{Zo z7MTD?>6R)cC7+uhmkSA0dkHPa6gM6zQ%f11%|>|XeFXD69)||V9)cCPxDgRDkgG-Gzf>q7Y!_zxTCS- zvRQL|!C;^;DAFb`h?p}8Z@U~&gHXmV;g)yKIe`_LIdlaz9BBY-rF&Sdp~y>T&8zz% zuh2D9a}ptol?jP{Q@4X;eXsjZ?yrWoBX;pph>nmA<$og`Ad@@&S=d{UUI!Ok`9pdk+Y@oi7wK9D`>L(`4LX~q+z3^8n6vc(Bm zq$G~*%S;3&7%NTP$0k5JshiyUoy`}zL<3dy`|&*izRzPD+SW+s9{kplQ4+*&_ROj5 zC7$S9PqWV-#N>nz{j(y0M}7rJ?OWe|h!Pn2uaMW2ipanxq#)&sx-|dD$BGG1-uMG8S1+#)D^p+wh-z!4{wr6`Sf9k+%X0-;_7PtXm2U; zP{<`8??d0@B?v8dSOw zDFNw?F6ovUB_R?b0tzxpLL>wg5MdJ1N=Vl;fA@Vq&p*%qyI$9|T|4J{&iiv>pw1sh zKxl}>Fsj_4rl@CLUGI8o-<;Mpk!{=jK==ObSyz#oTcrhaK0>Gl5%E|MMU6G2IV%U^Es^s?PO5*km;FP~kRg}IfwGN=)CZxopd)AK&&X;an#(sL zytEx7S2I@BVuC*2vBPi7%|>7*w~A59fl;cUIQ%hX?n0qzv#nm4Zt*@RcGql6yiY7# zg}k@5HS9U(3c1!UzqUfbc9^);gH2THR4ngYMyTZNV-8h{cQEbm*kAYoh8);Y`~btp zXKMv~FGvQ7lWE|52t&g?QU8LK0oHN)!WTrn`Kt z0_A~Sk7{aTxQSe_cXO#pNsCQY+Wa8V_2OlYIhM0Rxm_cGEz?^j*yb?nyX+|ap(nYS z1f_4x+^umUL=*;?Xyq2PBL}mY%lK`Vy)^ee>PY?B-**^PC8)5oxlh94FLEE;EpCn| z4V5W+M|CndC-nEr@hD55&X-GvKc_;p|Ci=TJhlyvEKFrByeqgM7QVyM@OM3K(1=ml z0>^d96fT(M!Bc^is9r!N%mTCKo(vKna z?~3k4SI10F?e7`i#V16;$eqUxYBBwV)wBD=L{E^<)*d_~WV;^|DqLrCUhNCk8?Mtj*mWNog>31vbZ9a&^e!N$;+bU_q`;%io#z3(c2d)vzd z9|%u2Pt}QIa9ByFI8zRUj0qKU0(4ZZv1R8D_FnE%V|UK+WRc{Vk98N>72m=}%j*}e zYV%|io&)t<#ZUXFC}$XJ??l_vb$VB`y$|FY2hGW>m*zQZ_s55O!&6s~&niCL31zTx z5=rInd41PQDUSIY3W^{YrOaVNC?O+bGwS}5Iy|NRqwMo?U_l%Fu|zU1>d}GtD|^LI z=dyx>6c7q;h1_^691mdM)Kd-}Gds7weD{~%FYj~ZvMuVHK1Pqh9X)n3#=ME9_9?vk zB`vvuNL6YK!;2{6#dg20B_8K8Not=CP42S(O+K)yNjr<8sIDhwuB0;|nBFJF3of+u zOfv3DgJ$5NI4m(Zhpd(Y$r78vZ^9INJI(8kqmcE+&|F+*q0D3;6h9w_2b6gX!;Ji% z-(tsB!dF!?zUUtTCmv@L9siD^o&BDFSK48{nil%=QLQ$~PiKB4Cq`X42~EvGW@J7E zJ)6}C*+C)SlKkYnKq!F9SDE#Aq&vEEr5 z9}3bohOD0>R}H91!VfqS#Ru8ohof@bXFyYIO%NOs1j-26SJ_%yG9}5t%e?pdbK)N^XsFmA=z2Q`}f8k zj6Z#DWF*4wrW@w(WmUvK{&Xyf8Z)ch66FXU)PyB-4jHOGc6|d8v+%d|Nb!{- z^J!KVL5FrDktrZY{*#*Teg6G$J@=@I9~)F1PXM_<0k9b)&J?m<6|oUZp{gvQ>k+59 zPlVzKVtZFSR%2CKRVDBBuy8x=EInjy9eMev${kFhwV3ZwiQsodU6QWK z`YR}Yvl-4gT_tU;{i8k#{L-6}lV~XHVu(6in)@!Ez3_-eqq1Y-@dF4tBG6e@{6|WG zc4>Qm!kH6Je%z$L!M71aVdl|9qXD~fM&6Zi)E%_@1?^zVGZay~CbU2C>5SpYMPKU^ zkOY&0zr5VCoolrX5B=105^l;0y=Z^n8fcU-hqNZcd|94i(Q*A}Kq3HT@`a2}h7HyC z;+AfNvU>1${^zP#E>75aTy!KZhfE*}o{MF0Q;+@4o|ql}vtA8aWU2BQnqv`(pV0F) z333Ne_P`Pv8z`zab;fL%GuA@y;;%YHO&WGi}_9S zu044zxl9>69nrGX#`r1H-zGaLI?4U~`K`kD zYI09*t$8AU9AEv3e=>(Bb8%(vSAMqHfo4y@Uky*}5A}O=;tH*ToHq*V7si zMs~L`I%+QHp}j@TYY$nj9Z73+5NZv@^O~8DuohRcQg=0aPUg(+;2S z#fzlW!M$Jx#5!`kyjZZQSWl`AlFF&c*zC$R&i_!u()StE*lu!(d`C|8wxBcQYM!m^ zTZH1{x?!cB!9z29<6kfP{=`%|d614bPVEu|3S&h=Bqm&DmeWvMho3$<38GTp{JLZL zHU5^>RlbLdY?nyl_g$0R!EH7Z2AmPeqm5bD=T{byk`dGe>v(<3Q8!%G1^-u>T#hw$ z!A|+@7&<&5d1u;-6!7a)Ywg+!E$?mexRCdH0uA67_V9I=3Jb&<{B|2FF6+(-&F`-Z zGLuP#-6MO@Z{>JRlvY%UP(~TUbgQ&&CdsA=W9UE9%lSd>w|*vM1BjWAPXLYZh1iw^ zK&jRouoCYWjdVR$%&7cPjj}4vjKQ}VFbLWL%@F>(c__h)uKUK*o8qO?fkl+&xIn}m zYn((V&}MIJ?37rpNXdVKhYkYVtqC4w?|}W|Nf=3G518t6ZPcSl(h_aVeNL`Hl|P}& zXzeiQsB=3F{FNDsDm82M?E(92^#F)tz$>0Qsym|aF-?#n-S1hkladm#_LW!T+{Ip6 zgPFv}gV;g>{nV$gFrKg2bF_KJGx7DZ%KMRZ{@!L$D@Ms#*tQTVh5rQ)kz&5HLCLiQ zpi#6q07V2k&=1#7&Us0;$64E@lPSMeO-CgxP4X`W1ma^rvYU{2rqffyQPOLh1QR%bwURCqun_C!cA`)C0gyItMCGE4 zLsgAfGcG$2Ka++%v@_O9>D%yFpjRfA+|`=YDN;oCQ5AH#+G@$Q!MEL|6! zLTC)d;PoX~Y{kKIX7-dgffsvWXF3}29TR^Qn3t5Exi4xWRVJvZ2Cw0Y^p>Hxz%^(GkmNj1TT%%cc42qx8rqWmGW>{a}tq&l686*;o-k)R~_J zhSMJ~QpR|;LJ5IC(Z%xbc(*RBd$42o`80mNlM+$AJ`1=sNxHsq_|DIH1s+8cy=A}2 zy!o=$L#Lu+7~}zWQN)#K-z`@P&f+|&YfFLMJUM=|)9~cGHWmM$L)My`%v0@f?rV;q ztY*bOk8d7oEMaf_C6Cxvjs1^_Q1?EWfc1Z$@?SQQ9A5v!NRqvqCKxH@N9cJ#n+kpD zXtDO>?SubPSiS?SKd*v|JyTaK;(t=uxg&H>HylaAOp9Bf1@yM=42L@_iua($o)psD z<({Nuq2BnwK;7}{RZq-klA`J?!Bx`)lcDzxf1@|(p0B*~#4VwwBa;PzmvPuQ00NAJ znAt$Am<0}X0W|~a-*BnL!+_~RI1k!okMqp4fqn&^kX4zRJKwIStF-PL{KzccyPhn8C!(CiO@AHk?1X&yH22C>spEfpbRt%K`M!DpIwXa(@On({QC&N%XcFqOh0Z-sCKt z-8BqJ*Yik*G|hd~j-LuRC@J~Z*mFO$*LNX}gr5$CN{Kpz)`D~~AYay>t)H<*Rn!x_ z9U8oB>eTt)#@T{*jGiT9TS6G_Vz2Fe3jI7ylBLs4(53~U#!#>-e{15AYx#{+9n?5tax}AvoqusR9vp0j!vF7%Q92j9* zlUpc)e6Sl0QUznB+ybMhr#9jhk~Dw*+M3Z#BndxZuIa@#NxXeN^>!Srm0>jQfoB=7NUui!z?2@azFBH; zz}IY0O)A_dOq#}`s{$vW|71q3&nItSkG;MsEb$9qar7WmalhctVwN#a6i))m55ool z$x*?M>=|m+3LiylyDW`d>3^B=Mze2M#j8|cx{}Db=1p4$RkWkkimjAK&{;s9ew5ft z-ENXzfws4{?t5YLibS2TzG7V?8D_@!vA#Io6A9Px3ERKN&2qvL~3B66EU^cs-0h;L}r_icGE{<;>z<$xNyR zBZ3_jvLK+7228&hN82qgV9rQDg5EC|vs#j{Nwuz;a{diTca-Ccn=? z2?LO!LI+Gp{D{d#a4ONRh85Dx?X$)Ie9z>>h=~~F0a!SEX{&!x(Re+o)xNZ1cyHk3 z*;wh>e}c{W`~0qc)Pgt3&=r;wCkY^lrG^sBfHvKMO6j^vF9 zg=|$x@NF@r{Eyy5HA|sPl{tJLfSCVkZE+n5>P1ink18$q%p+DuwV2Q3byAM=+IDnq zB)gqHhGLBIK_&A~%I+as#RJxm^e&|VLuaBMmeF&WI>IKU1)msEy4O#Nbx2Pedv^+N zlWNFsDcv!jVsECri$P@JFU6RUIIPbyt zZ{h z(fv6*;agldT5ZIb%~?_pH&HP1v7nH+a&uKqE)3%lt(SbYcWlajS|&umd#YxGev9$% zzO<*vmFX-qbwmYr&;F~kcow=#7!h^5nvdwxRfGeIPw)TY8;1%QEa#6RYUYiB8YXs_0&4PO0c#QFp8;9+(Vm{s@qdK=g=0J+%no z5RO4CKX?XE3lxxcfoWEKHe~5~l($83T~XF}9MF%Sf25(3p_as>f|(*C>|F^M`xN5M z{a^MWDX46{hD|y)Wft>&3quehNigu*XK?6Gva<}gU_ZXJ4={WZG_HMEz8xt0-m2gi-y5}}bq@QihDnlmc$Y>U%@$5SZ@ zRZ8P$e>b}n(M)cuN+vJXx@7f!hZDPGO9H*A1mzH)51))4D0IZ6O{Py2PplJG$$_}0 zA*)fx-#zR?owp z?>_AxDbL8=OKFzF6m%&s6kp`q-(0St{*SM{B1NO6%#>+i50b@ofK`!awdq4(7Xkbx zOtVFaXtQ9WuXJIjr+6u*sf7DT2}dRQUfmW<$aB_~NV&e7h&fmv3$5G~G;W4xvyKWVzk+(0dJz-thZaU}~( zmqZ+@;?2lZTLt#k4A2Jf1kQkJ3CFDzkPBS#3!goi?(k6qSi-oSX2qqqUy!owDH%&@wFFcj(6?N1`cmME&IfAQi$+Ylv) zS+|im`l)Ljls9|xNj$z14J7C0w$y3tp+(X6-6(1XJEDrYg=o12CGa370C`4L-D59M z%d}1?a?rJh@$LF@vFYOQyyak?6qNKo_T5lS18dCC_yP#x zi0eZEh=`Hk2m0U+XmSpqJSpK>w@fR+)dP2Qn?TQ+8v^^6*Wmj8EfKXFV(HKZEEL`J z37&PCz=JhS%Dfo9B6++7G)ki6K88z6^Ao9s>zV=|2<&3Ch;o$nahq?1h$=L>xXb{8r-5}8x8HoiqRfeCXWe{X;m=|g^&Ls&K%`kK=pbF+l_a7xqCiYO%<}hPg4%b3Oe~u%!I%)*5%BM%RA$Y+3q)nsbg84h zBhCs~pg{uW9yp0`jwtLw`t-XjBUrJQ(IA|(m#_{82eR75f`Ejo^hKlLiJA93i?Ta2 zp=oL!qu_5FE>*pU1wa4PwALJajP5@+)z*O_p~dppeNYaVHVg^0p zW@dwa(ppH)PQg7(pbyh+Xf(IEV`1@2i#s@6*PJ(a*H#%^(?iR9E20 z(abCpjFtriDj)e%#X8gtbV3(6*VmaVVKpXSi0I@#4s*c-e4kBi9-HYWq4^lgH4!g0 zvBdF44`UxsND3TC_S@b`*4KLv^vH=ZhYYb+ms5foTm>m?Wz-mvaiS%@)wbTS`FIi@ zQo~$O4Fgiv45cND(UqYyD$9p`uhGAD$felyc)BB`E9JE$dRF!*z{W9*DPOR>SI1ncp_l?+a9zU>h_MU=jov^oMmrk{TMSC( zLIDl)fa~N)ehfm{7)Dl2J;5ZD>iC{|Eq9XCBJ8d7;picRLlKY19fBmxpiXLZ^36{u z8$_QeDs)duLR?CuWq#x(SHd|zMtK6ifa(d&NPlAL6CIJmCRZAgB)ON`kS5bQPe|EM z(xiB~ps@%yx3S$0l6nq|0UXb~c<3DM2z=|zaKX?cP&MWMPdgVA?vOmMFGT)8VeN~noI ze>zM=Nl8?0+0D@}T%Y4dETA5?e=wC6RyGhe`y(&~kBpwY)BZX)5+5BN9}IQ&xyy#Y zg5#M;EA#IvML4>xKTXE&O6c*{N{(mIo@#Sd)$a!Mp2mqt>rGwVH-mp#FyqFzkzck> zYm`Jb|G?V44o>!)6TG@%dY$=6?$K$yTHn{PN4hsTOEWxH_wI|(ISZp`G)`f6z|Si# z56xo|tfICPFHJE@uiAUw{&*;UsYgIA0BbaA_=BCz2_+T@d_>vy-{P2v2W`7xnb%hz zW{uQRZTi)85+T6fyk^|2n^K2F6wXCg60%*_ko^b1;`XQ*c7UudQS@7{L~7+FRY+uV zN|96fI$rj`O=X1Upe!TUzcrJhAI4%qanlh*Q@H?gtv+ zL^vpie3)gkt@2nI#(d#hNg)Nz@+fI)-H1I+?J)jv8M9#Es3a`6F&@hNRn;M?uY#bD z6MZ+6#_(r-#Z-#eie$&4RgUR=`0A*95TC-T9k=Oo@KqMyDA^-vLV}w*$wHY?bR`)A zf+Q=>mTv91u&@*uSoy@r4i-tJI&Y)siL}vy?U?%8z#OIZ{IH+a7Xrkeq5Lo~{PgpG zwP1buW*o&{dW2cT#%uqR%Yi)Oe37}qHJ;k#ai2^^Dm^igF4fFr-}5ag`vSr(`3Zk| z+>f)Qx_*`8(Fe4AU*WC%BSTr*s#aiEV7A}!X5L*kcIiPKy15?TE{AWDh=hL`55&Qazj*H(8e`Qz!QJn<>%5gPLnyz!B$~ zUSyUJUa@b{m2r_%s}^;*8CPni3Hq?dPA`xVC-?Rdt(Mq;0BJqMjGPBQh(vKk=!-9- z`IpnhEci6gp$&99OkKek<)IMu`6tcm$6jYM7)RR`}phop+fehsy^-IVRoX?>WdioZ%tKrB-w)UZ0qP){sV>1CD@O;29zg z=j7s4`uK6p)$qbfQ`}|oFWMcG&~u#QdhlJ6a!^8WiI*0k8L{!ulX8GE5cvRzrlBY^ zC0s9^F<;Q`gwzb<(VYh;7LV>|tZ_#O1#?_$D6#SXhwfay5da_6qO;gL;Ohd>xLq)U z2Fbf>#EapSq0`I+6JAZ~#Qx654=~k6jeRynt|9vJyn;Tkdh-hk43c&j?=QqS=N~+b zc5Zb^j^>M$FBuyEiaZ9(9w}!8Tez_>ztnQ$CN+fjeqqCLp!8%jLXH<@TqLnA11V1y&~qMyT3eIW zy3ti87*2aDDsKN*cg2CUy-L)AMQU^ z$6C?LODY4@x5ljdlx(9R(Ml+)gipp`Xe0rOI@pj`&8voj@~>wpg_zmG`52G>ah*2d zp2s0Em}L^p5T~{M+1fMO(Un#lLxvxeLu5$bDnd!U_1StfW)g{|8 z$srmXiJ{MXHWgt{71&v)5g+YbmOMj5^{%Xvk~?xw)KyNWcj|3G-Z^_Gb1fy34WsDZ zteEIf$8e6z9oyz4#pZH;0o*yc%ZVepNewwBI$9OVWb8;Yp-B3`GI&HLmt_~y}pNa2lC5s6desAvC;Hj6A5 zxo;Qzi_|&I5s@x12ZSIT%?0s`%GNBv7H2(K%ft%Y`-Rt=Cggk4`DaO)`t2ty7R3T{L=R>%t_tofJta6bt-j z@Nm_wiVRkJS=1Bbd);@9XfvL5L$&A_U(g__G2Gg#QzNm2-|EyzCG&enHi^C+Wq|Jw znn_-DZbf8d$qSgl2`rR!=;Zx4rQyE|e-Lcqe8;Fk_>) zMKc+DSZk$$yMHC5A-Y`kE@>930ehk5Ru84no5G5LQN`xyWB{!S=Tk=UO8XNCrb*Gw zI7=%|AR4>H#~-~pAtoYT{Bq;p4Ky;4$q<3)r+Y?lidra=O7uCjPgM40leXg$h1W+)~V|1)V(*sS18j>#{s{S%=Xyo3KdMT3>cb&0ngshQA z#sgTfXQDXBh2WTpxtzA2hN68S>yBzqlZ9aH<*+bi-|+bq^P<^`N;?whkIbjO?wwa}X>?RSMycqUNtL2bcq|}Kn?00NBY8NbgK5Bkbu|!xxpz0Ml<#Y0gf94&x zdiM_hyxi+LJLl6&+Tp4w(M`~l%IIIzYgU|2TP|EVygZs3dCTrqopp`6dQ4)YhQK*p zDBo$Xq|+mxMj8_Fb6Fnh&0y1-GxB9?=?`D}^D_scYG4}7 z64ouEifM&UQa;uV=(N^dvg=nTol3NR(9g*mHI%v~o*V3PG|RFgS%uZy zM_dU#4pFQYzMr>jy<3)>3IX9zFL^IYxkGd8PPp$0`{nF;NWNSD&|gD!l6@3ml=h|k zkbzC!m;}y&P({XKpgR52LxI|SAB&g0YmpBO6-&wqQsYFx(q2ASqoirTqaOCh6*zS^OM?N zT2ucy2DuJiB^_~j!+7}@FS1RxKZTIQ22&yFFt?TQ7>+$ti|j<0{`-}#n9HZ&JpM_6 zTVOw*gNISz6c^p7k!i1iqyt}nk@U`?od^i7L{875+MK;d2%!O}GRsXjEK!gZzQyNK z9K2d^pF7^e# z5S#h<;n4eEVdqa27U zx`ca({;Fo9*%$d0sM4@lT#1ko>I<_>=50(`T&k-eO?q>^3xHYA7!gJ;`# z)Rb#1H1URP^zu!s_Ta@JOR#B`vafJtn$+pt++d57M7Rv9 z8zu76>6t7Lz|yyB0)X7?DHy=(O-o0MfS_QVN~Y2X4c-YH2d~Ni)?_REKZ%@ z>N6Vu=w-9awTs3(0Js||xQEDoUz)H;EJv;-m_JdIjFlbP$Inqgn=JhX1%*8W=u zFwXF5H?!W05PzoA=RB6AwC1xAe5yvTOW8mY;}nS6EUxNJ^2gNzJrjSqLvu*~5PcaU z><^TSv9I+Fa(F#va14D46`KLJ0mcP(Dbv;xab9-g1(>BlXOFwcDCT;cH5C)+nDgyp*_@AUdR4DE68@nMR;;frsY59=`r~uPh|a z&nH>ss^$}4RkIKFL0^vSV^{3Z-Ppv-L$$~f)Mn%_;fp8ZZ1PLbyTyo-M@EC*eKsHD z)oq(m`pc(uLgSUEb3M{NXO`7jQWCZ@l)VPjvm$M4cSwo zQrT%l1SG%kx%J_h`&-8?^OHS38M7QnBBQV!!T%lV#`bUemAbEsOUShO3G`&yQCtT! zYoIN_3@~*X=tofk2kp)*#B?8KfZB#-nGdTGz&(buc$?$a5-*v~#$uNJ^eToANmsMv z=u;or>!s|#)9`j?U()*t^3leY$;;P#&ai-%iGXwLK&XGvq>&MG;+WPiuEV17%bgH{ z37N3w`(G7uUf#mCv+$Fgq_rHN9F&R!?2vKcU(8qS2eSuz|ng5*M87W5>Viybk!E(7E8RL9-Yp@(S$a|T&TYt@r9xVjaT-?ME~={c?|TPJrf&@=|+w@aL+c#75wG-aIP*W}ncL^zt27&+Y;B z1tDZ8u?u}qSqbs1rA_6wh`tj!(VGwBuZ&E~U7sD_kd&mon!b7dT*{fU`i+ql;{{7p z!fA>ZZd&(7Bj_NH{yXiyQoUY8b>Z>J#6TA9MPXxZuFX;m7pZ@+zPVTKWlF|NvYyA@ zo1k>>1KlSb=LcU<;wO8PWGB-%#PwyJ-O{O7$Aaruqjn~DCZYg-PfKUm7C#A{Ubcb1 zbSm^b2mXbqdfbro`O*!)XZMBowa!l=^850YA55bIZO{ahEj35%9d%O0QKY1_xC){7 z%v5(}PSK;WK$mDv?j~pwMs#Rcjjw-Ft)`?}x{u;s%js{F`j>U9$rDb;)6;K?@(b_% zzw~T8;b>4^cxLzadxV_--`m8-B4Nq52&RXtV`#}glS+Fmmw}yH0WGAg0)pr6Ns!Wq zRC|%-#%}Szu1-zy!F?-lJmHBnxNR?gZwo;u!SP( z#aDi(1oS&M^31seui^^e)NRTUvt<>&Y%SJ`2ML$jr{gIBL44n5jcVoG z60TRp?L|oIV}I2WzzP1ueAY4c<(4*8iV^5&d_e0(j+!>vnmT@B!>>yk{A)BHXoL?} z>3vSMwnyfAZ?F+w#}Z@##S2L}6Pk?iH9@i(Kk!x0Uy9!1H4xB&11gy_V!-vRqinpi zR8xtFvKYJ7n6eqTzJo1$0I5>y|Eo2*()jU@GOYFD_ParY*(dK!2w~rk-@*uCE!OWr zr@bf|RERg?=^6ib687@n#U&u^+h75J8GsA{U_QP{1SN7Lu9c%qTZUU3d3P1i`@X?r+S5+5M?eB4p0LH$Aw?WONFezjOFjv6f_W%Gi>@2@xbnxTF!h3RXw_*8WAo zEgPHmnfAabLWg{k8A7VovI^2t8Dqx7<-aPZU`Jaz?wQKRGlV09Seb6Yx*0u>K7e+*=C=BxWDI=q~JLopFqc3F`UcIvyeYseiDI{YO zdlE#;{bmFSz~G^QuOWy%Ns;d-Dx2iyh0tD;ILnMmh>KJ87kvOSUl#^^GiO8#I&elw zdL`3Czf?ZHMKI-PuLK+=z!$sIm@o|feovL>IL`ny2qdPc{kqs7^HDiVtjGkIjD=x+0qh zSSqX_P_}OlKRt{YS>Y0)2!7tn)+B;*Bf5$O**^jND@>YY$xtp&Hxlmhb}P7N6`=(v zcQfmD3mD7q+xRngL2#;9Eb18aePi{lf8>VJ9$V2l85c_{DaX3Ra{Q0Ks@~S^$#w4M zhi`8cpOo;E{Eg~8(772%T0nN7oSBaH&;5fMj1T~+Lt`5DYpNS!xa`Oc*BL6NmF)D1 z03ATisjW$|PI=*fpAslq^3;{A2Buvn;>HJ{CP9M$ga8r%96$_r40gcWpP#Ry)8DH> zFzi`x658-!BSU-tzI_}NiYfRT-60SA_gPH>tlTtd>HfL$M>DB+FCF_>@HzJ%PYfta zezXZp{W7nPK;0W9AF-?+DKv7~K9}Pn>{~@qlF2AzOR~fHzk1ofRM}nh!Rnosym;4! zr*7R_Yf<=Ax&FUYSw#NebHB3;gTZe?S4+~;TGRD9CKKK0d0-pZlW2xwC}VIhG#Xj= zQ2aYSD_3>tUmuer(}8^DV`HsTTj!@uGf-1UT@Rg>$#g_l05pJ36*vV6%6FSIl=I&h z;JuF&;Jt=6S#2h43b=FATE;!E$gEoh6s)IOjX+&?d2~3()^FZ5xYeBOX zKw$KNzT()VAaZlH-#2P0=vJL^8G75Og%R6!X3dCOH(M^6WP-hvbE^j~yI@NJ@hHFa zIU*#u3#>xs#*Ti0%9OTV5_)=PDqyf7+)yP;>&!(NPbT89`ph?jH#u?pKUOo@NQ#Gl z%SUqsZaf{p?wd4o6J>qV!DwP{19QN0_5r%E2R6U7<-^T?mg1Lse@i|~u{uJ68b#tf z027ORlQ)?(H_JR9I@w+du4`R_Q8#{#?4Q{-)Bh?F!1^T!x?gQEJ!&@_OKxXQ3^QfE zj7TYqLOIA6EBbQ2ct=q+Qz(%Gz-xJT0sn??IAyYdC_hR77M93W4k_sX!QkoI*&2^@B-Rfm%aHNGcMRh zmP^OCo|zmDNor7O^F?WT0^?L{9Ws)uvn7Dv>tev&&n5`T=t3L4UR&C<=Cv6Uz1NEK zjQK|+V*W33-r0^0#sdj+Jn>+b#46%RWi9pRy#Zg_W#7!7v3KBK>O&b-l8xfFVw-;$ znULV^eZ@;id(2Qs#x{T4RG2#td<5DeoFT2H`;1Z<{4E>fTezJa!2achsz!ee4;%Dj zJcOHF#=K_SR92Fm3g4D-Yz7?HN8A_al4bxhNx=2hno`vPb|kcrnMJ-4pdV+m2q^lk zf9Xd0X?piR7&%FZ0FvYYD9{H&`eU!y%XxngG&$Qc9mI&lWtGiea39>@dRmOGF^$D2 zzB8_CCsQ{3%-wP6!b1vg`o;JS4eQ;cie6G7!SB1emb68P+ylkInuP_U*87|{J&!9H zCdd;mk-zX~P9MI*9Y)Ur`S-oFD2s)(z=6O<6=j=AUz?0Hp7?L6{p;ZK@F-*{7It>dZTkB60jf7B5Cj~bHx zztqrOd0ko6Mpn-};{iqSZS*uDQV3(EO+@=Hg;dq$P2$@Hdcn$rst^45E9w7TDXM*m zbC72+m^FAD-gUdA+Ftv%7Wjf1NnD&sjU^=H`G|*&vnU%wn80C*!#$Y)b_LmkC&_Ta z(vgWN@Dg9eK(;W7%c&4~p{d-mn@|7W7T`zB`JRWfET)$aUPo7CyaPN>O8*z2+t9N}vn}7+Nn>M=FS!mxW$?seXpB+Wt;A@Q#=-?)aML#6Q>=)6P zL$D;fnbqkilL!wNJbfe9nI!$r} znq%ee{@wS8d7Xb#MP2J(5@l(xI)UPFHP8gg`y@{T-VaW|09^ybT(-{L>q>ajgI(b* zodJlTe8xqIB<>>8;B!%y?!LLvmnFD$k=i}PCI4bnT0-}6y^JwT`jTND=PmW-iTW>1 zvOjJC6^bS0CfdT#Ah`W=)f}SKT%rUo$Ga}&mRtWkxkbvI_@vvkd{h-ES0y#}|J?ZS zF{t0^XoGA}MOO^TXwQeQ(MzvGuSTV`WWRo)-WP#ls{?^Ur`Wn+^-|l1* zyq6msiNdbBX-DxHuEzIWaczsU+xmEhx6u?$RAbP-1>pUCzda} ztplH6%azW4mf5%Jqs~b=ZIlrdtOQAH+7g*_w&U8^MSB+4~OGYBwYh>5E+YP$nojQqWZsDBJRG#wcVs>&LUsA$9CzBH{4}-Vc;JfCsp?r zNRs%F08E>yZk>UQtYMv_XN}QYnBFb+c%Hmz;`6w#!Zs~`^XJ{cOJ$%y75hU|$tZT> zV(xG^lpW>3_V``Ts91cM3OuI9P`EczK)Gsx5d{Atf?fz7@Q|#a?DuIll<*OOkdHuy ziQ9PkB{7dX`w7+l4hO3#No3-M!=7ld5prqD8T#wK%`fnWAN+s|B3`|<>zrRC)F^Q! zY0>J%Vw$t*6!eDHU#|0_glmFx$*8O6<*0827dV}$fts(1gvZP3_9cfZEO)gAw7Q0( zBGeUM3~Rl0izA!vKeIL7{#=N{8AavXR8d5vAB{cPtoc&%c=fu)J7Pf$a0J)_&Vc`H zZT+eC<_s=$F}IKVcx^XQ?1i{1SvYnAD;{aQXP&W&^tR4$f30q_7jyF=z0-#)5`pig za5Z%pGh(2HXl+fLA!1hIurmMz0TL={0Il*({7C>Tr~c-lf#1NQF&b_3fR3J;DAkHF z_?2x*HL`XSq+$lCB^$S7DE8^YRpvNK`R%>%&Z32K6=J_V4Bhsu?u(K;WlCR8S{};BR;4&rzIRFH3O9+@HVg1dDt~h}}yJBJ_DY}k}$dX~! zZq2)2#SaA@4R~ez;x|Iuw^sQ&;b~+t==2+n2nn8{c(p5sUftEA%`k%&o1y)qfSu@n znhcC%dc-?wd&WD;JFg|ykbD4mSu^|Edh7eM;h03dLE_ljxrq$u9*WIv;%+#s)QXN+ z2XA~Xs=8u~vkvHy*ng#S+Pu{LXR)^AkE`nhrwbW;AwV z9UAk$=8C!)Ww%9*n=o?2L2W=!@C>Au++=u$%&*xr-}&W?CS%8MI6s+ub{QAx^BAJ* zOeZ_I`t~Qh_!Yw`={ttGTV=$|qH@V~O|$u!`np8&bjj7)ezKA1;$h}%#7Md7*$sn7 zN*@V&w@U;6dPC{~Z=|`k4NK6=fOkM7k@NB-YNQZcw@Lc&t`E9YFbsZqh~MghJoaCw zjuh?$yLn5gw>iU9w?t@X9jH2lJhLR4y>eiS@lK zn+_BT^w+%zp#8{x=OUlSLd@letf#i^fg4R>ReQ%~hF(RG_dJR5yyc!-4>;4VliQL7 z6}W#pvv665DtD_as*T!(NBV79V)WlVtL{9Zqc$uReU2@nn~}HsbLHU(Pw9`pIa7fT zmKj7gbuFc=Y2&5gQq>!sni`{F?HsppHLOtK z>`~!pyww-*RA3c7Yv_m3fuB9xnM{Y&EC0P1&&1AETM}nG;0`HhU*~T%YP_s-w7IXs z^RMtRQ*QjdU{%QuOG9jdQXE>;yH%Za@zV$JyuxTIfe!9r#y2{qByFQAJ}K&tpf!gs z*LQUrx$TNoLV1~Gs#;$=6JJngMr-#%Ohl(6Mc2E9{OXiM)@VaGUW^jccbGsyYQJ=y zaWTF(?CGa}E))Hrjw`ShRHBua_o#e39G~o$*&(I=3q{C5!0fpdScFGbAmLf z*+U}=O27Cdyx674LX%Q0xjC{jXsI?C3Y#Uy zjV|j+W!(G4dN(ELnp{tNIbj~*L=2Y6ew0$X~Bcw7cLc47(`l#PN|`0E5w-s>tM8s z@P9p(I#X5qDd?63V&As?Z~m5CScFkBXZ6FE4^+nqbWS4`yWeSRa&u)-jhs@6pHH2w ze5bQ>(|$GmvLI1V&D`RZBee#EXV0^sM?h<(TrW-(CEn|m)XZ!ESDwwzuKArWK27ipmt3fh73f+>@c5D|TzsO8532I^jq zIHfa-h7F!~hQMCUc?UG+E}h@Ic?UTR4mTp{b+&)0uBhmLL(Lk8l2D2v>P@s)7nNds z`g0>Z3zeuP^?u7uc{y-UoBQkM$y!KlY0`VaL1i&fOJ3TLO$YI+mzp113NoK0WuL*K zp(?SsP8Ct+G^69^i&e+ZZCy4EUdlZDaqrt}+7NTk$#ljJ6EZ^ZV;2)7J%!dKW!jmo zsn<~pQJ4BiG+dp8HFhN%zEfqqC-w0lE$9m@))(dO9CNR;DmIK8 zv0_b(ofsgiX*9Hz3(mbh!DYDWFQ)OrKMGE~7wE;e#?9J44=6Z$7s;a^DX_i{oeBBZ4cPq4e?H{E!5y6c>=6Y;NZqGGldWwbYN>F+o6zkt*v` zyt9x=tHM~vf6?m;AK}n6II2%`$tuYzd|BNavCz7xtxhtSEiuTAc5S-% zrFC+@k{JrPG5E9WsR%%HE7EslXrGB*0!uHviXQYtpL0=Hf$n2);o zMCqGC?nTN8r%E1E6N66jbp~_C8Qi)ZB=BoF^8IJ?GrEu$r78ILjPm{WqUqe0H2ajV zQ*M$TsNd$K%S<7AEXIssSg+72{pn8rbw4Z(-lvX#PIdTMm^+VuL(?~{Nsf(ix`2`I z5U#6Ngv;qoDpi#zzM79_@^t}PGu=9p4AFK+3_tqD;nCQ{UTQI!H6n>jwoxvJry)Ol zXJBW+7UAmf_gn~+gknGk*NZr^xUi`&vU{Sw&)Hey$?`_~r%L442LJS-16ds{`SIIf z=6qE(`y{{XJPgWn-1IzhfBgdiNlvR1_e1z)i7LZ5p;WO@)p(T+LYFuB^uLAh%<}P3 zw!6F*Ngw86gBdKjcwnhjF#^Is=k&nQM>LwUcZlrtQaKz&9no#}x2?7L&l|_%U5-XI zKfIdS)XR!Aev3OX4kO&ZbFV=jn%Wr z7Is(iS7Ae!`~-P^MLeHPsN-s=L{LRPe(ZA|)^*d|*N1Awa`j~z$gpazX9wu}=-{a? zw?(QJg7pg3<1h3!5+1EKVUa{%oErBoX*e`X3Zk7a55BdZaH@96lG4Z58f^Dmie$U2 z|KeX}-r#Kz}jKs8`&I!?^^r6fOo~|V!_M8^Z_Q>WFWcndfiSA=h zKg4W|ovIi0BzX+)v1Tw<{h>HBf!^vr)>U8b3Gw5kJd-OuGh3ADgnUnf4r_wN87m9* zW;Jnt?sQL-jWTnV=lbmrwtfC>ca&SS`2P^~j`4AK-TQW9+fC9W6I+eV#`pj+&ErQ+-dB>IL!&P4i&b(>`)I>#Fz#Ddk-J?Cs#4<6%4oT{?f zSh?`!EKF?=jQP-3Jm3X2dJcw6|7s`@B`N37)0-!9!0EQU-gwhS1a@{@d(eP(Bv9L6zAD9&~flGPiDn!o) zyMsnXzrIHje1@PM*k-^a)Y-*D-V}_*v*#0GJ-T(q*9@q@Mc{5*QO*N@eU4Chv2_xCqnI=Jf zBZG+dtH5NzPF7U#=BfRYpKH87<`N9AH z;aCC~UY4O9;y!^4;q+YKH-;vY*rDR!Y6_{t*Qb$a zdr7&uxG;hmjPP-CR`_w`(!syOkyRi}ZSvbB{bKQ`)`ZMZRwv3?`(Y3sY&$<#mxC`) zU`aNaVr@&0c@*Z%4?l}+2^U=ZxjZ$ZO+kP|CBr#Q|WqA^ppNc1fN5b3N_eEWLxqYtu-#-?hTX_=ej5z z0dI>4xkHw|#WWj?5W747lUpe2LIU^>lOiL&kv%MMEku<@GMI009dJ9ChyE+{kEx&M z7QT1w#tjvYyQcQD0KHs72MdG+8+3;ao(wb)+mC-k?bmo_6r~46l(r$0zr4V%F3M9% zdjwp5DxPOPi+62$Y6j5@T_0$7n$9g`nu`wFtC^(U=57M%)i`Z_5?vmwLO=wo5TqoG z`O%-g`O5K$24A^7ngX_ilx=YU948|3&}It4B?HzWx?#_Qd2@bc+04g0%AYXx#fnwz zMXAGU1^JTvQ^@vFB`(PxYyGFty2mY0=ebgT+0x~F+5vxgq$gIK08+qgVO4PZC3Ixp zij;sVof_@z`^mz+0BO~Q(N4a#)AZLx*~JIHJ+;sWxbq_|8V*Xz6>sId6iG8_vb;$f z{`Ah`g3QM15n7HMpbdW z+~watI-8P6y|1+r`XHdT?0`+r((R#wKhiA<5qV|&S1STSUjDU$G)&|e{^DFClz4}|+TmhU_r#qmaKHPc>Z zO!DM8*U~{{R@1BmtR_33S{ar2z}-ZiIVmZxpYKF;c24S@?(BV8+UnlLS&z$Vqq z%iX+VZF(rF+3?-#Xnm$^i*Se<6pyl84wy0|ORy1MU_ys?U!C(rS5VhQ{p5^G7bmOj z&66a6L746F04f9bb&HxIEN-hmG3v*$AVGeE;Q3FKCP7&S&VD49=Tc;Rz0s)$)Z%~qd-buc?8SJKQkRe+~_`FHYg-n0N_9Hky zF(jblPwv9`Z23L@jd>*h6^z#M##>ab8ogK(4dlTD(L>X0*j+~HZbWjA|3$ulX(aO+ zQ`1O>oV_FC!WH17?6n`C0>3B8ATj*IgvCiS;gPpl`!?B0cdPEY%FpppR)?dz;(lQw zaC;;KecRetWL@TN-LfYI#H#N~^s@Bz`G@>sB3n(rms_yu!Mmpb3B3To0X&!6eY12L zwn&yN&M)nZ7$FSWEWoeE9n5jN^?8Sw+MtBMlILK5>iONELgqdd9SngAXJSZWf>ATf ziCc*Aypw2A#a>i!q|q$cOMA2-UJilr;kt|tGks}CjiJ{<@yaD+mK6}j)lay6rtdDB zli?{i?qqT~VqpZLyQtmm=qN*VyDXeu`fdMPWgP~%3u`c}7TzCNJJW!X$jbTpdn?pT zoQ-YyG7=o=D?5QUmR2bcJ%}%&OhJ1jO;auk#OJy2ZohF{{@CE7cc zHNB=|zv{NmUXRY}2&4FXD|7$wU^t}qN`-o1c4y9O)V2e8)b)AY2O?i7Ar=VPwPXD( zPgjW8Pv1CQa58~?IuO80+kDHyYNsB|{>Xb3eG0xt+S3fc=cesZDFyV6`RW9^ENkCy z@*FT+3VMDgyN;N1%LSZ8sJnCqsaCkbHw*mn`BpnKZ~daLHAnkh=lfPz@hhqi3xhe1 zH-oiqo?HefhLn)m6$$|^5@OEC=g4=@}Or-9Z+^-sx9dDXAu zBUd%#-(j9L2{@@oYqd7B%zsIn)j|+RhiPB^NerU~wd`5{(Fw;?-!=F3W{&bO=(nbk zC#S0#5gV_KSaRpLaM%9wl`Y@lS>-GQIR^_SZ;C^W?ylJ%TsrpA4cR?2hznS(65nKn z=NI~PV5M=-Ucvs;kpwpJw9&rEbJ+uk-OUHiME#k9@`I}P>7gCvQX)l0fwcIDFBoEvM^sStu+;G}c5Yuol&QD68lyvGB;cQEbqv#*d8cXQIBF zJQky7Yr8A9bY%M(!8{Z?#9<*`m8k(d%(t~|ZO^Fht5)dV^yXRM^f?YrpA>6JByxjd z3a=yFr@y_h(%n1d#9#ON4Y}z+-tu$iT%f)TDo+n{c<*^e7ilP)2ISkdVYHrZwRG%P zGO0kg^A-4SPw0VRBK@T0_{eY;u5;z5>z{yA$!OQ$Z8~IRR3?Y~P6U?p$6S(a#Mv$0 zjUeCl8mFzDE6OhMpQtyz@>4rPO>wCK&MAQ2%)dAG#TLmcT%~y-Ay@-t~8OYkFTg%rnA)jqquij8j1<2W@G6HWP_$+x3mp$+O?h$?h-*ZpMn`rp zxPBOkRrk9D`PSNfRCI@QxZx-+d|gts&hY0i6RETJy5YcLlZ}dEs|;z^wv<*b`Jcbb zn{bWhm0Uc>{+0t7-{iGAy=P;n#RCrt*C>A075gA`|+h0f%CrK)P z+G`z_IUOf0$}CCu;I%&FgM1NujyzB7%nSHIYsb6|N=ECiI%hKQc)Y=`T=CLApGIm? zx4Zu?Vt`de`Rg!3uD9-@9SSSCoBR*uw8m}e_NO0cF(G-+5_gfuT#=>#jD=uZ-!=s%{EUoUKFl8a2&%;oTLWdZT+(fk(tk z%Ip$z;a{r$rFJC;4aJ!b*SI+_c#JssY`LPlDkdm2&}Gt58%q+8gtw;EV1~O5O-Lp%$V;{lM{DP%ha*kIhe-8- zB)^`b_OjNEP2fHE>Wof750|L%$Kbh9GQ;fbkM8E#Qwt0~yy9~&`o#;_LIQ4W;05%e zH6anu0@=!CuW;$Fb#uj5>&nKvFs}j3pTtr|%{Ib5QZME45im~(w9&o71o{owf8&Ms zj$=;w4KoSNy&acP7;H)yYzH$WOSg}Jj5w1o21ZU1!bAJTw#lyWNew6v>ty*8f7++Y zI2<&`Wx7#)vxS~LN#b_vVq z)zy&o33Y8Gx*IyNxYk}&dl&>d7-IjD5vIt#pC5K6W=36GS98|aALmmWz9_H%IgPT( z(p=R%g_jjaRbQh8zxRQR`ZE+C<@Ke=ri20!|L;y@NV0$R9!~6lt*~($9YvqIy73N~ zz$)ZMx?RKZ#4PM6UHbUoUU$0iItYCvW<`&`)N%hxj8gp}m{%;4pgP`4$qlt^#6NeW zTyV0AFmzFwafsM0;MptZ94TJuiGexa`KAHiJ-#2)<#%+x$n@YjQ8Gh-_l9_WztE^+ z0n6r*)>f1Y72Bt_UE~reVG2^rOK~N^mWea-m=X5*AC7Hoe`HdzmLH}@_=hCJ^9bc( ziB2Nn2=WL6Dr!h&K{cFLQ@cI4@KHnqv!_h5X zt9ir^Zo-?{c$aZMN&(DugnSiu2cvGzmp;Rh+#3q%aaiXLHz8Dt+({r_=OMKi-*@%YjeDUdbeJ;FL;Pa?A2e>(n=r*ii=YWdL*& zSZBlRsn`#GZ*5fu8$Q({&7k&rE1vX}moF#ze`-=9lc9@bUdsEP?lt&1jT;IUAu?z} ze39Jv-E0G7!{=k}4ZFX7%g(ZZs##-y(qDd{g?wLN`rj++=>J|(m%?WMdqp+=pCI;r zPpCLRpTjlTqAdJT8NHoY7pybuF^iD4{v90&EeTmCZ^X=7w?6JNck#zBw2B%AW2WvN zLC%@IkySyWpLn|}q`9vWY0MO4gdVTG);Udoq@W$I-{I-G!-Ug609BX!X_%I4yYyhO zC$dOfg-vXfSL{lU-G>C>Z&b;f!G&fDQ@N~p9a*`0!L4 zR-wH*XnnWRoF!A)XA8W26aQm0R2ZHGcXy4AK&9g7F&;-l;;)e& zkbq_Tc=+oojCQ0ULkqlc+u?OJjq8$G+jW1e7Ucs%x?~sjc)}4=xsB)v0o!x3cdY?K z8k$_&L*k`#E?Kc^Rf3PG=vR|&3Hv5G1bjzwW~jcB(V|T=22gGkw={l@pgAm07?C(C zXL8`o^!VxE!=z|3hz?J!`OEHZu4S?KEe}1h!xOX$+P?xzf~N?Bg$ouE`1wO_licpa zKRm{_Ad}vXb;Euy`tBS|8e)vj&0QL8w z>{|rcz+|dPv*7^W^nR$m23N&mhB(#tuxe&?)UDw5;mBrCTrnFD^)=haBL%{S!z-$; z>u^82C19IbR!?xG8|Zo5z{~6h*GXfu2kq=LJRvlt@as0yJ`nbIHReysV3g};!=`n(Vql!<26Ym~VzB25m z2on5&FN6_8MN$*Tr;|$BhE}VZ$?5|5M;@v~lwhS`-^fKf4FVJ}2)tzgwDCz%iEP-_ z+QfXTB3Gp70W^!BEs{Nq3SxFC%*M6C?Xw_Jg0DaV^`yB}fc+qm|N0Jcj#s3@?3sA5 zGPecg;(v$FHIv1WTjEngjaAKOG{HO=bxF%>)^m_<2q?R4&=47kY8#BgCLGGnvtsaq0TMnP2UpH(?Nhz%`(MC7`QA zSb*vWBBT~xS~FPM51F|<^K6*#>8v@_TM|4pFLUysFpYajLw?Qly*4|)(lgVXWj4kT zJpB;*_kkAnL%{#S)#3kztA1hR|AniF|36%rj|ODd0@`$}1Dd}Lzz0B;@0I)0a)Gu> z?@}Na<0O^He`5TR@aH9St>ZO4oM!>`JlP}@k9MMY5FD-0CPrXgRkww68+C{gK!o~< zb#s`+n{vOiHTEiROxK+n8+(*8jp#h7E&(-LWlVqCe(rv<-BiVE>X_Us0J*q82~oc~ zHeG(7SQ(9WM2SD#Ki1zI+nk3}b_ZEK<-(;Ph3=%{Ww(!_Tiiw~T)Z0AU-G4Y0oR*W zS1KHj(v`-!#j}0k7`+c8>nRvtUQ-&>DpJa9b)05vqcJMY3GCDA|8!W3`q-T1U%x+5 z#%x)tE@`<2m4ukBkUZG!B)=avPTgyNG2G}oWd>;35R~}19nYN1YOMf&$b;Tsjvv{r zkJ?gpV4{(32W$zDUa*g0h%%IfOon_J5WFEHWt%g;+#p`)6g06N60|Xg*5)JoqsIeo zsY@P$KYx=W7Mj(wLPH7Z^XB9*erk+%H6@=sZ_J7BhW4 zb-Fwp?9Lq^hUULQy9T8M-`(#-Q;TRI=|HV2QgvSml8A<}e=0ChxSBP@$=8x9ov6Xb zO)4KSyZ|l~pVEa{uBeKizPY8CSSnwm9TMWKi$9_Z9zJ97KwlW0#yyu*Wes&UqO{}b z_6@&CY*4+(f9jnchHB^Ne*EKW^i47TRP3E#5(!g)QjY(?Yn1=MxT!sr-ZKCTb`?a} zyCilH{9R;Vjb!HsuTJZ;f>wpq5>9zR=u9D_b&TaC$W<#v}hhTwIM=EFmvhpOj zb+h4iQs5Qy@FED-^XM4oa}pyLbYPL6?&r*T-NWJ6%00mAl*VR^(_dg$@yNMrrJazj z*n3Q7bdin#6()2~-@#^(Ktu>R456Yez=iz7{}fL zB=thod$^ouL~%k+rA=am#>=34Y@lfm(kGcg!NgVz%$KzpTOkS?8Mb7++TLMje(X4u zH{u3f7Y9i_Iwnzx2@QhcZJ1Z#DXM6ds&w164BG5;4@emsJBm)np_2!@%Xn989(`>N zkD~$woX6z>WXdv$f1W@!{7ze9t`~;y zf(3`BeCyiIr>pjh;W=(P6GU0LKV?uSW#*|u@3#>Tr&C^5RpnQY6v$D(bj(3r31zR- z&H?j36JB?*>D#y|%*b+un0}LOLlwK7vBLMnvBAvk`+|d)MXpgEHD0rAjxrXRcb$r# z*yqn0HojHp>n(_((V+$MLAN=&QA1G;ENz%wXx+P7l*Ugqfdf$ zrlG#Jwxm7zVQ*DsM;Zo~yJbyxUzshm`xauW27i|)cgCkmU(%W4rYk|&$u~I*ZQkIS z_%?Uo%n}&$edM_p0`-i=1uino`g4igkVjSzOY-DV+`=C5F=#6>F3&D&0fZA zfjsQa4)!vP``7@c>yaw)o_sr3N<=}$>@7rh&?Xa;{n;@#vj3uy8mC=Mrh?n6#YK5E3VY1ZO#D8q90~i?E#r(@IW84#ZUrvt@ z2E5nj7L?bIrU(a?Gv}w6u6*=pGY>khiFj~dz3p!Y^2jUC`4Qa$koi&h#+U18)JpjY zJ~!L9E==cx6wS-WoZm<5gc<@m)$9Z4^Q}7khRCL@xs$CYn#wmnpqjN5z6o$!q}_{r zmT{x-{95L8+aRE4vCt6RSPR12JY(mdWqe&G(PS0o;#&5aVpi_U;$F+DuI8%&{z0X||9_95h6 zv}=M&$^yDCoTbsToPNRuT&U>7ca)1>=1?u;bK3#Oe4MbR{AIwUfIVEFR4he|?50dq z)Rqg7!)6?zp2UN`kZTO}ju=LFeDZA$pQhim&XHlW`npDlXyN>DavHGx(%f`-43655 z!iS^5YOMfbpaswUK!fu*va^;zVgPi~m5f)+LAc(7_u2I^Yg#xol%I5EyfRdYZ_CW# z`s`118MaF)i74(bYIju*xA?35zaK$vqZ|()xZ?3!P^^xU*TVClpP1dpn7-!b>+WPj zC@OJS`(vM1Dk}VxOynctxyN{E-whn_Q@SfJ_KTwTQ*4*!SC+BKtbi!p?xeSpPyxF0 zh8V5NYvLFj=I<*}29o)o>2z3d+++{xD;I<(0~e-Hu(sY8hv73`dVuPSRZC7k-XCj0 zb;3u>KOlGJj~bmnO5V8oTVfFwxCOG5uH1m^)Xqr-TCSefCL7RaRnr0f!w7?;P7{2a z&70Z=Cl?olE`&(;qKM0!JopkT1YW9lUT|-JMQ(TVOs9R`Z);ivl4GR&;U4A%W$J~p z?5wREEHry}>YSGYPjk?Zn(K=*omP)PJ#~Ccg$fdiuD;Rri5G`hE?&!emW9+&KeZ5_ zXR@l8K0b4A(Q@wb-~O0(t)VOvD0aj;qD3V{e061N>~MQIDG)@sViI|PEO6gif4}?4 zv;3hmy1Lf$CD7Ybievw)bl_=qZMpQpiofna@Qc`-`rvC(fCb8Hk3_u(QhebaNQ=J4 zaMFO&x~rilK&c5{L^0c|Thr&<(oP`lX5{SQa%{7@0b1{?)pW z_d*0i1?t+vluC?%0uqKo2oxl=>t2rp#<4cLL!QRfgTs2NI~Wd6_hK*O+HLzzS>m>< zs_592=Aqmy!+~4|C=}H(5TWy|CMv{XfXIt8a@~+=ru9$pmCT0lhE5?E*wt=}Vr4G9 z`ilJ2d$Hx&jkd;qmeVzIqR2N>5#f6@59K8xr5#!l|HOPAlAa!TxzWs90^og1lOU@h zV6PqibGs_rC7H9$brQk{au$0$za`)mKKGS`S_Pz{v()2qw271ra+VVMkTqLY4r#Xj zVI?G()#8r`b6PfL5{`WkQK-L+QYLQbnO|qGgj@wg2`Cqm>xR09VUoZUe1a1r$bB_6 zK&9qeD9WF(P3ky?LK zaFo#Q)JxoYo;rC0f?v6EROMk`=@T;USF##siB11qNWyt-HFe?W%LnpR+l@+pb877G z$i$k>o00h7YcaWunOf%{WiO#d#o!T<`EoHNsY$@)y0S)4+zH6T>?Q9=Ys3rV$Kt-agX|4|1)2!Br2r8WMvE9 z1*U+I@xa{~C=DfQBR#O_Z4Rb~aFiE*y9w*74c0WVVORT$M+)Y;UvjwFru51+ZuzMH5o1<;_gcg5 z&@3AnKrI7uny_|%^pCXgZ?N4s>ZQCuj^0n^7>RKOE&DJeD$=ne6xf=WDe_)@HBrz{ zzV%^ICZFiyPEJsWD3OZIjhoExQ(Yr*+Lq&}Dfi=wWj-3rQsvUg_jGzLATr{+3+uyS z@2l=>=_=HbTY&G3g1*?W;h*w?%H42%=rTj04o6Ki%g1LO zdEmVSVsU-1OY*GG<+qFY&h~^`PGSC2D}5;cR91$_CxS64d@|BasCc(5DE7N~AwOSO zU?zx1O2JwYFHhQWum1INWCJdJ1*_|R^#w$4NqkmHf$X3;UF2H-B#4(Tb8|`b5Z~72 zc38!eXrhsu$E~dVxF>tYdNajM+WCUg*cSr_(rtZrDg0Y}G458{y?K#Y@%1?rF@@*V zYat2l(*^EUB~!at4-ju1gZbJs_f6Y4UmNzx0(lW3+{_Mtajg;Xcsi+jDY(K&dJ)L- zCG3@4sATEy8S;LQ-=yVbj}O&QsBOYjgnhpNIh1q&I~r#Jysq@;zK7?nS*B~an>C4V zpo^x>*vnjA<-BTCQo369Z;K!8tNAFhaGdCn&5`7yUlGpJT&>q_L3Ku^_WS=Ua=y%qW^Oj!iDAn-EvCq_7 znx}wgwJYROcVi|R>7RTHl8@zr75Dd<+9r$VLJ9VoMckvcv%A*Ui_3WApJ1PnD4TV> zaI!giBb)Ymd+DJO(%t>os+Fh;sHO;{#L$xRs5EJq2~7Szrz-v@yC6=E@*%kj(8G_I zLqp;U_xVExlDV^sX=27^PbajMn&4?et>{V&o~{v1>Ar*a@bl`#q;7A#^zSaG&gU|>RibdV$D~sFzN@&okP&KB zYxkt>nV#J_4mST`kzJy~0jjd&qG4i9m^PgzAj@O`daM*-X;gz80n(!-M|uP4MG-VNv8e>cPJ#BxUl|60{r zravFW$ibz0ehY}~u#J7EJ@?;?y5?+pubcN|W3co=meOq%tIuxQ5S*CoT)dXKu@t=_ zD{Tq%`7$vxHi684){){dFvot*X5M>dD}GVq;u2K60f?cJ4>=xMbCzv{*M5{ zpQAaA;b)F3124IJZf$e}*Feizpb)Rk&f1{5EZ>9l`w}Dsrl%co5iNZyb?CNj|5gJP zapd#{B|C+zs2LknWs+zI zNi&RR9NfUaXNex6r5EfOGjr}&YuqQI7y6oor-GF~PKxTlKH`5llx8oE)@jHQ=6JzJCM|%^W%7di;>a^>r}Jp3-RUGyr?W}e`Xr^oeFd{fp(Xz)Vs|>x zzR8$Yw$ySOv@Y%WJT}5Q(Z`cP!7xQsO>}33(OL(UJ$?S=lg083^G?X|d!LO+!<8o0 zi?3uN$w_9Co&J(#7kUh*svpnje%`93P;&k8MdYI3Kso-cOa7J4515OnI+#-t32uC* z8hguyIqtPNqO%6VtuQZjqH{C`O@#L#6N`=1iS-965ZmMx0+z)TAByv7J|&7vzd}#a zbQ)$WkSz0A2Rv@GO-0Y%bzGewz5~)6rSJa9#WrjpLXLhk;b&KX1jOa1Q*p)j)xw~^ z;OZOIO_Scwz9B-UP|G0G{FMUk8C!>`gu{x*>EKJKPrUiTqQ0T7zZrCCzUc_L9x2#~ z9paLzgT&f4NWzSnyN!)p3YT@{sPR{2=)2Hy7 zmgO~D&S-B4D+|{;nail2uX^H+SDq~wHMXw4!l$m13t>_LsIU5h@{Z<isql6XPJGm7D@+~M2Raa0<#;#cCnu5X zplzMD!;RY&JWVcWgRIFPhDWzY)@x(I%jb99S22z=s<<7I9&P3Nak9ZbJKkMNo#Z2h z!)7~9@2i>XI}s%gU0U%*_aJ4;^z;qlCE!*&gXfkPA_et+>RV+431yZ$gT6s27W+Uc z2qV;Yu5oMCk8mhAJO!75RN}2pSv7H;O4Kiog4JI;QdfnwwA`j zYs#2yNi$4PcyOJe=AvWl#c8*rv+jGVs@iIqz(c22JG(Aevu55;b#mm$Rb{&8YdXuU z^7InnMWQ*nqipe`Zf&04m-&Frb1!towbv=FbZuQDyen|>L88Lh+c#N8=cfbPoH&Rv%A%3LKjhXD3-nLKLKg8fu+-c4rfPok=` zhQjil(+x=QYkfcs!>WcmSWI~vh@}7!RK^C1H+$Z zD}wwt1_z?)8wLh?v^o!-zU99(ojzX;%Xc}+Zyp5G!5X*$>ydtUG&hqifZo2m*88Xz z=d9|j1?VE36$q57w<0CKq-X%$<46^&4~UyX-UD_U;Z$&ZBMEVGFkgr9n%>5q2F%?= zs=QKlG-s-;8(NC&zNj6QbNZ;ID)}d!Ozy$7I~J9%kK?$k7v^t$ocZ&ki`UmQZLRYG zY7#T!s}MW@B{E*6nnBl1pp`FlcIb4injmh*Cy28#RM6bXjBa4n?=I<<=j&MJ>Jnf) z`9b{{fF!7=p|dQ<+n?lV1P`o$dn!@gQpV@-^VVGZzSjyTVuxzSJ1}@RQ0ZB&@7xf) zY^Z18_Wa}7gq##3?f^;3%(!vUJ0W=OE3Ru5`|sbn#7J9DuLU=vAg=iW7BB#SAXXv+ zy~PE*T9sJiq&BD2+BBAAZ4%PKW8N+-`!Hxa|8ShavF>zco}HQ%se^x zd1^9`Y{U^Du8b>P5yk4L^Zx5Fy^rRPIks~-UztIa6lrNI(TG*4N3J|a3b`7df7~a4 zQoDH9_@{;2rZR^Hy*aS?#gf2lEJ&-a0yjCFX%Eh#7PQ1x!$SZzC36laV<8wzOsBYB z-GtZ*zZ7=|@Dl`E>RbvgO`jG6>J1;xwH`gqE7o4oaO&7kWgy0*(&&?ucV1?8yLz8TA>`o zgv;zy318mya46qVZq6EV9FYjpA77>BOrM78AoZHqe>e0_s(JC9Xbw=wnxb9(mdTRT zACaF1?VLVtqDw(pC^FC{}21-!)h$Qv6{V6yY0|LPDSU6{A2`N!yZfEgW!2d8lXL=coX zaRI7w=ucBf7vEcRe5YNmHBfAf|K~^;*79sKEH^5yM^xk#DdBg+z!_Va(iyq)b9an` zX(JMvUW~0_8X_!Fj=m@0upM`aIzP)H7#F|+g9Wpf;e=@EBnaZ|Wa~CzP5n$zW(L6h zCy2-0I71uAKjp~jedR>gx~H1-pd~fVsd)wTe|qRPupZjqjf*haES?Yyfze{emL}_o;4g9w3Rq5JH{ zENwnnvbD>jpO@asQe1NIo{SH4=@MOE52gQgveDgKhV)IW#9?cPg!}akR)bA0Y1j3O zC$Go-_&goVo1u8$d9#sH2t=XFAjiYY4K8r>tT-PHKe z474wQ9Jj8X@;;782=aKgFz)>^-eGe_e?I>YZt(@fEtpF98{w;Jn3ho1gWf`xj-j(p zO|W2QSfpt4SDbf3&@XEt_WJk3fq92%70xMM@#zSH!xrUFAKt3PwTjz=54}woC#!u& zj8dW$#(~mc5ihdkV~``Jsil3W>Hm5LPSiR3~ z;@cBWUm$?xQKi<7rSd0v6*(%MK0W{xO)Vt(dYO~j<OH_2nb^n zCn)hZ4zvTVnsdR8dv?`2Kl(l(3964Z|E7M@2I19cP~BLNOUF?HpHo>nEwg+Um&L?$ zm;v!29@gMfM$IQMhh_Xw=j^Gmv|A|AaPUceN_-{S#P2YTE;HmlOKDJfk!Wx$N(q`I zfEm6Ic?xbVZX@MY?voWFN@H>X^S&(bec&t^zK8XLSsHBn0>)s$_MY4bHOgVpqGqNb z(ij#rngstKCQ|bnzaFU^%1Yt5ljGTa3Us&N4dgKL~Y3 zDyuX()wt6#xspHZh|kBe2c?`c_W~s8dkIrAy8DG$A^2B+EahNLsD94B|KCXjA9b9X zCykfmwst=|Teq*mvzlTk`1R3gJSn_0eE9uc3alCYVp83*coV?dh9Ck#A2~Yoz)yW` z`VXah>>tsG8hk+a2@@1Ibeh2nGAr4#Bq&pMh@5gMfPdr(84b?;N+72NvU^|^=2%7i z0uDiAVZ6+r*xRt$rIV87G@aEVcsC+fE07qH;rYN8t8DtKq4ni_XXv)v$ynB@!LDG0 z?p#kH@h7jEcf*;F6rO?u9Krf{}ep0 z0EzOX{D}%mw|G{$h*8RTyiSm?o~OQVH#e+%ker1moM7_31?07&ZB|)6?>OZG5W3|T zGsnCDn~#wML+mv7agsJdVE9eI7@;RDt?f+d9G7UMt}ZxOI`&<58-*(+eSvmZ%(`8W z>cGFl%!~>X6(@tDk%J$nj!(;htIReR+58bK_Z2zJWu8jJ$!AzmctIh3GgD4dHRo>C zlE5RieY1)c9V8BxNNH_mllUy@6V-&WjokWL4Cv%fNp)QGf}&pFSYroLBnoCEkyN#eVfpMS6H}NQz9O0&|3vPN$X`~ z{5y(xBCyNLNdlNuPFM+NxASU)dLjFuuQU!8kvzHory-FQ-visk;@`~u^5!BK2bDB` zg$#wmNP!=73Ndd~@~MUKNF1FgXZQ{+)G_uofAKhiro@)1>@LsZPw93V0oY!P z6^=Xy%lt?L|AR8>B0e?hD-%ZaX+A3t!sqMjHIe5XB>F>>r^K~N7b~+_yJ%u=vT6ik z^@+6D5@&FPtVmx0rYrT)3rD3KW()-4=|2x}xDndrmA` zXhxpv*l@BNCgFKRj+hV8nE(om1bFEnijz2Gn4?5LIm3kwAw1E9Xd9~r0#M6sJ8eu{ z=KvwvxOpb(ltv5$Fa8<%8m? zd)vBZoOT7zn#7FuL_L^r6E{!DuT6sAhz>BM0ZMZXu35W3hVNQJG+} zhNq2xE>hgbkvdZpNUeaJDybSV6AQGFmm;|T$*mw+ks>wXrHnuNnl`Czo*lo~{6ABM z8b6;}e~;`@xs1F!Go+hV*gtv+9ltIrDq4oeBul`WhjwJS6nM}WoG%rkDneocIph?o ze={;>OPT;3m(kOXqfj*h%EJvL5B%UL#pWsDe^9X4ea(O%A1s%jC$IM}oyfPcFQ+$a zv@w4EBbYoi{DKZpM?g-|&j1v#WLvDn3O{C4-e_C0$$4Pb(56<7{IXP-4wiGQe_JaK zS^=|jrwm08)G`ds0OpWh_$KCsb$wWYlM&g-RCEVk*K+W> z(moH*RP2qjV?7LsiUPCY-S0uq8G!9zy8={%zlDz>PE%W_U1)R6m7Dg1Ouvz&Xd&>_u)l9vyVdRclfv zdns1z5qu0W1}@b??dKnXFUua=Lu}Ol*?moqJ4Xd#5rjwa8%k1*3PK-*7TkeHfofV` z7U}UX4OfV1@P5AfUhmiMR;r|tQOryNAsHdBB@nQa6sEL~5mE20nFZ`#AF)4NQv3V; zv*`)M=HEbpg;(B0A^+MwODxS(t!UVt?pC%9C%905`X|KDi^FCg3cu32*L8)5 zNn2Tm%W-xKs^o@?g!)?Ow^^K|fWmZl*)_inl3Oc?ui9zRLvse$ z9JpH&eeK7Y(R*k?0@1}(keCqZ*hPv82S+7b`0Vv~CvgKn%ZmH2$YY;4AN4X7{xs`K z0N8=8YOXOB3E)>6mh$EhrfUtG*ie58E&wZjNy_Yt+-MD)9kVK>}>P%?@1Zm zZl%In&s1YUMHG`PFg@Dc7zYgiB8-@eKvNpJI-z8jhel;kYDvwETZ6Ss2tOTqlySLv z?bU(%C8@$-Z_-mExobCo1tSf9T&r77!gEePl`Nhm z$0Vqj*O64E@mUn)#K{USy_Lgw&IHGYlx%^*oNa3>lYs9}^DLF-^7q)`&1w{6eHOCj zyWljtM*_l9_B8Ub#IV%_$fbwUI^L|YzEJSQer0$enD)>obf)95(e!-BMv*VR)u5!L z3XPWem(qBKb@u(Fj4Xsq{UnlHz&R2uPl}0AuA_uh*>3rkwg1`J?An$9nmPch$0Gz~ z*gZ%vd?fP^ZI!0BB-P1p!G1PmcpnXU^doLn5Gj#xC5yP3H`qZY#1`Y z8A>HzH>9}YSX9!!Sc*x-saOR(AR~$$!#MOepW))xX;v)eba+2XTB>PBZO0@ma|+4g|JT$#hDR1|Z32#M+jhscZQJVD zs<`8%W81cEvt!$~C+B=K-!;E$*HypvTf5f%tf%}$3z=h83y&oNpU3o<^sO0fz5%*! z`N7QdgO{#Wa&K2?Bh$U6;j@|1;O$e8_w+J_PJVigKr_q{nF zLf9*G35g0nkL0$3@y?>;`6x=^rBT6W6qO$<_6F!G3G@lJtWdG8z~+NxBxTrax-{GX zhdsq)Q9oX^;peD()Sn`lN@6Tj2>|8a%TCcNtNn%I0!-E z?Q}5VZ>o5VCT=o=hlqZx@r0BEt6?SpJ*Z}OJCizd$J`=sixSAmPx z@{2?HlI6cV4#VamJ6feO`CrO%AVV#qUN$*JG&+Rk5=1Y=1JId|L3i+A(*OqeaSfz1 z2CuWJ$B_9ywM$kud|}I?r%m<=tJhaU2e5=WtAI?)PO7__ZGkJ0yTu} zyMBgP+c}WWGW!$gh^~=dg!111CR=xj7jg{PqtRDi)nEyRiQ;UN+bBy|GuI%b-7p24 zx27_6MGk&uxSP{*3ZUG8ha^_`XJE>K#rjR+@z(_{NKSou!|>z4CgiDX9r|@y2AcUZZ*CXNpG6$|VBpj~oNQUi#h7Pw?a_(o*8p zr;*Qfk0RHBr&az5)W~b(Tm#YCf6TJ)SN{r@i|U#;*V5l$J8@|d}FcYB@sp(CO za)i75MNrQqE>>d60~8D$p*0pv^mp}S9@HWDOr8&eA&Lbc`5>(!f4LH^Z|4~_y}YRG zP1N92e0r(OxNkiIs?s$!J3N!@J#K*NyH~Ez-G)kPwyMpxq0XAS9nbh&eG!sIJm1k# z^?lXI6%ltY3R}Jjrz@PWz--^^VUE-xE|(qnc@$>8PeLI^=&uuutlz}7#&k?91Oj%z z9Mo59=#Il}pDL1gCIx>2uDFs4MeEh&H(LR+A5}HaHlAVt_L(K-yCmRm?LN$mPn<0k zwXJl~MfYiX#F(+-{L>zh?F-eR4xYLPws75QkwGSdT(oMPuj3o30Fj&K&}_Ab*6+#u zKS35PPS)lpUf-GyRe;ehd6uP{&`<^^#21a$oXv%2?G>T7F@e8+4l`?IiU}geTsfaC zS+Q4bTQda!n(6B*9afE0b1P;^;W$pkEM4V`QETE1Q=#0YGI>U9JlE-Z#=Eb?9Oyy3 zp(@s|xvZj8^%wtcU?Z7z#ND83fxjX*2*U_v_Gjas7dGp?-m7d;odyrT>$kyr;nZ4z z^gyE&E+O3>(8l-=a&|k3=2ZGuoV!bQoC(|}yxZ~tl$=JrUBsDuF8Zw)svDnqRe+C! zl5fjXWKf&KszdSbB@cF-&$+h|k2}Oa88vUy>@cypVg`|*L9x>cvrhW5XHf=+m#Iw+ zvM;YXLK{4thF=iGJhvNl_iN1`m~q*M-4l5WqHoCyHS(DpMfQL@4U++D@UseVG)p-? zsE<{E|24H0yWQda{xo4@elN==SB>5xKl|r<*vg~+mmVxw4dz+k@;uM;jZAJ2D$Yq! zg@$ER^9B-JJcyl@t-5_4>3vIbQeRNrG?GtwqwIz)rt|(Kq8M9 zyv$mOIfp{SeJPt8XQsWf&43`p_t3nYTrPM3tb)Z-gVnD0b5U5cuwm8sl5nf6);jDL zTU!QbHo1YHSB?lD2u5Rrd;0*SfANXz zIayiN-16Ql?gpuJ#4$eH_Z|ZZ0_cB{bnL4XhgXcssNiHMu?FTKl4Ob>a$&yAIown& zXP)6x!spGjAkJ23AK#b0=dtUY_18Tl51+Lxf9SL_%gPZ(Oprj`ATBCo{-Bn{gUH zRiIUqaTAAdA|M6|j)uV195<$_QJjbygH3@|A6&ZXWu0U#%>g?_MuFn#Gu7Z;;$8tQTV5E3Z(~_-$F%X}xxZM^+|KDtU(1l(%VJ+LUOoys36J z$;{Pggej5Z0@rInrv1@gr{=y2`1jJ=fH4as^pt~RJRpf=$BYslLxoGHZItmmYIKd- zduMV|e@}DUXd1KI^t0z^g$x>lrOOgPLM95+MaHNQWstPao_Z?eiK0d&_i$;71allR2 z*Joew)d;R_WneTCazD`qX&D?aI=ttoWdvG^VS6!0!R{Jb0@tpqH*ctR)i*upwb5SO z`N1$SWlhE9JVUk4)4R-a9cwZI1hnQP-eV%G07YO(Wx;f~ri4o4w+jgE3RIt;In$r1 zgFi8uVy&>T5{F}Kc{Dhx$)T$aUjD~xcoq$qZ=4*&B*l^;Wj?z3SE#id5m56i-=Pr$ zFt8x007PyE^Jj7CaRM_5(Y_IUa-K}>Xvr_pBiK7>&g z_IF-8kva-s7AGJhq~8!%<6Zbjh@d6nfS?#4#i_e#XywafzFwJdBJzlzqBJ}QS%N^y zpzNU%5=Q{g_emGmgW11?(`Sr7BS`t`;TGrx^5?@a@splP*?mVM{5&~Z0f=f8p8{gg zTCavgkPI0H`qIn1Gd;yJf%^QqE_;KT_M}E#wRV*z zzqp^)`d{n8{vuVG@Nk$v`n>lZATFx!V`hkQpHi}iKU^#L7#Id2YNO`yue!{8!^=+n z9pbY{c>i4THeI~0*9&yP_vr$CHC#xnna5_<8Y)i*oLA3fjnII-VirYI!54ZO?cS1h zZ3dXcTA-_=`TyD{(8xT}4VmDwn+k}e(Wnt(u@5-WJKQKu++HHVh{CnziVP3kxYa7H z+vg>X$+K~(+xX{GeSi)}O=C^LRVC}hsBO#DmkIooyhvj4lZj+8O}}?|vAQ^N`JKUZ zbX;N#pbZ=8XPj4z>%@z7AgY%2RgCmzd{~`FH3tbc1~!$G*?6%pph_PXOsyI+q8_^6K%5mNX?2 z=_1K2{CZ?w$CU(l2ns4#DEJIV6A>=_1)3pIEN4*4y4HE zy0EC=7AZ;@Ah>{OdN4G7Ob1E|+&ksz)EHrvtv7|c5Y3*>x7B_d0fnL7GYYApahGH8 zbUhwU{=i?&6`{`QmK{0*!w%l0GqlyX>y1l@M8x^67NsowpUXP1N6($Qs&Ovg1)MrF zkZH%4%>RC^u4T2PsB^@cvw46&6+(>YT_WJ-S}c*XzP|!gkj;W8sJE4SdIa0pR(S&B z*dusKM4Q*x+%|((6?d`a7D{ANJY;S7A*+^GI=}tBFk~j zYcvenf)qcZvr)ciEZYKkH&UST>o8=HNYNJ$65mC%4Ia`*IYGCHO3mZh(t5Sx$Y&-# zyW;xeEZhO04x6*9zS(sht3ffa%WA5iIJ{Fe+b!*|Bwt%9%Pi>xe0SABV7cAWlpN3B1YN^ zJtN(%!5ud2DunCfNatbcrzB$g^~+=RJSOrK!a$PAP)7GTN#Wf^*y;4grQmc%0Dg@x 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 delta 149903 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 bdAQgC)UKaZQItwwoWjy zZQHiZiIWK?#>Ac&U!Ldv*1GSy_x|bA-RE@wI6rFDuG+P$o)cNLd*nqA#X@J{*UvuV z$;3VU=ow|0yr<2P5j3*nrY$a0kHqZ4=yPY{;Rv~2@-Wvh=$4YdBa>)wZNukL-#151 zTr|9Katt05ecW9oyw2Z4ptJ|I0n!wjne-w{s$7U4;GbUCJS3n`$!wp%ok0D2?I&M8 zVgoVGe1x5nbO)gie_AS!AWWTmm*wXNoX1Q%c|%F7{bIPC?AF`AP1G&NsCMCaIWoTa z!UUYaTi_PHYxvFQX`q85(g-@5{KGrTfS#VyC9+7Y|i} zk-+OM_6U82nf#Il;s3-*QWzb^$3oHv zSlvD*_i=V^W=yY6pl|=OKph&bD-QE+C%6$t5M#G zr$Q?!AbW~&7W{RO`evw|K$-0oQNcfNdc_~36!2_0G_c?WFHaJ%#}wE9p>G3*N7WL8Fff2#FjMN zIUN(FF30t4*?*HM1hD;b)3&OZw9RIQ-wsp1=W^zIRm3d-y-} zTBV9l4`2OX-pvR>LNU;SH(VoLQGJ6U%_8&>ZfubzK|Vow{AUhAnh>CDXo!C;kX?h? z!_GoM$|_Eul$K-Eyp>xmK+e0%2~oX&_gzG+ihV9TE;9P#dBDqWk}dQTEAVmfbe60( z$*m)&T9hWDbJJl@%7bssqr6OSCSN))7lvDq?+82_cs#1RDhD9mq7N_dI$$N@gpJ32 za8KYo`W!LsCp=D9wV4rOulNzK+xJLmmqu41;uPZF){ti?)H4E^6z$Ltn5F|Z(P#rj zqGsoUx=LUtWaSnwX;TWGf=+&hw+IX&^F8=O{W|hQ_^Mx2+J?fXb!_aYlgh(0)*O%J zcRLpW`2F{apksTTh;=w~GNjt_#7UDp)xY(9KJ6|l;^*Iz*S9uN1W7vqTIo^?U0kyZ z+*emi;yjBR=9`db^szmC^RXeFN!Ir6;fuARFIG;h3QrAzl0y}QBh-D|u;C-PfcejR z?-1R&^N);ldXpt}4QJ9U3iXAhQC5oqRkvxNW7Bn|Btp<4u=D9eMKZe?nB>a%R7+Tx zi!H>Uqw}W#!!0xS^)5O)(-K@++%hXR*|m#RC3^M_Hc+7C zb$=r;Lo>;N!giZf&g(2IZX}ZJGn);Z(6p@z-XV!-(1efZi)oqV{bvk4EWIj&%y3L>WW2UWmbu|OR_TD-( zL!QfSJgRNjA1Jf6$2lZCmfzct?jyA(p+D7wJoh)HLh&6{ua1s9?5g>pH>1au3af%`*c@A^=NQzgQ0OW=irL?&jtwj<8MWg#E}=_mx6BN3Wr94S*+ zyworGMjwjq0jP$%W=i_%zpnvVpD2qeZ!=MRza&30?xW71!v=|)TP$@#c+!#&34Ue{ z=>*#g1E?Z+9#7fBiQj!F+#S{_wEPOa^!Xd$1HNtK=?<4! z)AjekAU_0KvC~^zfHhDdbPo(n#2zvk_t@~z{O&n^x3@we}e8z4%u z-cv^r#YiP;)xvEMlBxzEj&&K;evOQThNVX>%Br@5P)55-T1CPC7)EK;VsvoO5FCpw zFtQoSGTAd`;DI&9tk!x)*ViLknJm5<>IxS}OKv|YSdo-g9(8dugz+?OFc%~GEbMX^ z(^#9Nr)}lWj6|fK#JanPCoLUo7kS>BnuaxNa3IOLwt0_8AEW}rJ5?>k11fWG8(MRQ zqc~_&R<_Euhr*C!yv_?6Ft;rZqd31BlwWY%(78G=q8RyxDLvg|RsO{iz-U67cNb3! zP4WppW5Y=b-G*OL>>+6MD>M0Kjsu0Nwp*if&sR8c`kf9bah`%KZtwSns0MBsUB#e} zb*N6Zk#Ob4>7K@k^0ZfoVgrPyO{#-mAG@6ombqp>|@bx(Ein_j=Y4k3M8 zXvGzC)^xfxZDeW>vmBS!U_5W z;Ja|c^n?_CL=5UDaCRNQ@9KNKIq-_yXKMM%71X!Z#o0Sc)a%7{1j-a={s#gOcE

    fqW4 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 dnV>;F8_ks0;`?No2@ql^g@D)QUb`Pa{{uMI diff --git a/test-models/src/main/pegasus/com/datahub/test/TestEntityInfo.pdl b/test-models/src/main/pegasus/com/datahub/test/TestEntityInfo.pdl index 3b8aa4f39f7b7..d1daa7b8d4593 100644 --- a/test-models/src/main/pegasus/com/datahub/test/TestEntityInfo.pdl +++ b/test-models/src/main/pegasus/com/datahub/test/TestEntityInfo.pdl @@ -97,4 +97,10 @@ record TestEntityInfo includes CustomProperties { "fieldType": "DOUBLE" } doubleField: optional double + + @Searchable = { + "fieldName": "removed", + "fieldType": "BOOLEAN" + } + removed: optional boolean } From 08bdfbdf93da589053f63f412ced820d6d575fb7 Mon Sep 17 00:00:00 2001 From: John Joyce Date: Thu, 25 Jan 2024 15:25:31 -0800 Subject: [PATCH 12/19] feat(ui): Supporting rendering custom assertion descriptions (#9722) Co-authored-by: John Joyce --- .../types/assertion/AssertionMapper.java | 1 + .../src/main/resources/entity.graphql | 5 +++++ .../tabs/Dataset/Validations/Assertions.tsx | 6 +++++- .../Validations/DatasetAssertionDescription.tsx | 17 ++++++++++------- .../Validations/DatasetAssertionsList.tsx | 14 +++++++------- datahub-web-react/src/graphql/assertion.graphql | 1 + 6 files changed, 29 insertions(+), 15 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/assertion/AssertionMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/assertion/AssertionMapper.java index 2536f4d2521ee..43b7b5bb102ad 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/assertion/AssertionMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/assertion/AssertionMapper.java @@ -66,6 +66,7 @@ private static com.linkedin.datahub.graphql.generated.AssertionInfo mapAssertion mapDatasetAssertionInfo(gmsAssertionInfo.getDatasetAssertion()); assertionInfo.setDatasetAssertion(datasetAssertion); } + assertionInfo.setDescription(gmsAssertionInfo.getDescription()); return assertionInfo; } diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 2ad4982579380..3ea1b38d3db0d 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -6803,6 +6803,11 @@ type AssertionInfo { Dataset-specific assertion information """ datasetAssertion: DatasetAssertionInfo + + """ + An optional human-readable description of the assertion + """ + description: String } """ diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx index 68660164ee877..b3086d7867012 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx @@ -35,6 +35,8 @@ const getAssertionsStatusSummary = (assertions: Array) => { /** * Component used for rendering the Validations Tab on the Dataset Page. + * + * TODO: Note that only the legacy DATASET assertions are supported for viewing as of today. */ export const Assertions = () => { const { urn, entityData } = useEntityData(); @@ -47,7 +49,9 @@ export const Assertions = () => { const assertions = (combinedData && combinedData.dataset?.assertions?.assertions?.map((assertion) => assertion as Assertion)) || []; - const filteredAssertions = assertions.filter((assertion) => !removedUrns.includes(assertion.urn)); + const filteredAssertions = assertions.filter( + (assertion) => !removedUrns.includes(assertion.urn) && !!assertion.info?.datasetAssertion, + ); // Pre-sort the list of assertions based on which has been most recently executed. assertions.sort(sortAssertions); diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionDescription.tsx index a91d11d1e9887..daebfd5597588 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionDescription.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionDescription.tsx @@ -19,6 +19,7 @@ const ViewLogicButton = styled(Button)` `; type Props = { + description?: string; assertionInfo: DatasetAssertionInfo; }; @@ -319,18 +320,20 @@ const TOOLTIP_MAX_WIDTH = 440; * * For example, Column 'X' values are in [1, 2, 3] */ -export const DatasetAssertionDescription = ({ assertionInfo }: Props) => { +export const DatasetAssertionDescription = ({ description, assertionInfo }: Props) => { const { scope, aggregation, fields, operator, parameters, nativeType, nativeParameters, logic } = assertionInfo; const [isLogicVisible, setIsLogicVisible] = useState(false); /** * Build a description component from a) input (aggregation, inputs) b) the operator text */ - const description = ( + const descriptionFragment = ( <> - - {getAggregationText(scope, aggregation, fields)}{' '} - {getOperatorText(operator, parameters || undefined, nativeType || undefined)} - + {description || ( + + {getAggregationText(scope, aggregation, fields)}{' '} + {getOperatorText(operator, parameters || undefined, nativeType || undefined)} + + )} ); @@ -349,7 +352,7 @@ export const DatasetAssertionDescription = ({ assertionInfo }: Props) => { } > -

    {description}
    +
    {descriptionFragment}
    {logic && (
    setIsLogicVisible(true)} type="link"> diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx index 05fc2d1c496db..3eccfb8931fc0 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx @@ -83,6 +83,7 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => { type: assertion.info?.type, platform: assertion.platform, datasetAssertionInfo: assertion.info?.datasetAssertion, + description: assertion.info?.description, lastExecTime: assertion.runEvents?.runEvents?.length && assertion.runEvents.runEvents[0].timestampMillis, lastExecResult: assertion.runEvents?.runEvents?.length && @@ -101,6 +102,7 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => { const resultColor = (record.lastExecResult && getResultColor(record.lastExecResult)) || 'default'; const resultText = (record.lastExecResult && getResultText(record.lastExecResult)) || 'No Evaluations'; const resultIcon = (record.lastExecResult && getResultIcon(record.lastExecResult)) || ; + const { description } = record; return (
    @@ -111,7 +113,10 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => {
    - +
    ); }, @@ -146,12 +151,7 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => { - - } - trigger={['click']} - > + } trigger={['click']}> diff --git a/datahub-web-react/src/graphql/assertion.graphql b/datahub-web-react/src/graphql/assertion.graphql index d4015fcebdb3e..0b64c4c8d6ddd 100644 --- a/datahub-web-react/src/graphql/assertion.graphql +++ b/datahub-web-react/src/graphql/assertion.graphql @@ -46,6 +46,7 @@ fragment assertionDetails on Assertion { } logic } + description } } From 69ff9c3af3da11deb6f915f11820b2489caac6e0 Mon Sep 17 00:00:00 2001 From: John Joyce Date: Thu, 25 Jan 2024 16:51:40 -0800 Subject: [PATCH 13/19] infra(ui): Add a react context provider allowing sub-components to update theme conf (#9674) Co-authored-by: John Joyce --- datahub-web-react/src/App.tsx | 34 +++++-------------- datahub-web-react/src/CustomThemeProvider.tsx | 32 +++++++++++++++++ datahub-web-react/src/customThemeContext.tsx | 10 ++++++ 3 files changed, 50 insertions(+), 26 deletions(-) create mode 100644 datahub-web-react/src/CustomThemeProvider.tsx create mode 100644 datahub-web-react/src/customThemeContext.tsx diff --git a/datahub-web-react/src/App.tsx b/datahub-web-react/src/App.tsx index 79c9ee91ceaa1..e8910e7dc2ea8 100644 --- a/datahub-web-react/src/App.tsx +++ b/datahub-web-react/src/App.tsx @@ -1,20 +1,19 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import Cookies from 'js-cookie'; import { message } from 'antd'; import { BrowserRouter as Router } from 'react-router-dom'; import { ApolloClient, ApolloProvider, createHttpLink, InMemoryCache, ServerError } from '@apollo/client'; import { onError } from '@apollo/client/link/error'; -import { ThemeProvider } from 'styled-components'; import { Helmet, HelmetProvider } from 'react-helmet-async'; import './App.less'; import { Routes } from './app/Routes'; -import { Theme } from './conf/theme/types'; -import defaultThemeConfig from './conf/theme/theme_light.config.json'; import { PageRoutes } from './conf/Global'; import { isLoggedInVar } from './app/auth/checkAuthStatus'; import { GlobalCfg } from './conf'; import possibleTypesResult from './possibleTypes.generated'; import { ErrorCodes } from './app/shared/constants'; +import CustomThemeProvider from './CustomThemeProvider'; +import { useCustomTheme } from './customThemeContext'; /* Construct Apollo Client @@ -71,33 +70,16 @@ const client = new ApolloClient({ }); export const InnerApp: React.VFC = () => { - const [dynamicThemeConfig, setDynamicThemeConfig] = useState(defaultThemeConfig); - - useEffect(() => { - if (import.meta.env.DEV) { - import(/* @vite-ignore */ `./conf/theme/${import.meta.env.REACT_APP_THEME_CONFIG}`).then((theme) => { - setDynamicThemeConfig(theme); - }); - } else { - // Send a request to the server to get the theme config. - fetch(`/assets/conf/theme/${import.meta.env.REACT_APP_THEME_CONFIG}`) - .then((response) => response.json()) - .then((theme) => { - setDynamicThemeConfig(theme); - }); - } - }, []); - return ( - - {dynamicThemeConfig.content.title} - - + + + {useCustomTheme().theme?.content.title} + - + ); }; diff --git a/datahub-web-react/src/CustomThemeProvider.tsx b/datahub-web-react/src/CustomThemeProvider.tsx new file mode 100644 index 0000000000000..f2e2678a90d8c --- /dev/null +++ b/datahub-web-react/src/CustomThemeProvider.tsx @@ -0,0 +1,32 @@ +import React, { useEffect, useState } from 'react'; +import { ThemeProvider } from 'styled-components'; +import { Theme } from './conf/theme/types'; +import defaultThemeConfig from './conf/theme/theme_light.config.json'; +import { CustomThemeContext } from './customThemeContext'; + +const CustomThemeProvider = ({ children }: { children: React.ReactNode }) => { + const [currentTheme, setTheme] = useState(defaultThemeConfig); + + useEffect(() => { + if (import.meta.env.DEV) { + import(/* @vite-ignore */ `./conf/theme/${import.meta.env.REACT_APP_THEME_CONFIG}`).then((theme) => { + setTheme(theme); + }); + } else { + // Send a request to the server to get the theme config. + fetch(`/assets/conf/theme/${import.meta.env.REACT_APP_THEME_CONFIG}`) + .then((response) => response.json()) + .then((theme) => { + setTheme(theme); + }); + } + }, []); + + return ( + + {children} + + ); +}; + +export default CustomThemeProvider; diff --git a/datahub-web-react/src/customThemeContext.tsx b/datahub-web-react/src/customThemeContext.tsx new file mode 100644 index 0000000000000..0b273d0024885 --- /dev/null +++ b/datahub-web-react/src/customThemeContext.tsx @@ -0,0 +1,10 @@ +import React, { useContext } from 'react'; + +export const CustomThemeContext = React.createContext<{ + theme: any; + updateTheme: (theme: any) => void; +}>({ theme: undefined, updateTheme: (_) => null }); + +export function useCustomTheme() { + return useContext(CustomThemeContext); +} From f7f0b14f376cad8aa3951efd305fcd15a1f01966 Mon Sep 17 00:00:00 2001 From: tom Date: Fri, 26 Jan 2024 02:51:41 +0100 Subject: [PATCH 14/19] fix(ingestion/metabase): Fetch Dashboards through Collections (#9631) Co-authored-by: Harshal Sheth --- metadata-ingestion/developing.md | 2 +- .../docs/sources/metabase/metabase.md | 2 +- .../src/datahub/ingestion/source/metabase.py | 47 +- .../metabase/metabase_mces_golden.json | 61 +- .../metabase/setup/collection_dashboards.json | 1 + .../metabase/setup/collections.json | 1 + .../integration/metabase/setup/dashboard.json | 40 - .../metabase/setup/dashboard_1.json | 1084 ++++++++++++----- .../integration/metabase/test_metabase.py | 8 +- 9 files changed, 901 insertions(+), 345 deletions(-) create mode 100644 metadata-ingestion/tests/integration/metabase/setup/collection_dashboards.json create mode 100644 metadata-ingestion/tests/integration/metabase/setup/collections.json delete mode 100644 metadata-ingestion/tests/integration/metabase/setup/dashboard.json diff --git a/metadata-ingestion/developing.md b/metadata-ingestion/developing.md index d1eef21974f1d..fc3a689124b2c 100644 --- a/metadata-ingestion/developing.md +++ b/metadata-ingestion/developing.md @@ -10,7 +10,7 @@ Also take a look at the guide to [adding a source](./adding-source.md). ### Requirements 1. Python 3.7+ must be installed in your host environment. -2. Java8 (gradle won't work with newer versions) +2. Java 17 (gradle won't work with newer or older versions) 4. On Debian/Ubuntu: `sudo apt install python3-dev python3-venv` 5. On Fedora (if using LDAP source integration): `sudo yum install openldap-devel` diff --git a/metadata-ingestion/docs/sources/metabase/metabase.md b/metadata-ingestion/docs/sources/metabase/metabase.md index a76786f7e5853..68422b8decce9 100644 --- a/metadata-ingestion/docs/sources/metabase/metabase.md +++ b/metadata-ingestion/docs/sources/metabase/metabase.md @@ -19,4 +19,4 @@ The key in this map must be string, not integer although Metabase API provides If `database_id_to_instance_map` is not specified, `platform_instance_map` is used for platform instance mapping. If none of the above are specified, platform instance is not used when constructing `urn` when searching for dataset relations. ## Compatibility -Metabase version [v0.41.2](https://www.metabase.com/start/oss/) +Metabase version [v0.48.3](https://www.metabase.com/start/oss/) diff --git a/metadata-ingestion/src/datahub/ingestion/source/metabase.py b/metadata-ingestion/src/datahub/ingestion/source/metabase.py index af41a74f311f6..d22bfb2b8b52f 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/metabase.py +++ b/metadata-ingestion/src/datahub/ingestion/source/metabase.py @@ -90,10 +90,17 @@ class MetabaseSource(Source): """ This plugin extracts Charts, dashboards, and associated metadata. This plugin is in beta and has only been tested on PostgreSQL and H2 database. - ### Dashboard - [/api/dashboard](https://www.metabase.com/docs/latest/api-documentation.html#dashboard) endpoint is used to - retrieve the following dashboard information. + ### Collection + + [/api/collection](https://www.metabase.com/docs/latest/api/collection) endpoint is used to + retrieve the available collections. + + [/api/collection//items?models=dashboard](https://www.metabase.com/docs/latest/api/collection#get-apicollectioniditems) endpoint is used to retrieve a given collection and list their dashboards. + + ### Dashboard + + [/api/dashboard/](https://www.metabase.com/docs/latest/api/dashboard) endpoint is used to retrieve a given Dashboard and grab its information. - Title and description - Last edited by @@ -187,19 +194,29 @@ def close(self) -> None: def emit_dashboard_mces(self) -> Iterable[MetadataWorkUnit]: try: - dashboard_response = self.session.get( - f"{self.config.connect_uri}/api/dashboard" + collections_response = self.session.get( + f"{self.config.connect_uri}/api/collection/" ) - dashboard_response.raise_for_status() - dashboards = dashboard_response.json() + collections_response.raise_for_status() + collections = collections_response.json() - for dashboard_info in dashboards: - dashboard_snapshot = self.construct_dashboard_from_api_data( - dashboard_info + for collection in collections: + collection_dashboards_response = self.session.get( + f"{self.config.connect_uri}/api/collection/{collection['id']}/items?models=dashboard" ) - if dashboard_snapshot is not None: - mce = MetadataChangeEvent(proposedSnapshot=dashboard_snapshot) - yield MetadataWorkUnit(id=dashboard_snapshot.urn, mce=mce) + collection_dashboards_response.raise_for_status() + collection_dashboards = collection_dashboards_response.json() + + if not collection_dashboards.get("data"): + continue + + for dashboard_info in collection_dashboards.get("data"): + dashboard_snapshot = self.construct_dashboard_from_api_data( + dashboard_info + ) + if dashboard_snapshot is not None: + mce = MetadataChangeEvent(proposedSnapshot=dashboard_snapshot) + yield MetadataWorkUnit(id=dashboard_snapshot.urn, mce=mce) except HTTPError as http_error: self.report.report_failure( @@ -254,10 +271,10 @@ def construct_dashboard_from_api_data( ) chart_urns = [] - cards_data = dashboard_details.get("ordered_cards", "{}") + cards_data = dashboard_details.get("dashcards", {}) for card_info in cards_data: chart_urn = builder.make_chart_urn( - self.platform, card_info.get("card_id", "") + self.platform, card_info.get("card").get("id", "") ) chart_urns.append(chart_urn) diff --git a/metadata-ingestion/tests/integration/metabase/metabase_mces_golden.json b/metadata-ingestion/tests/integration/metabase/metabase_mces_golden.json index 9b143348fdf60..10c1c312a4d1c 100644 --- a/metadata-ingestion/tests/integration/metabase/metabase_mces_golden.json +++ b/metadata-ingestion/tests/integration/metabase/metabase_mces_golden.json @@ -191,20 +191,73 @@ "description": "", "charts": [ "urn:li:chart:(metabase,1)", - "urn:li:chart:(metabase,2)" + "urn:li:chart:(metabase,2)", + "urn:li:chart:(metabase,3)" ], "datasets": [], "lastModified": { "created": { - "time": 1639417721742, + "time": 1705398694904, "actor": "urn:li:corpuser:admin@metabase.com" }, "lastModified": { - "time": 1639417721742, + "time": 1705398694904, "actor": "urn:li:corpuser:admin@metabase.com" } }, - "dashboardUrl": "http://localhost:3000/dashboard/1" + "dashboardUrl": "http://localhost:3000/dashboard/10" + } + }, + { + "com.linkedin.pegasus2avro.common.Ownership": { + "owners": [ + { + "owner": "urn:li:corpuser:admin@metabase.com", + "type": "DATAOWNER" + } + ], + "lastModified": { + "time": 0, + "actor": "urn:li:corpuser:unknown" + } + } + } + ] + } + }, + "systemMetadata": { + "lastObserved": 1636614000000, + "runId": "metabase-test", + "lastRunId": "no-run-id-provided" + } +}, +{ + "proposedSnapshot": { + "com.linkedin.pegasus2avro.metadata.snapshot.DashboardSnapshot": { + "urn": "urn:li:dashboard:(metabase,1)", + "aspects": [ + { + "com.linkedin.pegasus2avro.dashboard.DashboardInfo": { + "customProperties": {}, + "title": "Dashboard 1", + "description": "", + "charts": [ + "urn:li:chart:(metabase,1)", + "urn:li:chart:(metabase,2)", + "urn:li:chart:(metabase,3)" + ], + "datasets": [], + "lastModified": { + "created": { + "time": 1705398694904, + "actor": "urn:li:corpuser:admin@metabase.com" + }, + "lastModified": { + "time": 1705398694904, + "actor": "urn:li:corpuser:admin@metabase.com" + } + }, + "dashboardUrl": "http://localhost:3000/dashboard/10" } }, { diff --git a/metadata-ingestion/tests/integration/metabase/setup/collection_dashboards.json b/metadata-ingestion/tests/integration/metabase/setup/collection_dashboards.json new file mode 100644 index 0000000000000..b602d2dfb7dcd --- /dev/null +++ b/metadata-ingestion/tests/integration/metabase/setup/collection_dashboards.json @@ -0,0 +1 @@ +{"total": 1, "data": [{"description": null, "collection_position": null, "database_id": null, "name": "This is a test", "id": 10, "entity_id": "Q4gEaOmoBkfQX3_gXiH9g", "last-edit-info": {"id": 14, "last_name": "Doe", "first_name": "John", "email": "john.doe@somewhere.com", "timestamp": "2024-01-12T14:55:38.43304Z"}, "model": "dashboard"}], "models": ["dashboard"], "limit": null, "offset": null} diff --git a/metadata-ingestion/tests/integration/metabase/setup/collections.json b/metadata-ingestion/tests/integration/metabase/setup/collections.json new file mode 100644 index 0000000000000..a8a98c4e6d62e --- /dev/null +++ b/metadata-ingestion/tests/integration/metabase/setup/collections.json @@ -0,0 +1 @@ +[{"authority_level": null, "can_write": true, "name": "Our analytics", "effective_ancestors": [], "effective_location": null, "parent_id": null, "id": "root", "is_personal": false}, {"authority_level": null, "description": null, "archived": false, "slug": "john_doe_personal_collection", "can_write": true, "name": "John Doe", "personal_owner_id": 14, "type": null, "id": 150, "entity_id": "kdLA_-CQy4F5lL15k8-TU", "location": "/", "namespace": null, "is_personal": true, "created_at": "2024-01-12T11:51:24.394309Z"}] diff --git a/metadata-ingestion/tests/integration/metabase/setup/dashboard.json b/metadata-ingestion/tests/integration/metabase/setup/dashboard.json deleted file mode 100644 index 095abf1bbdc6d..0000000000000 --- a/metadata-ingestion/tests/integration/metabase/setup/dashboard.json +++ /dev/null @@ -1,40 +0,0 @@ -[{ - "description": null, - "archived": false, - "collection_position": null, - "creator": { - "email": "admin@metabase.com", - "first_name": "FirstName", - "last_login": "2021-12-13T18:51:32.999", - "is_qbnewb": true, - "is_superuser": true, - "id": 1, - "last_name": "LastName", - "date_joined": "2021-12-13T07:34:21.806", - "common_name": "FirstName LastName" - }, - "enable_embedding": false, - "collection_id": null, - "show_in_getting_started": false, - "name": "Dashboard 1", - "caveats": null, - "creator_id": 1, - "updated_at": "2021-12-13T17:48:41.735", - "made_public_by_id": null, - "embedding_params": null, - "cache_ttl": null, - "id": 1, - "position": null, - "last-edit-info": { - "id": 1, - "email": "admin@metabase.com", - "first_name": "FirstName", - "last_name": "LastName", - "timestamp": "2021-12-13T17:48:41.742" - }, - "parameters": [], - "favorite": false, - "created_at": "2021-12-13T17:46:48.185", - "public_uuid": null, - "points_of_interest": null -}] \ No newline at end of file diff --git a/metadata-ingestion/tests/integration/metabase/setup/dashboard_1.json b/metadata-ingestion/tests/integration/metabase/setup/dashboard_1.json index 288087a67da6d..e968093c43850 100644 --- a/metadata-ingestion/tests/integration/metabase/setup/dashboard_1.json +++ b/metadata-ingestion/tests/integration/metabase/setup/dashboard_1.json @@ -2,332 +2,854 @@ "description": null, "archived": false, "collection_position": null, - "ordered_cards": [{ - "sizeX": 4, - "series": [], - "collection_authority_level": null, - "card": { - "description": null, - "archived": false, - "collection_position": null, - "table_id": null, - "result_metadata": [{ - "name": "customer_id", - "display_name": "customer_id", - "base_type": "type/Integer", - "effective_type": "type/Integer", - "field_ref": ["field", "customer_id", { - "base-type": "type/Integer" - }], - "semantic_type": null, - "fingerprint": { - "global": { - "distinct-count": 517, - "nil%": 0.0 + "dashcards": [ + { + "size_x": 12, + "dashboard_tab_id": null, + "series": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "description": null, + "archived": false, + "collection_position": null, + "table_id": null, + "result_metadata": [ + { + "display_name": "EVENT_DATE", + "field_ref": [ + "field", + "EVENT_DATE", + { + "base-type": "type/Date" + } + ], + "name": "EVENT_DATE", + "base_type": "type/Date", + "effective_type": "type/Date", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 28, + "nil%": 0 + }, + "type": { + "type/DateTime": { + "earliest": "2023-12-04T00:00:00Z", + "latest": "2024-01-15T00:00:00Z" + } + } + } }, - "type": { - "type/Number": { - "min": 1.0, - "q1": 127.95550051624855, - "q3": 457.48181481488376, - "max": 599.0, - "sd": 183.35453319901166, - "avg": 293.316 + { + "display_name": "AND_VIEWERS", + "field_ref": [ + "field", + "AND_VIEWERS", + { + "base-type": "type/Number" + } + ], + "name": "AND_VIEWERS", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 28, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 4720, + "q1": 5083.5, + "q3": 9003, + "max": 10560, + "sd": 2090.2420089751945, + "avg": 6688.214285714285 + } + } } - } - } - }, { - "name": "first_name", - "display_name": "first_name", - "base_type": "type/Text", - "effective_type": "type/Text", - "field_ref": ["field", "first_name", { - "base-type": "type/Text" - }], - "semantic_type": "type/Name", - "fingerprint": { - "global": { - "distinct-count": 509, - "nil%": 0.0 }, - "type": { - "type/Text": { - "percent-json": 0.0, - "percent-url": 0.0, - "percent-email": 0.0, - "percent-state": 0.0035, - "average-length": 5.629 + { + "display_name": "AND_REDACTED", + "field_ref": [ + "field", + "AND_REDACTED", + { + "base-type": "type/Number" + } + ], + "name": "AND_REDACTED", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 28, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 948, + "q1": 2019.5, + "q3": 2500.5, + "max": 3180, + "sd": 460.56365857271413, + "avg": 2251.0714285714284 + } + } } - } - } - }, { - "name": "last_name", - "display_name": "last_name", - "base_type": "type/Text", - "effective_type": "type/Text", - "field_ref": ["field", "last_name", { - "base-type": "type/Text" - }], - "semantic_type": "type/Name", - "fingerprint": { - "global": { - "distinct-count": 517, - "nil%": 0.0 }, - "type": { - "type/Text": { - "percent-json": 0.0, - "percent-url": 0.0, - "percent-email": 0.0, - "percent-state": 0.0015, - "average-length": 6.126 + { + "display_name": "AND_REDACTED", + "field_ref": [ + "field", + "AND_REDACTED", + { + "base-type": "type/Number" + } + ], + "name": "AND_REDACTED", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 28, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 3545, + "q1": 10909, + "q3": 13916, + "max": 18861, + "sd": 3132.780684756446, + "avg": 12122.32142857143 + } + } } - } - } - }, { - "name": "amount", - "display_name": "amount", - "base_type": "type/Decimal", - "effective_type": "type/Decimal", - "field_ref": ["field", "amount", { - "base-type": "type/Decimal" - }], - "semantic_type": null, - "fingerprint": { - "global": { - "distinct-count": 11, - "nil%": 0.0 }, - "type": { - "type/Number": { - "min": 0.99, - "q1": 2.399411317392306, - "q3": 5.52734176879965, - "max": 10.99, - "sd": 2.352151368009511, - "avg": 4.1405 + { + "display_name": "IOS_VIEWERS", + "field_ref": [ + "field", + "IOS_VIEWERS", + { + "base-type": "type/Number" + } + ], + "name": "IOS_VIEWERS", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 28, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 6477, + "q1": 7481.5, + "q3": 10428.5, + "max": 13182, + "sd": 1948.047456520796, + "avg": 9075.17857142857 + } + } } - } - } - }, { - "name": "payment_date", - "display_name": "payment_date", - "base_type": "type/DateTime", - "effective_type": "type/DateTime", - "field_ref": ["field", "payment_date", { - "base-type": "type/DateTime" - }], - "semantic_type": null, - "fingerprint": { - "global": { - "distinct-count": 1998, - "nil%": 0.0 }, - "type": { - "type/DateTime": { - "earliest": "2007-02-14T21:21:59.996577Z", - "latest": "2007-02-21T19:27:46.996577Z" + { + "display_name": "IOS_REDACTED", + "field_ref": [ + "field", + "IOS_REDACTED", + { + "base-type": "type/Number" + } + ], + "name": "IOS_REDACTED", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 28, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 1470, + "q1": 3020, + "q3": 3806, + "max": 4670, + "sd": 665.7415088559197, + "avg": 3415.8571428571427 + } + } } - } - } - }, { - "name": "rental_id", - "display_name": "rental_id", - "base_type": "type/Integer", - "effective_type": "type/Integer", - "field_ref": ["field", "rental_id", { - "base-type": "type/Integer" - }], - "semantic_type": null, - "fingerprint": { - "global": { - "distinct-count": 2000, - "nil%": 0.0 }, - "type": { - "type/Number": { - "min": 1158.0, - "q1": 1731.7967120913397, - "q3": 2871.359273326854, - "max": 4591.0, - "sd": 660.7468728104022, - "avg": 2303.4565 + { + "display_name": "IOS_REDACTED", + "field_ref": [ + "field", + "IOS_REDACTED", + { + "base-type": "type/Number" + } + ], + "name": "IOS_REDACTED", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 28, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 4872, + "q1": 15019.5, + "q3": 20457, + "max": 27466, + "sd": 4688.492913816769, + "avg": 17683.89285714286 + } + } + } + }, + { + "display_name": "IOS_REDACTED/IOS_VIEWERS", + "field_ref": [ + "field", + "IOS_REDACTED/IOS_VIEWERS", + { + "base-type": "type/Number" + } + ], + "name": "IOS_REDACTED/IOS_VIEWERS", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 28, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 0.662587, + "q1": 1.8403745, + "q3": 2.241517, + "max": 2.576166, + "sd": 0.4488826998266724, + "avg": 1.974007857142857 + } + } + } + }, + { + "display_name": "AND_REDACTED/AND_VIEWERS", + "field_ref": [ + "field", + "AND_REDACTED/AND_VIEWERS", + { + "base-type": "type/Number" + } + ], + "name": "AND_REDACTED/AND_VIEWERS", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 28, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 0.671656, + "q1": 1.3536655, + "q3": 2.5325145, + "max": 3.097553, + "sd": 0.6816847359625038, + "avg": 1.93937275 + } + } + } + }, + { + "display_name": "IOS_REDACTED/IOS_VIEWERS", + "field_ref": [ + "field", + "IOS_REDACTED/IOS_VIEWERS", + { + "base-type": "type/Number" + } + ], + "name": "IOS_REDACTED/IOS_VIEWERS", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 28, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 0.199918, + "q1": 0.34496099999999996, + "q3": 0.4352085, + "max": 0.47286, + "sd": 0.06928869477079941, + "avg": 0.3833206785714286 + } + } + } + }, + { + "display_name": "AND_REDACTED/AND_VIEWERS", + "field_ref": [ + "field", + "AND_REDACTED/AND_VIEWERS", + { + "base-type": "type/Number" + } + ], + "name": "AND_REDACTED/AND_VIEWERS", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 28, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 0.179613, + "q1": 0.245343, + "q3": 0.475772, + "max": 0.522253, + "sd": 0.11732033433182058, + "avg": 0.3620892142857142 + } + } } } - } - }], - "database_id": 2, - "enable_embedding": false, - "collection_id": null, - "query_type": "native", - "name": "Customer Payment", - "query_average_duration": 820, - "creator_id": 1, - "moderation_reviews": [], - "updated_at": "2021-12-13T17:48:40.478", - "made_public_by_id": null, - "embedding_params": null, - "cache_ttl": null, - "dataset_query": { - "type": "native", - "native": { - "query": "SELECT\n\tcustomer.customer_id,\n\tfirst_name,\n\tlast_name,\n\tamount,\n\tpayment_date,\n\trental_id\nFROM\n\tcustomer\nINNER JOIN payment \n ON payment.customer_id = customer.customer_id\nORDER BY payment_date", - "template-tags": {} + ], + "can_write": true, + "database_id": 3, + "enable_embedding": false, + "collection_id": 112, + "query_type": "native", + "name": "REDACTED iOS vs. Android", + "query_average_duration": 50982, + "creator_id": 42, + "moderation_reviews": [], + "updated_at": "2024-01-16T13:34:29.916717Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "type": "native", + "native": { + "query": "-- 1. Table with redacted search users Android\n-- 2. Table with redacted search users iOS \n-- 3. Redacted from Android redacted\n-- 4. redacted from iOS\n-- 5. Compare the numbers iOS vs. Android\n\n\n-- 1. Table with redacted search users Android (to include date, platform, auth_account_id)\n-- 2. Table with redacted search users iOS (to include date, platform, auth_account_id)\n-- 3. Redacted from Android redacted (to include date, platform, count of redacted)\n-- 4. Redacted from iOS redacted (to include date, plaform, count of redacted)\n-- 5. Compare the numbers iOS vs. Android\n\nwith AND_viewers as \n(\nselect event_date, platform, auth_account_id \nfrom TEAMS_PRD.REDACTED.MRT_CURR__MPARTICLE_SCREEN_VIEWS\nwhere screen_name='redacted_search'\nand event_date>'2023-12-01'\nand platform='Android'\nand dayofweekiso(event_date) NOT IN (6,7)\ngroup by event_date, platform, auth_account_id\norder by event_date desc\n), \niOS_viewers as \n(\nselect event_date, platform, auth_account_id \nfrom TEAMS_PRD.REDACTED.MRT_CURR__MPARTICLE_SCREEN_VIEWS\nwhere screen_name='redacted_search'\nand event_date>'2023-12-01'\nand platform='iOS'\nand dayofweekiso(event_date) NOT IN (6,7)\ngroup by event_date, platform, auth_account_id\norder by event_date desc\n), \nAND_redacted as\n(\nselect redacted_ts::date as redacted_date, platform, count(distinct at.auth_account_id) as AND_redacted, count(group_redacted_id) as AND_redacted\nfrom TEAMS_PRD.REDACTED.MRT_CURR__REDACTED_CUSTOMER at\njoin AND_viewers av on av.event_date=at.redacted_ts::date and av.auth_account_id=at.auth_account_id\nwhere instrument_type='REDACTED'\ngroup by 1,2\norder by 1 desc\n), \niOS_redacted as\n(\nselect redacted_ts::date as redacted_date, platform, count(distinct it.auth_account_id) as iOS_redacted, count(group_redacted_id) as iOS_redacted\nfrom TEAMS_PRD.REDACTED.MRT_CURR__REDACTED_CUSTOMER it\njoin iOS_viewers iv on iv.event_date=it.redacted_ts::date and iv.auth_account_id=it.auth_account_id\nwhere instrument_type='REDACTED'\ngroup by 1,2\norder by 1 desc\n)\nselect a.event_date, count(distinct a.auth_account_id) as AND_viewers, AND_redacted, AND_redacted, count(distinct i.auth_account_id) as iOS_viewers, iOS_redacted, iOS_redacted, iOS_redacted/iOS_viewers, AND_redacted/AND_viewers, iOS_redacted/iOS_viewers, AND_redacted/AND_viewers\nfrom AND_VIEWERS a\njoin AND_redacted at\non a.event_date=at.redacted_date\njoin ios_viewers i\non a.event_date=i.event_date\njoin ios_redacted it\non i.event_date=it.redacted_date\ngroup by 1, 3, 4, 6, 7\norder by 1 desc\n\n\n", + "template-tags": {} + }, + "database": 3 }, - "database": 2 - }, - "id": 1, - "display": "table", - "visualization_settings": { - "table.pivot_column": "amount", - "table.cell_column": "customer_id" + "id": 1, + "parameter_mappings": [], + "display": "line", + "entity_id": "DhQgvvtTEarZH8yQBlqES", + "collection_preview": true, + "visualization_settings": { + "graph.dimensions": [ + "EVENT_DATE" + ], + "series_settings": { + "IOS_REDACTED/IOS_VIEWERS": { + "axis": "right" + }, + "AND_REDACTED/AND_VIEWERS": { + "axis": "right" + } + }, + "graph.metrics": [ + "IOS_REDACTED/IOS_VIEWERS", + "AND_REDACTED/AND_VIEWERS", + "AND_VIEWERS", + "IOS_VIEWERS" + ] + }, + "metabase_version": "v0.48.3 (80d8323)", + "parameters": [], + "dataset": false, + "created_at": "2024-01-16T09:44:49.407327Z", + "public_uuid": null }, - "created_at": "2021-12-13T17:46:32.77", - "public_uuid": null + "updated_at": "2024-01-16T09:45:45.410379Z", + "col": 0, + "id": 12, + "parameter_mappings": [], + "card_id": 1, + "entity_id": "tA9M9vJlTHG0KxQnvknKW", + "visualization_settings": {}, + "size_y": 6, + "dashboard_id": 1, + "created_at": "2024-01-16T09:45:45.410379Z", + "row": 0 }, - "updated_at": "2021-12-13T17:48:41.68", - "col": 0, - "id": 1, - "parameter_mappings": [], - "card_id": 1, - "visualization_settings": {}, - "dashboard_id": 1, - "created_at": "2021-12-13T17:46:52.278", - "sizeY": 4, - "row": 0 - }, { - "sizeX": 4, - "series": [], - "collection_authority_level": null, - "card": { - "description": null, - "archived": false, - "collection_position": null, - "table_id": 21, - "result_metadata": [{ - "semantic_type": "type/Category", - "coercion_strategy": null, - "name": "rating", - "field_ref": ["field", 131, null], - "effective_type": "type/*", - "id": 131, - "display_name": "Rating", - "fingerprint": { - "global": { - "distinct-count": 5, - "nil%": 0.0 + { + "size_x": 12, + "dashboard_tab_id": null, + "series": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "description": null, + "archived": false, + "collection_position": null, + "table_id": null, + "result_metadata": [ + { + "display_name": "CALENDAR_DATE", + "field_ref": [ + "field", + "CALENDAR_DATE", + { + "base-type": "type/Date" + } + ], + "name": "CALENDAR_DATE", + "base_type": "type/Date", + "effective_type": "type/Date", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 30, + "nil%": 0 + }, + "type": { + "type/DateTime": { + "earliest": "2023-12-17T00:00:00Z", + "latest": "2024-01-15T00:00:00Z" + } + } + } + }, + { + "display_name": "REDACTED", + "field_ref": [ + "field", + "REDACTED", + { + "base-type": "type/Number" + } + ], + "name": "REDACTED", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 27, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 682175, + "q1": 738644, + "q3": 805974, + "max": 847312, + "sd": 46783.99996291344, + "avg": 775505.5666666667 + } + } + } }, - "type": { - "type/Text": { - "percent-json": 0.0, - "percent-url": 0.0, - "percent-email": 0.0, - "percent-state": 0.0, - "average-length": 2.926 + { + "display_name": "REDACTEDRS", + "field_ref": [ + "field", + "REDACTEDRS", + { + "base-type": "type/Number" + } + ], + "name": "REDACTEDRS", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 27, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 46173, + "q1": 47556.94427191, + "q3": 48890, + "max": 50769, + "sd": 1164.9989906758983, + "avg": 48354.8 + } + } + } + }, + { + "display_name": "REDACTED/REDACTEDRS", + "field_ref": [ + "field", + "REDACTED/REDACTEDRS", + { + "base-type": "type/Number" + } + ], + "name": "REDACTED/REDACTEDRS", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 27, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 14.706168, + "q1": 15.398378, + "q3": 16.920933, + "max": 17.289964, + "sd": 0.8020030995826715, + "avg": 16.033017833333336 + } + } } } + ], + "can_write": true, + "database_id": 3, + "enable_embedding": false, + "collection_id": 112, + "query_type": "native", + "name": "Redacted redacted per redacted user", + "query_average_duration": 20433, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2024-01-16T13:34:29.916788Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "type": "native", + "native": { + "query": "with dd as (\nselect distinct calendar_date as calendar_date from TEAMS_PRD.DATA_PLATFORM_MART.MRT__CALENDAR_DATES\nwhere calendar_date>'2022-01-01'\n), \nredacted as\n(\nselect dd.calendar_date, count(distinct auth_account_id) as redacted, max(redacted_ts), min(redacted_ts)\nfrom TEAMS_PRD.REDACTED.MRT_CURR__REDACTED_CUSTOMER t\njoin dd on redacted_ts::date BETWEEN dd.calendar_date-29 and dd.calendar_date\nwhere redacted_type='REGULAR'\nand instrument_type = 'REDACTED'\ngroup by dd.calendar_date\norder by dd.calendar_date desc\n),\nredacted as\n(\nselect dd.calendar_date, count(group_redacted_id) as redacted, max(redacted_ts), min(redacted_ts)\nfrom TEAMS_PRD.REDACTED.MRT_CURR__REDACTED_CUSTOMER t\njoin dd on redacted_ts::date BETWEEN dd.calendar_date-29 and dd.calendar_date\nwhere redacted_type='REGULAR'\nand instrument_type = 'REDACTED'\ngroup by dd.calendar_date\norder by dd.calendar_date desc\n)\nselect dd.calendar_date, redacted, redacted, redacted/redacted\nfrom dd\njoin redacted t on dd.calendar_date=t.calendar_date\njoin redacted tr on dd.calendar_date=tr.calendar_date\ngroup by dd.calendar_date, redacted, redacted, redacted/redacted\norder by dd.calendar_date desc \nlimit 30", + "template-tags": {} + }, + "database": 3 + }, + "id": 2, + "parameter_mappings": [], + "display": "line", + "entity_id": "b1jUcPcQM0XFMuviv4g3K", + "collection_preview": true, + "visualization_settings": { + "graph.dimensions": [ + "CALENDAR_DATE" + ], + "series_settings": { + "REDACTEDRS": { + "axis": "right" + } + }, + "graph.metrics": [ + "REDACTED/REDACTEDRS", + "REDACTEDRS" + ] }, - "base_type": "type/PostgresEnum" - }, { - "name": "count", - "display_name": "Count", - "base_type": "type/BigInteger", - "effective_type": "type/BigInteger", - "semantic_type": "type/Quantity", - "field_ref": ["aggregation", 0], - "fingerprint": { - "global": { - "distinct-count": 5, - "nil%": 0.0 + "metabase_version": "v0.48.3 (80d8323)", + "parameters": [], + "dataset": false, + "created_at": "2024-01-16T09:50:09.487369Z", + "public_uuid": null + }, + "updated_at": "2024-01-16T09:50:34.394488Z", + "col": 12, + "id": 1, + "parameter_mappings": [], + "card_id": 2, + "entity_id": "lXypX5aa14HjkN_Im82C2", + "visualization_settings": {}, + "size_y": 6, + "dashboard_id": 1, + "created_at": "2024-01-16T09:50:34.394488Z", + "row": 0 + }, + { + "size_x": 12, + "dashboard_tab_id": null, + "series": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "description": null, + "archived": false, + "collection_position": null, + "table_id": null, + "result_metadata": [ + { + "display_name": "EVENT_DATE", + "field_ref": [ + "field", + "EVENT_DATE", + { + "base-type": "type/Date" + } + ], + "name": "EVENT_DATE", + "base_type": "type/Date", + "effective_type": "type/Date", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 11, + "nil%": 0 + }, + "type": { + "type/DateTime": { + "earliest": "2024-01-01T00:00:00Z", + "latest": "2024-01-15T00:00:00Z" + } + } + } + }, + { + "display_name": "KNOCKOUT", + "field_ref": [ + "field", + "KNOCKOUT", + { + "base-type": "type/Number" + } + ], + "name": "KNOCKOUT", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 11, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 175, + "q1": 853.75, + "q3": 1116.75, + "max": 1174, + "sd": 296.0767713709648, + "avg": 916.3636363636364 + } + } + } + }, + { + "display_name": "EXPIRY", + "field_ref": [ + "field", + "EXPIRY", + { + "base-type": "type/Number" + } + ], + "name": "EXPIRY", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 10, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 78, + "q1": 295.5, + "q3": 408.3925271309261, + "max": 431, + "sd": 105.10704500218294, + "avg": 336.90909090909093 + } + } + } }, - "type": { - "type/Number": { - "min": 178.0, - "q1": 190.0, - "q3": 213.25, - "max": 223.0, - "sd": 17.131841699011815, - "avg": 200.0 + { + "display_name": "PRODUCT", + "field_ref": [ + "field", + "PRODUCT", + { + "base-type": "type/Number" + } + ], + "name": "PRODUCT", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 9, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 57, + "q1": 163.75, + "q3": 233, + "max": 255, + "sd": 59.31119777763877, + "avg": 195.27272727272728 + } + } + } + }, + { + "display_name": "ISSUER", + "field_ref": [ + "field", + "ISSUER", + { + "base-type": "type/Number" + } + ], + "name": "ISSUER", + "base_type": "type/Number", + "effective_type": "type/Number", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 10, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 43, + "q1": 214, + "q3": 292.25, + "max": 304, + "sd": 79.35879397910594, + "avg": 245.72727272727272 + } + } } } - } - }], - "database_id": 2, - "enable_embedding": false, - "collection_id": null, - "query_type": "query", - "name": "Films, Count, Grouped by Rating, Filtered by Release Year, Sorted by [Unknown Field] descending", - "query_average_duration": 25, - "creator_id": 1, - "moderation_reviews": [], - "updated_at": "2021-12-13T17:48:39.999", - "made_public_by_id": null, - "embedding_params": null, - "cache_ttl": null, - "dataset_query": { - "query": { - "source-table": 21, - "breakout": [ - ["field", 131, null] - ], - "aggregation": [ - ["count"] - ], - "order-by": [ - ["desc", ["aggregation", 0]] + ], + "can_write": true, + "database_id": 3, + "enable_embedding": false, + "collection_id": 112, + "query_type": "native", + "name": "Filter popularity", + "query_average_duration": 2830, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2024-01-16T13:34:30.128815Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "type": "native", + "native": { + "query": "with issuer as\n(\n select event_date, count(*) as issuer_clicks, count(distinct auth_account_id) as issuer\n from TEAMS_PRD.REDACTED.MRT_CURR__MPARTICLE_EVENTS\n where event_name='redacted_search_filter_button_tapped' \n and event_attributes:filter_option::varchar='issuer'\n and event_date>'2023-12-31'\n and platform='Android'\n and dayofweekiso(event_date) NOT IN (6,7)\n and event_attributes:redacted_type::varchar='knock_out_product'\n group by 1\n order by 1 desc\n), expiry as\n(\n select event_date, count(*) as expiry_clicks, count(distinct auth_account_id) as expiry\n from TEAMS_PRD.REDACTED.MRT_CURR__MPARTICLE_EVENTS\n where event_name='redacted_search_filter_button_tapped' \n and event_attributes:filter_option::varchar='expiry'\n and event_date>'2023-12-31'\n and platform='Android'\n and dayofweekiso(event_date) NOT IN (6,7)\n and event_attributes:redacted_type::varchar='knock_out_product'\n group by 1\n order by 1 desc\n), product as\n(\n select event_date, count(*) as product_clicks, count(distinct auth_account_id) as product\n from TEAMS_PRD.REDACTED.MRT_CURR__MPARTICLE_EVENTS\n where event_name='redacted_search_filter_button_tapped' \n and event_attributes:filter_option::varchar='product'\n and event_date>'2023-12-31'\n and platform='Android'\n and dayofweekiso(event_date) NOT IN (6,7)\n and event_attributes:redacted_type::varchar='knock_out_product'\n group by 1\n order by 1 desc\n), knockout as \n(\n select event_date, count(*) as knockout_clicks, count(distinct auth_account_id) as knockout\n from TEAMS_PRD.SCHEMA.MRT_CURR__MPARTICLE_EVENTS\n where event_name='redacted_search_filter_button_tapped' \n and event_attributes:filter_option::varchar='knockout'\n and event_date>'2023-12-31'\n and platform='Android'\n and dayofweekiso(event_date) NOT IN (6,7)\n and event_attributes:redacted_type::varchar='knock_out_product'\n group by 1\n order by 1 desc\n)\nselect k.event_date, knockout, expiry, product, issuer\nfrom knockout k\njoin expiry e on k.event_date=e.event_date\njoin issuer i on k.event_date=i.event_date\njoin product p on k.event_date=p.event_date\nwhere k.event_date Date: Fri, 26 Jan 2024 20:54:06 +0200 Subject: [PATCH 15/19] fix(ingest/glue): Profiling breaks for non-partitioned tables due to absent `Table.PartitionKeys` (#9591) --- metadata-ingestion/src/datahub/ingestion/source/aws/glue.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/metadata-ingestion/src/datahub/ingestion/source/aws/glue.py b/metadata-ingestion/src/datahub/ingestion/source/aws/glue.py index 826c18f69fd01..93601533bf8d6 100644 --- a/metadata-ingestion/src/datahub/ingestion/source/aws/glue.py +++ b/metadata-ingestion/src/datahub/ingestion/source/aws/glue.py @@ -833,9 +833,8 @@ def get_profile_if_enabled( **{k: v for k, v in kwargs.items() if v} ) - partition_keys = response["Table"]["PartitionKeys"] - # check if this table is partitioned + partition_keys = response["Table"].get("PartitionKeys") if partition_keys: # ingest data profile with partitions # for cross-account ingestion From 051f570c47386540266e088d396feed70784f9d5 Mon Sep 17 00:00:00 2001 From: RyanHolstien Date: Fri, 26 Jan 2024 14:17:14 -0600 Subject: [PATCH 16/19] fix(search): fix filters for hasX and numValues fields (#9729) --- .../metadata/models/ConfigEntitySpec.java | 12 ++++ .../metadata/models/DefaultEntitySpec.java | 12 ++++ .../linkedin/metadata/models/EntitySpec.java | 45 +++++++++++---- .../elasticsearch/query/ESBrowseDAO.java | 8 +-- .../elasticsearch/query/ESSearchDAO.java | 3 +- .../request/AutocompleteRequestHandler.java | 10 ++-- .../query/request/SearchRequestHandler.java | 13 +++-- .../metadata/search/utils/ESUtils.java | 55 +++++++++---------- .../ElasticSearchTimeseriesAspectService.java | 32 ++++++----- .../elastic/query/ESAggregatedStatsDAO.java | 2 +- .../fixtures/SampleDataFixtureTestBase.java | 54 ++++++++++++++++++ 11 files changed, 175 insertions(+), 71 deletions(-) diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/ConfigEntitySpec.java b/entity-registry/src/main/java/com/linkedin/metadata/models/ConfigEntitySpec.java index b235e2adcae11..8bd89071e299d 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/ConfigEntitySpec.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/ConfigEntitySpec.java @@ -3,10 +3,12 @@ import com.linkedin.data.schema.RecordDataSchema; import com.linkedin.data.schema.TyperefDataSchema; import com.linkedin.metadata.models.annotation.EntityAnnotation; +import com.linkedin.metadata.models.annotation.SearchableAnnotation; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -19,6 +21,7 @@ public class ConfigEntitySpec implements EntitySpec { private final Map _aspectSpecs; private List _searchableFieldSpecs; + private Map> searchableFieldTypeMap; public ConfigEntitySpec( @Nonnull final String entityName, @@ -89,4 +92,13 @@ public List getSearchableFieldSpecs() { return _searchableFieldSpecs; } + + @Override + public Map> getSearchableFieldTypes() { + if (searchableFieldTypeMap == null) { + searchableFieldTypeMap = EntitySpec.super.getSearchableFieldTypes(); + } + + return searchableFieldTypeMap; + } } diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/DefaultEntitySpec.java b/entity-registry/src/main/java/com/linkedin/metadata/models/DefaultEntitySpec.java index 5db8ca264f69d..2546674f9835c 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/DefaultEntitySpec.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/DefaultEntitySpec.java @@ -3,10 +3,12 @@ import com.linkedin.data.schema.RecordDataSchema; import com.linkedin.data.schema.TyperefDataSchema; import com.linkedin.metadata.models.annotation.EntityAnnotation; +import com.linkedin.metadata.models.annotation.SearchableAnnotation; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -24,6 +26,7 @@ public class DefaultEntitySpec implements EntitySpec { private final TyperefDataSchema _aspectTyperefSchema; private List _searchableFieldSpecs; + private Map> searchableFieldTypeMap; public DefaultEntitySpec( @Nonnull final Collection aspectSpecs, @@ -102,4 +105,13 @@ public List getSearchableFieldSpecs() { return _searchableFieldSpecs; } + + @Override + public Map> getSearchableFieldTypes() { + if (searchableFieldTypeMap == null) { + searchableFieldTypeMap = EntitySpec.super.getSearchableFieldTypes(); + } + + return searchableFieldTypeMap; + } } diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpec.java b/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpec.java index fac08c7e20646..9a75cc1f751d3 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpec.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpec.java @@ -3,7 +3,9 @@ import com.linkedin.data.schema.RecordDataSchema; import com.linkedin.data.schema.TyperefDataSchema; import com.linkedin.metadata.models.annotation.EntityAnnotation; +import com.linkedin.metadata.models.annotation.SearchableAnnotation; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -39,16 +41,39 @@ default List getSearchableFieldSpecs() { .collect(Collectors.toList()); } - default Map> getSearchableFieldSpecMap() { - return getSearchableFieldSpecs().stream() - .collect( - Collectors.toMap( - searchableFieldSpec -> searchableFieldSpec.getSearchableAnnotation().getFieldName(), - searchableFieldSpec -> new HashSet<>(Collections.singleton(searchableFieldSpec)), - (set1, set2) -> { - set1.addAll(set2); - return set1; - })); + default Map> getSearchableFieldTypes() { + // Get additional fields and mint SearchableFieldSpecs for them + Map> fieldSpecMap = new HashMap<>(); + for (SearchableFieldSpec fieldSpec : getSearchableFieldSpecs()) { + SearchableAnnotation searchableAnnotation = fieldSpec.getSearchableAnnotation(); + if (searchableAnnotation.getNumValuesFieldName().isPresent()) { + String fieldName = searchableAnnotation.getNumValuesFieldName().get(); + Set fieldSet = new HashSet<>(); + fieldSet.add(SearchableAnnotation.FieldType.COUNT); + fieldSpecMap.put(fieldName, fieldSet); + } + if (searchableAnnotation.getHasValuesFieldName().isPresent()) { + String fieldName = searchableAnnotation.getHasValuesFieldName().get(); + Set fieldSet = new HashSet<>(); + fieldSet.add(SearchableAnnotation.FieldType.BOOLEAN); + fieldSpecMap.put(fieldName, fieldSet); + } + } + fieldSpecMap.putAll( + getSearchableFieldSpecs().stream() + .collect( + Collectors.toMap( + searchableFieldSpec -> + searchableFieldSpec.getSearchableAnnotation().getFieldName(), + searchableFieldSpec -> + new HashSet<>( + Collections.singleton( + searchableFieldSpec.getSearchableAnnotation().getFieldType())), + (set1, set2) -> { + set1.addAll(set2); + return set1; + }))); + return fieldSpecMap; } default List getSearchScoreFieldSpecs() { diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESBrowseDAO.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESBrowseDAO.java index d610ea4b4e028..0a9a9fbbad086 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESBrowseDAO.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESBrowseDAO.java @@ -19,7 +19,7 @@ import com.linkedin.metadata.config.search.SearchConfiguration; import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.models.EntitySpec; -import com.linkedin.metadata.models.SearchableFieldSpec; +import com.linkedin.metadata.models.annotation.SearchableAnnotation; import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.search.elasticsearch.query.request.SearchRequestHandler; @@ -557,7 +557,7 @@ private QueryBuilder buildQueryStringV2( queryBuilder.filter(QueryBuilders.rangeQuery(BROWSE_PATH_V2_DEPTH).gt(browseDepthVal)); queryBuilder.filter( - SearchRequestHandler.getFilterQuery(filter, entitySpec.getSearchableFieldSpecMap())); + SearchRequestHandler.getFilterQuery(filter, entitySpec.getSearchableFieldTypes())); return queryBuilder; } @@ -583,9 +583,9 @@ private QueryBuilder buildQueryStringBrowseAcrossEntities( queryBuilder.filter(QueryBuilders.rangeQuery(BROWSE_PATH_V2_DEPTH).gt(browseDepthVal)); - Map> searchableFields = + Map> searchableFields = entitySpecs.stream() - .flatMap(entitySpec -> entitySpec.getSearchableFieldSpecMap().entrySet().stream()) + .flatMap(entitySpec -> entitySpec.getSearchableFieldTypes().entrySet().stream()) .collect( Collectors.toMap( Map.Entry::getKey, diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java index 1ec90ed6f61e2..7de2770626ae3 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java @@ -78,8 +78,7 @@ public long docCount(@Nonnull String entityName) { EntitySpec entitySpec = entityRegistry.getEntitySpec(entityName); CountRequest countRequest = new CountRequest(indexConvention.getIndexName(entitySpec)) - .query( - SearchRequestHandler.getFilterQuery(null, entitySpec.getSearchableFieldSpecMap())); + .query(SearchRequestHandler.getFilterQuery(null, entitySpec.getSearchableFieldTypes())); try (Timer.Context ignored = MetricUtils.timer(this.getClass(), "docCount").time()) { return client.count(countRequest, RequestOptions.DEFAULT).getCount(); } catch (IOException e) { diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/AutocompleteRequestHandler.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/AutocompleteRequestHandler.java index 333d9602734d2..3835032247874 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/AutocompleteRequestHandler.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/AutocompleteRequestHandler.java @@ -41,7 +41,7 @@ public class AutocompleteRequestHandler { private final List _defaultAutocompleteFields; - private final Map> searchableFields; + private final Map> searchableFieldTypes; private static final Map AUTOCOMPLETE_QUERY_BUILDER_BY_ENTITY_NAME = new ConcurrentHashMap<>(); @@ -56,14 +56,16 @@ public AutocompleteRequestHandler(@Nonnull EntitySpec entitySpec) { .map(SearchableAnnotation::getFieldName), Stream.of("urn")) .collect(Collectors.toList()); - searchableFields = + searchableFieldTypes = fieldSpecs.stream() .collect( Collectors.toMap( searchableFieldSpec -> searchableFieldSpec.getSearchableAnnotation().getFieldName(), searchableFieldSpec -> - new HashSet<>(Collections.singleton(searchableFieldSpec)), + new HashSet<>( + Collections.singleton( + searchableFieldSpec.getSearchableAnnotation().getFieldType())), (set1, set2) -> { set1.addAll(set2); return set1; @@ -81,7 +83,7 @@ public SearchRequest getSearchRequest( SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.size(limit); searchSourceBuilder.query(getQuery(input, field)); - searchSourceBuilder.postFilter(ESUtils.buildFilterQuery(filter, false, searchableFields)); + searchSourceBuilder.postFilter(ESUtils.buildFilterQuery(filter, false, searchableFieldTypes)); searchSourceBuilder.highlighter(getHighlights(field)); searchRequest.source(searchSourceBuilder); return searchRequest; diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java index e6ee909c80dae..277e15e1334d5 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java @@ -97,7 +97,7 @@ public class SearchRequestHandler { private final SearchConfiguration _configs; private final SearchQueryBuilder _searchQueryBuilder; private final AggregationQueryBuilder _aggregationQueryBuilder; - private final Map> searchableFields; + private final Map> searchableFieldTypes; private SearchRequestHandler( @Nonnull EntitySpec entitySpec, @@ -122,9 +122,9 @@ private SearchRequestHandler( _searchQueryBuilder = new SearchQueryBuilder(configs, customSearchConfiguration); _aggregationQueryBuilder = new AggregationQueryBuilder(configs, annotations); _configs = configs; - searchableFields = + searchableFieldTypes = _entitySpecs.stream() - .flatMap(entitySpec -> entitySpec.getSearchableFieldSpecMap().entrySet().stream()) + .flatMap(entitySpec -> entitySpec.getSearchableFieldTypes().entrySet().stream()) .collect( Collectors.toMap( Map.Entry::getKey, @@ -182,12 +182,13 @@ private BinaryOperator mapMerger() { } public BoolQueryBuilder getFilterQuery(@Nullable Filter filter) { - return getFilterQuery(filter, searchableFields); + return getFilterQuery(filter, searchableFieldTypes); } public static BoolQueryBuilder getFilterQuery( - @Nullable Filter filter, Map> searchableFields) { - BoolQueryBuilder filterQuery = ESUtils.buildFilterQuery(filter, false, searchableFields); + @Nullable Filter filter, + Map> searchableFieldTypes) { + BoolQueryBuilder filterQuery = ESUtils.buildFilterQuery(filter, false, searchableFieldTypes); return filterSoftDeletedByDefault(filter, filterQuery); } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java index 77a67f100895c..4d74bfb66b8db 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java @@ -132,7 +132,7 @@ private ESUtils() {} public static BoolQueryBuilder buildFilterQuery( @Nullable Filter filter, boolean isTimeseries, - final Map> searchableFields) { + final Map> searchableFieldTypes) { BoolQueryBuilder finalQueryBuilder = QueryBuilders.boolQuery(); if (filter == null) { return finalQueryBuilder; @@ -144,7 +144,7 @@ public static BoolQueryBuilder buildFilterQuery( .forEach( or -> finalQueryBuilder.should( - ESUtils.buildConjunctiveFilterQuery(or, isTimeseries, searchableFields))); + ESUtils.buildConjunctiveFilterQuery(or, isTimeseries, searchableFieldTypes))); } else if (filter.getCriteria() != null) { // Otherwise, build boolean query from the deprecated "criteria" field. log.warn("Received query Filter with a deprecated field 'criteria'. Use 'or' instead."); @@ -157,7 +157,7 @@ public static BoolQueryBuilder buildFilterQuery( || criterion.hasValues() || criterion.getCondition() == Condition.IS_NULL) { andQueryBuilder.must( - getQueryBuilderFromCriterion(criterion, isTimeseries, searchableFields)); + getQueryBuilderFromCriterion(criterion, isTimeseries, searchableFieldTypes)); } }); finalQueryBuilder.should(andQueryBuilder); @@ -169,7 +169,7 @@ public static BoolQueryBuilder buildFilterQuery( public static BoolQueryBuilder buildConjunctiveFilterQuery( @Nonnull ConjunctiveCriterion conjunctiveCriterion, boolean isTimeseries, - Map> searchableFields) { + Map> searchableFieldTypes) { final BoolQueryBuilder andQueryBuilder = new BoolQueryBuilder(); conjunctiveCriterion .getAnd() @@ -181,10 +181,10 @@ public static BoolQueryBuilder buildConjunctiveFilterQuery( if (!criterion.isNegated()) { // `filter` instead of `must` (enables caching and bypasses scoring) andQueryBuilder.filter( - getQueryBuilderFromCriterion(criterion, isTimeseries, searchableFields)); + getQueryBuilderFromCriterion(criterion, isTimeseries, searchableFieldTypes)); } else { andQueryBuilder.mustNot( - getQueryBuilderFromCriterion(criterion, isTimeseries, searchableFields)); + getQueryBuilderFromCriterion(criterion, isTimeseries, searchableFieldTypes)); } } }); @@ -222,7 +222,7 @@ public static BoolQueryBuilder buildConjunctiveFilterQuery( public static QueryBuilder getQueryBuilderFromCriterion( @Nonnull final Criterion criterion, boolean isTimeseries, - final Map> searchableFields) { + final Map> searchableFieldTypes) { final String fieldName = toFacetField(criterion.getField()); if (fieldName.startsWith(STRUCTURED_PROPERTY_MAPPING_FIELD)) { criterion.setField(fieldName); @@ -241,10 +241,11 @@ public static QueryBuilder getQueryBuilderFromCriterion( if (maybeFieldToExpand.isPresent()) { return getQueryBuilderFromCriterionForFieldToExpand( - maybeFieldToExpand.get(), criterion, isTimeseries, searchableFields); + maybeFieldToExpand.get(), criterion, isTimeseries, searchableFieldTypes); } - return getQueryBuilderFromCriterionForSingleField(criterion, isTimeseries, searchableFields); + return getQueryBuilderFromCriterionForSingleField( + criterion, isTimeseries, searchableFieldTypes); } public static String getElasticTypeForFieldType(SearchableAnnotation.FieldType fieldType) { @@ -446,7 +447,7 @@ private static QueryBuilder getQueryBuilderFromCriterionForFieldToExpand( @Nonnull final List fields, @Nonnull final Criterion criterion, final boolean isTimeseries, - final Map> searchableFields) { + final Map> searchableFieldTypes) { final BoolQueryBuilder orQueryBuilder = new BoolQueryBuilder(); for (String field : fields) { Criterion criterionToQuery = new Criterion(); @@ -461,7 +462,7 @@ private static QueryBuilder getQueryBuilderFromCriterionForFieldToExpand( criterionToQuery.setField(toKeywordField(field, isTimeseries)); orQueryBuilder.should( getQueryBuilderFromCriterionForSingleField( - criterionToQuery, isTimeseries, searchableFields)); + criterionToQuery, isTimeseries, searchableFieldTypes)); } return orQueryBuilder; } @@ -470,7 +471,7 @@ private static QueryBuilder getQueryBuilderFromCriterionForFieldToExpand( private static QueryBuilder getQueryBuilderFromCriterionForSingleField( @Nonnull Criterion criterion, boolean isTimeseries, - final Map> searchableFields) { + final Map> searchableFieldTypes) { final Condition condition = criterion.getCondition(); final String fieldName = toFacetField(criterion.getField()); @@ -485,10 +486,10 @@ private static QueryBuilder getQueryBuilderFromCriterionForSingleField( } else if (criterion.hasValues() || criterion.hasValue()) { if (condition == Condition.EQUAL) { return buildEqualsConditionFromCriterion( - fieldName, criterion, isTimeseries, searchableFields); + fieldName, criterion, isTimeseries, searchableFieldTypes); } else if (RANGE_QUERY_CONDITIONS.contains(condition)) { return buildRangeQueryFromCriterion( - criterion, fieldName, searchableFields, condition, isTimeseries); + criterion, fieldName, searchableFieldTypes, condition, isTimeseries); } else if (condition == Condition.CONTAIN) { return QueryBuilders.wildcardQuery( toKeywordField(criterion.getField(), isTimeseries), @@ -513,14 +514,14 @@ private static QueryBuilder buildEqualsConditionFromCriterion( @Nonnull final String fieldName, @Nonnull final Criterion criterion, final boolean isTimeseries, - final Map> searchableFields) { + final Map> searchableFieldTypes) { /* * If the newer 'values' field of Criterion.pdl is set, then we * handle using the following code to allow multi-match. */ if (!criterion.getValues().isEmpty()) { return buildEqualsConditionFromCriterionWithValues( - fieldName, criterion, isTimeseries, searchableFields); + fieldName, criterion, isTimeseries, searchableFieldTypes); } /* * Otherwise, we are likely using the deprecated 'value' field. @@ -537,8 +538,8 @@ private static QueryBuilder buildEqualsConditionFromCriterionWithValues( @Nonnull final String fieldName, @Nonnull final Criterion criterion, final boolean isTimeseries, - final Map> searchableFields) { - Set fieldTypes = getFieldTypes(searchableFields, fieldName); + final Map> searchableFieldTypes) { + Set fieldTypes = getFieldTypes(searchableFieldTypes, fieldName); if (fieldTypes.size() > 1) { log.warn( "Multiple field types for field name {}, determining best fit for set: {}", @@ -563,31 +564,27 @@ private static QueryBuilder buildEqualsConditionFromCriterionWithValues( } private static Set getFieldTypes( - Map> searchableFields, String fieldName) { - Set fieldSpecs = + Map> searchableFields, String fieldName) { + Set fieldTypes = searchableFields.getOrDefault(fieldName, Collections.emptySet()); - Set fieldTypes = - fieldSpecs.stream() - .map(SearchableFieldSpec::getSearchableAnnotation) - .map(SearchableAnnotation::getFieldType) - .map(ESUtils::getElasticTypeForFieldType) - .collect(Collectors.toSet()); + Set finalFieldTypes = + fieldTypes.stream().map(ESUtils::getElasticTypeForFieldType).collect(Collectors.toSet()); if (fieldTypes.size() > 1) { log.warn( "Multiple field types for field name {}, determining best fit for set: {}", fieldName, fieldTypes); } - return fieldTypes; + return finalFieldTypes; } private static RangeQueryBuilder buildRangeQueryFromCriterion( Criterion criterion, String fieldName, - Map> searchableFields, + Map> searchableFieldTypes, Condition condition, boolean isTimeseries) { - Set fieldTypes = getFieldTypes(searchableFields, fieldName); + Set fieldTypes = getFieldTypes(searchableFieldTypes, fieldName); // Determine criterion value, range query only accepts single value so take first value in // values if multiple diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/ElasticSearchTimeseriesAspectService.java b/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/ElasticSearchTimeseriesAspectService.java index 6cf8e92d61929..cb06dc75c70bc 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/ElasticSearchTimeseriesAspectService.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/ElasticSearchTimeseriesAspectService.java @@ -14,7 +14,7 @@ import com.linkedin.metadata.aspect.EnvelopedAspect; import com.linkedin.metadata.models.AspectSpec; import com.linkedin.metadata.models.EntitySpec; -import com.linkedin.metadata.models.SearchableFieldSpec; +import com.linkedin.metadata.models.annotation.SearchableAnnotation; import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.query.filter.Condition; import com.linkedin.metadata.query.filter.Criterion; @@ -296,7 +296,7 @@ public long countByFilter( ESUtils.buildFilterQuery( filter, true, - _entityRegistry.getEntitySpec(entityName).getSearchableFieldSpecMap())); + _entityRegistry.getEntitySpec(entityName).getSearchableFieldTypes())); CountRequest countRequest = new CountRequest(); countRequest.query(filterQueryBuilder); countRequest.indices(indexName); @@ -319,10 +319,11 @@ public List getAspectValues( @Nullable final Integer limit, @Nullable final Filter filter, @Nullable final SortCriterion sort) { - Map> searchableFields = - _entityRegistry.getEntitySpec(entityName).getSearchableFieldSpecMap(); + Map> searchableFieldTypes = + _entityRegistry.getEntitySpec(entityName).getSearchableFieldTypes(); final BoolQueryBuilder filterQueryBuilder = - QueryBuilders.boolQuery().must(ESUtils.buildFilterQuery(filter, true, searchableFields)); + QueryBuilders.boolQuery() + .must(ESUtils.buildFilterQuery(filter, true, searchableFieldTypes)); filterQueryBuilder.must(QueryBuilders.matchQuery("urn", urn.toString())); // NOTE: We are interested only in the un-exploded rows as only they carry the `event` payload. filterQueryBuilder.mustNot(QueryBuilders.termQuery(MappingsBuilder.IS_EXPLODED_FIELD, true)); @@ -333,7 +334,7 @@ public List getAspectValues( .setCondition(Condition.GREATER_THAN_OR_EQUAL_TO) .setValue(startTimeMillis.toString()); filterQueryBuilder.must( - ESUtils.getQueryBuilderFromCriterion(startTimeCriterion, true, searchableFields)); + ESUtils.getQueryBuilderFromCriterion(startTimeCriterion, true, searchableFieldTypes)); } if (endTimeMillis != null) { Criterion endTimeCriterion = @@ -342,7 +343,7 @@ public List getAspectValues( .setCondition(Condition.LESS_THAN_OR_EQUAL_TO) .setValue(endTimeMillis.toString()); filterQueryBuilder.must( - ESUtils.getQueryBuilderFromCriterion(endTimeCriterion, true, searchableFields)); + ESUtils.getQueryBuilderFromCriterion(endTimeCriterion, true, searchableFieldTypes)); } final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(filterQueryBuilder); @@ -412,7 +413,7 @@ public DeleteAspectValuesResult deleteAspectValues( final String indexName = _indexConvention.getTimeseriesAspectIndexName(entityName, aspectName); final BoolQueryBuilder filterQueryBuilder = ESUtils.buildFilterQuery( - filter, true, _entityRegistry.getEntitySpec(entityName).getSearchableFieldSpecMap()); + filter, true, _entityRegistry.getEntitySpec(entityName).getSearchableFieldTypes()); final Optional result = _bulkProcessor @@ -440,7 +441,7 @@ public String deleteAspectValuesAsync( final String indexName = _indexConvention.getTimeseriesAspectIndexName(entityName, aspectName); final BoolQueryBuilder filterQueryBuilder = ESUtils.buildFilterQuery( - filter, true, _entityRegistry.getEntitySpec(entityName).getSearchableFieldSpecMap()); + filter, true, _entityRegistry.getEntitySpec(entityName).getSearchableFieldTypes()); final int batchSize = options.getBatchSize() > 0 ? options.getBatchSize() : DEFAULT_LIMIT; TimeValue timeout = options.getTimeoutSeconds() > 0 @@ -466,7 +467,7 @@ public String reindexAsync( final String indexName = _indexConvention.getTimeseriesAspectIndexName(entityName, aspectName); final BoolQueryBuilder filterQueryBuilder = ESUtils.buildFilterQuery( - filter, true, _entityRegistry.getEntitySpec(entityName).getSearchableFieldSpecMap()); + filter, true, _entityRegistry.getEntitySpec(entityName).getSearchableFieldTypes()); try { return this.reindexAsync(indexName, filterQueryBuilder, options); } catch (Exception e) { @@ -515,10 +516,11 @@ public TimeseriesScrollResult scrollAspects( @Nullable Long startTimeMillis, @Nullable Long endTimeMillis) { - Map> searchableFields = - _entityRegistry.getEntitySpec(entityName).getSearchableFieldSpecMap(); + Map> searchableFieldTypes = + _entityRegistry.getEntitySpec(entityName).getSearchableFieldTypes(); final BoolQueryBuilder filterQueryBuilder = - QueryBuilders.boolQuery().filter(ESUtils.buildFilterQuery(filter, true, searchableFields)); + QueryBuilders.boolQuery() + .filter(ESUtils.buildFilterQuery(filter, true, searchableFieldTypes)); if (startTimeMillis != null) { Criterion startTimeCriterion = @@ -527,7 +529,7 @@ public TimeseriesScrollResult scrollAspects( .setCondition(Condition.GREATER_THAN_OR_EQUAL_TO) .setValue(startTimeMillis.toString()); filterQueryBuilder.filter( - ESUtils.getQueryBuilderFromCriterion(startTimeCriterion, true, searchableFields)); + ESUtils.getQueryBuilderFromCriterion(startTimeCriterion, true, searchableFieldTypes)); } if (endTimeMillis != null) { Criterion endTimeCriterion = @@ -536,7 +538,7 @@ public TimeseriesScrollResult scrollAspects( .setCondition(Condition.LESS_THAN_OR_EQUAL_TO) .setValue(endTimeMillis.toString()); filterQueryBuilder.filter( - ESUtils.getQueryBuilderFromCriterion(endTimeCriterion, true, searchableFields)); + ESUtils.getQueryBuilderFromCriterion(endTimeCriterion, true, searchableFieldTypes)); } SearchResponse response = diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/query/ESAggregatedStatsDAO.java b/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/query/ESAggregatedStatsDAO.java index f8b2cd8552357..580888e54b700 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/query/ESAggregatedStatsDAO.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeseries/elastic/query/ESAggregatedStatsDAO.java @@ -379,7 +379,7 @@ public GenericTable getAggregatedStats( // Setup the filter query builder using the input filter provided. final BoolQueryBuilder filterQueryBuilder = ESUtils.buildFilterQuery( - filter, true, _entityRegistry.getEntitySpec(entityName).getSearchableFieldSpecMap()); + filter, true, _entityRegistry.getEntitySpec(entityName).getSearchableFieldTypes()); AspectSpec aspectSpec = getTimeseriesAspectSpec(entityName, aspectName); // Build and attach the grouping aggregations diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/SampleDataFixtureTestBase.java b/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/SampleDataFixtureTestBase.java index a1af2325ee0ed..4742115b16e1b 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/SampleDataFixtureTestBase.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/fixtures/SampleDataFixtureTestBase.java @@ -14,8 +14,10 @@ import com.datahub.authentication.Actor; import com.datahub.authentication.ActorType; import com.datahub.authentication.Authentication; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.linkedin.common.urn.Urn; +import com.linkedin.data.template.StringArray; import com.linkedin.datahub.graphql.generated.AutoCompleteResults; import com.linkedin.datahub.graphql.types.chart.ChartType; import com.linkedin.datahub.graphql.types.container.ContainerType; @@ -45,6 +47,7 @@ import com.linkedin.r2.RemoteInvocationException; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -64,6 +67,7 @@ import org.opensearch.search.sort.FieldSortBuilder; import org.opensearch.search.sort.SortBuilder; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.AssertJUnit; import org.testng.annotations.Test; public abstract class SampleDataFixtureTestBase extends AbstractTestNGSpringContextTests { @@ -1936,6 +1940,56 @@ public void testSortOrdering() { String.format("%s - Expected search results to have at least two results", query)); } + @Test + public void testFilterOnHasValuesField() { + AssertJUnit.assertNotNull(getSearchService()); + Filter filter = + new Filter() + .setOr( + new ConjunctiveCriterionArray( + new ConjunctiveCriterion() + .setAnd( + new CriterionArray( + ImmutableList.of( + new Criterion() + .setField("hasOwners") + .setValue("") + .setValues(new StringArray(ImmutableList.of("true")))))))); + SearchResult searchResult = + searchAcrossEntities( + getSearchService(), + "*", + SEARCHABLE_ENTITIES, + filter, + Collections.singletonList(DATASET_ENTITY_NAME)); + assertEquals(searchResult.getEntities().size(), 8); + } + + @Test + public void testFilterOnNumValuesField() { + AssertJUnit.assertNotNull(getSearchService()); + Filter filter = + new Filter() + .setOr( + new ConjunctiveCriterionArray( + new ConjunctiveCriterion() + .setAnd( + new CriterionArray( + ImmutableList.of( + new Criterion() + .setField("numInputDatasets") + .setValue("") + .setValues(new StringArray(ImmutableList.of("1")))))))); + SearchResult searchResult = + searchAcrossEntities( + getSearchService(), + "*", + SEARCHABLE_ENTITIES, + filter, + Collections.singletonList(DATA_JOB_ENTITY_NAME)); + assertEquals(searchResult.getEntities().size(), 4); + } + private Stream getTokens(AnalyzeRequest request) throws IOException { return getSearchClient() From 388b3ec0ac10f7e3d142c9bcbf9c89be6ea92853 Mon Sep 17 00:00:00 2001 From: Harshal Sheth Date: Fri, 26 Jan 2024 14:01:48 -0800 Subject: [PATCH 17/19] fix(ingest/airflow): fix plugin support for airflow 2.5.0 (#9719) --- .../src/datahub_airflow_plugin/_datahub_listener_module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metadata-ingestion-modules/airflow-plugin/src/datahub_airflow_plugin/_datahub_listener_module.py b/metadata-ingestion-modules/airflow-plugin/src/datahub_airflow_plugin/_datahub_listener_module.py index e16563400e397..0e1ef69ebf18c 100644 --- a/metadata-ingestion-modules/airflow-plugin/src/datahub_airflow_plugin/_datahub_listener_module.py +++ b/metadata-ingestion-modules/airflow-plugin/src/datahub_airflow_plugin/_datahub_listener_module.py @@ -29,6 +29,6 @@ def on_task_instance_failed(previous_state, task_instance, session): if hasattr(_listener, "on_dag_run_running"): @hookimpl - def on_dag_run_running(dag_run, session): + def on_dag_run_running(dag_run, msg): assert _listener - _listener.on_dag_run_running(dag_run, session) + _listener.on_dag_run_running(dag_run, msg) From 5adb799f137a00c315144715786179ef4a6b2405 Mon Sep 17 00:00:00 2001 From: Harshal Sheth Date: Fri, 26 Jan 2024 14:02:52 -0800 Subject: [PATCH 18/19] fix(cli): fix example data contract yaml + update airflow codecov (#9707) --- .github/workflows/airflow-plugin.yml | 4 +- .../airflow-plugin/build.gradle | 2 +- .../airflow-plugin/tests/conftest.py | 11 +++++ .../pet_of_the_week.dhub.dc.yaml | 42 +++++++++++-------- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/.github/workflows/airflow-plugin.yml b/.github/workflows/airflow-plugin.yml index 7ae7b87b0f5ce..c5c75de4f7aee 100644 --- a/.github/workflows/airflow-plugin.yml +++ b/.github/workflows/airflow-plugin.yml @@ -87,8 +87,8 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} directory: . fail_ci_if_error: false - flags: airflow-${{ matrix.python-version }}-${{ matrix.extraPythonRequirement }} - name: pytest-airflow + flags: airflow,airflow-${{ matrix.extra_pip_extras }} + name: pytest-airflow-${{ matrix.python-version }}-${{ matrix.extra_pip_requirements }} verbose: true event-file: diff --git a/metadata-ingestion-modules/airflow-plugin/build.gradle b/metadata-ingestion-modules/airflow-plugin/build.gradle index dacf12dc020df..9555f92c8831d 100644 --- a/metadata-ingestion-modules/airflow-plugin/build.gradle +++ b/metadata-ingestion-modules/airflow-plugin/build.gradle @@ -108,7 +108,7 @@ task testQuick(type: Exec, dependsOn: installDevTest) { inputs.files(project.fileTree(dir: "src/", include: "**/*.py")) inputs.files(project.fileTree(dir: "tests/")) commandLine 'bash', '-x', '-c', - "source ${venv_name}/bin/activate && pytest -vv --continue-on-collection-errors --junit-xml=junit.quick.xml" + "source ${venv_name}/bin/activate && pytest --cov-config=setup.cfg --cov-report xml:coverage_quick.xml -vv --continue-on-collection-errors --junit-xml=junit.quick.xml" } diff --git a/metadata-ingestion-modules/airflow-plugin/tests/conftest.py b/metadata-ingestion-modules/airflow-plugin/tests/conftest.py index d2c45e723f1b0..994816ff037c8 100644 --- a/metadata-ingestion-modules/airflow-plugin/tests/conftest.py +++ b/metadata-ingestion-modules/airflow-plugin/tests/conftest.py @@ -1,6 +1,17 @@ +import pathlib +import site + + def pytest_addoption(parser): parser.addoption( "--update-golden-files", action="store_true", default=False, ) + + +# See https://coverage.readthedocs.io/en/latest/subprocess.html#configuring-python-for-sub-process-measurement +coverage_startup_code = "import coverage; coverage.process_startup()" +site_packages_dir = pathlib.Path(site.getsitepackages()[0]) +pth_file_path = site_packages_dir / "datahub_coverage_startup.pth" +pth_file_path.write_text(coverage_startup_code) diff --git a/metadata-ingestion/examples/data_contract/pet_of_the_week.dhub.dc.yaml b/metadata-ingestion/examples/data_contract/pet_of_the_week.dhub.dc.yaml index c73904403f678..bd081172b2a27 100644 --- a/metadata-ingestion/examples/data_contract/pet_of_the_week.dhub.dc.yaml +++ b/metadata-ingestion/examples/data_contract/pet_of_the_week.dhub.dc.yaml @@ -1,21 +1,29 @@ -# id: pet_details_dc # Optional: This is the unique identifier for the data contract -display_name: Data Contract for SampleHiveDataset +version: 1 # datahub yaml format version + +# Note: this data contract yaml format is still in development, and will likely +# change in backwards-incompatible ways in the future. + entity: urn:li:dataset:(urn:li:dataPlatform:hive,SampleHiveDataset,PROD) freshness: - time: 0700 - granularity: DAILY + type: cron + cron: 0 7 * * * # 7am daily + timezone: America/Los_Angeles schema: - properties: - field_foo: - type: string - native_type: VARCHAR(100) - field_bar: - type: boolean - required: - - field_bar + type: json-schema + json-schema: + properties: + field_foo: + type: string + native_type: VARCHAR(100) + field_bar: + type: boolean + required: + - field_bar data_quality: - - type: column_range - config: - column: field_foo - min: 0 - max: 100 + - type: unique + column: field_foo + - type: custom_sql + sql: SELECT COUNT(*) FROM SampleHiveDataset + operator: + type: greater_than + value: 100 From 2bb4b73f98ef46446e8025cd3657289bb24ff0df Mon Sep 17 00:00:00 2001 From: Harshal Sheth Date: Fri, 26 Jan 2024 14:03:16 -0800 Subject: [PATCH 19/19] fix(ingest/metabase): add missing sql parser dep (#9725) --- metadata-ingestion/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metadata-ingestion/setup.py b/metadata-ingestion/setup.py index 1fb570d76120e..c1a5da5826ead 100644 --- a/metadata-ingestion/setup.py +++ b/metadata-ingestion/setup.py @@ -340,7 +340,7 @@ "ldap": {"python-ldap>=2.4"}, "looker": looker_common, "lookml": looker_common, - "metabase": {"requests"} | sqllineage_lib, + "metabase": {"requests"} | sqlglot_lib, "mlflow": {"mlflow-skinny>=2.3.0"}, "mode": {"requests", "tenacity>=8.0.1"} | sqllineage_lib, "mongodb": {"pymongo[srv]>=3.11", "packaging"},