Extend team to support defaultRoles (#2885)

* Extend team to support defaultRoles

* Add support for team defaultRoles change description

* Add tests
This commit is contained in:
Matt 2022-02-20 21:21:08 -08:00 committed by GitHub
parent 9e4d8d709d
commit 0791c8c266
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 195 additions and 70 deletions

View File

@ -35,8 +35,8 @@ import org.openmetadata.catalog.util.EntityUtil;
import org.openmetadata.catalog.util.EntityUtil.Fields;
public class TeamRepository extends EntityRepository<Team> {
static final Fields TEAM_UPDATE_FIELDS = new Fields(TeamResource.FIELD_LIST, "profile,users");
static final Fields TEAM_PATCH_FIELDS = new Fields(TeamResource.FIELD_LIST, "profile,users");
static final Fields TEAM_UPDATE_FIELDS = new Fields(TeamResource.FIELD_LIST, "profile,users,defaultRoles");
static final Fields TEAM_PATCH_FIELDS = new Fields(TeamResource.FIELD_LIST, "profile,users,defaultRoles");
public TeamRepository(CollectionDAO dao) {
super(
@ -52,23 +52,33 @@ public class TeamRepository extends EntityRepository<Team> {
false);
}
public List<EntityReference> getUsers(List<UUID> userIds) {
if (userIds == null) {
public List<EntityReference> getEntityReferences(List<UUID> ids) {
if (ids == null) {
return null;
}
List<EntityReference> users = new ArrayList<>();
for (UUID id : userIds) {
users.add(new EntityReference().withId(id));
List<EntityReference> entityReferences = new ArrayList<>();
for (UUID id : ids) {
entityReferences.add(new EntityReference().withId(id));
}
return users;
return entityReferences;
}
public void validateUsers(List<EntityReference> users) throws IOException {
if (users != null) {
users.sort(EntityUtil.compareEntityReference);
for (EntityReference user : users) {
EntityReference ref = daoCollection.userDAO().findEntityReferenceById(user.getId());
user.withType(ref.getType()).withName(ref.getName()).withDisplayName(ref.getDisplayName());
public void validateEntityReferences(List<EntityReference> entityReferences, String entityType) throws IOException {
if (entityReferences != null) {
entityReferences.sort(EntityUtil.compareEntityReference);
for (EntityReference entityReference : entityReferences) {
EntityReference ref;
switch (entityType) {
case Entity.USER:
ref = daoCollection.userDAO().findEntityReferenceById(entityReference.getId());
break;
case Entity.ROLE:
ref = daoCollection.roleDAO().findEntityReferenceById(entityReference.getId());
break;
default:
throw new IllegalArgumentException("Unsupported entity reference for validation");
}
entityReference.withType(ref.getType()).withName(ref.getName()).withDisplayName(ref.getDisplayName());
}
}
}
@ -80,6 +90,7 @@ public class TeamRepository extends EntityRepository<Team> {
}
team.setUsers(fields.contains("users") ? getUsers(team) : null);
team.setOwns(fields.contains("owns") ? getOwns(team) : null);
team.setDefaultRoles(fields.contains("defaultRoles") ? getDefaultRoles(team) : null);
return team;
}
@ -96,21 +107,23 @@ public class TeamRepository extends EntityRepository<Team> {
@Override
public void prepare(Team team) throws IOException {
validateUsers(team.getUsers());
validateEntityReferences(team.getUsers(), Entity.USER);
validateEntityReferences(team.getDefaultRoles(), Entity.ROLE);
}
@Override
public void storeEntity(Team team, boolean update) throws IOException {
// Relationships and fields such as href are derived and not stored as part of json
List<EntityReference> users = team.getUsers();
List<EntityReference> defaultRoles = team.getDefaultRoles();
// Don't store users, href as JSON. Build it on the fly based on relationships
team.withUsers(null).withHref(null);
// Don't store users, defaultRoles, href as JSON. Build it on the fly based on relationships
team.withUsers(null).withDefaultRoles(null).withHref(null);
store(team.getId(), team, update);
// Restore the relationships
team.withUsers(users);
team.withUsers(users).withDefaultRoles(defaultRoles);
}
@Override
@ -118,6 +131,9 @@ public class TeamRepository extends EntityRepository<Team> {
for (EntityReference user : Optional.ofNullable(team.getUsers()).orElse(Collections.emptyList())) {
addRelationship(team.getId(), user.getId(), Entity.TEAM, Entity.USER, Relationship.HAS);
}
for (EntityReference defaultRole : Optional.ofNullable(team.getDefaultRoles()).orElse(Collections.emptyList())) {
addRelationship(team.getId(), defaultRole.getId(), Entity.TEAM, Entity.ROLE, Relationship.HAS);
}
}
@Override
@ -127,11 +143,7 @@ public class TeamRepository extends EntityRepository<Team> {
private List<EntityReference> getUsers(Team team) throws IOException {
List<String> userIds = findTo(team.getId(), Entity.TEAM, Relationship.HAS, Entity.USER, toBoolean(toInclude(team)));
List<EntityReference> users = new ArrayList<>();
for (String userId : userIds) {
users.add(daoCollection.userDAO().findEntityReferenceById(UUID.fromString(userId)));
}
return users;
return populateEntityReferences(userIds, Entity.USER);
}
private List<EntityReference> getOwns(Team team) throws IOException {
@ -142,6 +154,29 @@ public class TeamRepository extends EntityRepository<Team> {
.findTo(team.getId().toString(), Entity.TEAM, Relationship.OWNS.ordinal(), toBoolean(toInclude(team))));
}
private List<EntityReference> getDefaultRoles(Team team) throws IOException {
List<String> defaultRoleIds =
findTo(team.getId(), Entity.TEAM, Relationship.HAS, Entity.ROLE, toBoolean(toInclude(team)));
return populateEntityReferences(defaultRoleIds, Entity.ROLE);
}
private List<EntityReference> populateEntityReferences(List<String> ids, String entityType) throws IOException {
List<EntityReference> refs = new ArrayList<>();
for (String id : ids) {
switch (entityType) {
case Entity.USER:
refs.add(daoCollection.userDAO().findEntityReferenceById(UUID.fromString(id)));
break;
case Entity.ROLE:
refs.add(daoCollection.roleDAO().findEntityReferenceById(UUID.fromString(id)));
break;
default:
throw new IllegalArgumentException("Unsupported entity type for populating entityReference list");
}
}
return refs;
}
public static class TeamEntityInterface implements EntityInterface<Team> {
private final Team entity;
@ -273,27 +308,56 @@ public class TeamRepository extends EntityRepository<Team> {
public void entitySpecificUpdate() throws IOException {
recordChange("profile", original.getEntity().getProfile(), updated.getEntity().getProfile());
updateUsers(original.getEntity(), updated.getEntity());
updateDefaultRoles(original.getEntity(), updated.getEntity());
}
private void updateUsers(Team origTeam, Team updatedTeam) throws JsonProcessingException {
List<EntityReference> origUsers = Optional.ofNullable(origTeam.getUsers()).orElse(Collections.emptyList());
List<EntityReference> updatedUsers = Optional.ofNullable(updatedTeam.getUsers()).orElse(Collections.emptyList());
updateEntityRelationships(
"users", origTeam.getId(), updatedTeam.getId(), Relationship.HAS, Entity.USER, origUsers, updatedUsers);
}
private void updateDefaultRoles(Team origTeam, Team updatedTeam) throws JsonProcessingException {
List<EntityReference> origDefaultRoles =
Optional.ofNullable(origTeam.getDefaultRoles()).orElse(Collections.emptyList());
List<EntityReference> updatedDefaultRoles =
Optional.ofNullable(updatedTeam.getDefaultRoles()).orElse(Collections.emptyList());
updateEntityRelationships(
"defaultRoles",
origTeam.getId(),
updatedTeam.getId(),
Relationship.HAS,
Entity.ROLE,
origDefaultRoles,
updatedDefaultRoles);
}
private void updateEntityRelationships(
String field,
UUID origId,
UUID updatedId,
Relationship relationshipType,
String toEntityType,
List<EntityReference> origRefs,
List<EntityReference> updatedRefs)
throws JsonProcessingException {
List<EntityReference> added = new ArrayList<>();
List<EntityReference> deleted = new ArrayList<>();
if (recordListChange("users", origUsers, updatedUsers, added, deleted, entityReferenceMatch)) {
// Remove users from original and add users from updated
daoCollection
.relationshipDAO()
.deleteFrom(origTeam.getId().toString(), Entity.TEAM, Relationship.HAS.ordinal(), "user");
// Add relationships
for (EntityReference user : updatedUsers) {
addRelationship(updatedTeam.getId(), user.getId(), Entity.TEAM, Entity.USER, Relationship.HAS);
}
updatedUsers.sort(EntityUtil.compareEntityReference);
origUsers.sort(EntityUtil.compareEntityReference);
if (!recordListChange(field, origRefs, updatedRefs, added, deleted, entityReferenceMatch)) {
// No changes between original and updated.
return;
}
// Remove relationships from original
daoCollection
.relationshipDAO()
.deleteFrom(origId.toString(), Entity.TEAM, relationshipType.ordinal(), toEntityType);
// Add relationships from updated
for (EntityReference ref : updatedRefs) {
addRelationship(updatedId, ref.getId(), Entity.TEAM, toEntityType, relationshipType);
}
updatedRefs.sort(EntityUtil.compareEntityReference);
origRefs.sort(EntityUtil.compareEntityReference);
}
}
}

View File

@ -78,6 +78,7 @@ public class TeamResource {
public static Team addHref(UriInfo uriInfo, Team team) {
Entity.withHref(uriInfo, team.getUsers());
Entity.withHref(uriInfo, team.getDefaultRoles());
Entity.withHref(uriInfo, team.getOwns());
return team;
}
@ -98,7 +99,7 @@ public class TeamResource {
}
}
protected static final String FIELDS = "profile,users,owns";
protected static final String FIELDS = "profile,users,owns,defaultRoles";
public static final List<String> FIELD_LIST = Arrays.asList(FIELDS.replace(" ", "").split(","));
@GET
@ -370,6 +371,7 @@ public class TeamResource {
.withProfile(ct.getProfile())
.withUpdatedBy(securityContext.getUserPrincipal().getName())
.withUpdatedAt(System.currentTimeMillis())
.withUsers(dao.getUsers(ct.getUsers()));
.withUsers(dao.getEntityReferences(ct.getUsers()))
.withDefaultRoles(dao.getEntityReferences(ct.getDefaultRoles()));
}
}

View File

@ -4,25 +4,32 @@
"title": "CreateTeamRequest",
"description": "Team entity",
"type": "object",
"properties": {
"name": {
"$ref": "../../entity/teams/team.json#/definitions/teamName"
},
"displayName": {
"description": "Optional name used for display purposes. Example 'Marketing Team'",
"description": "Optional name used for display purposes. Example 'Marketing Team'.",
"type": "string"
},
"description": {
"description": "Optional description of the team",
"description": "Optional description of the team.",
"type": "string"
},
"profile": {
"description": "Optional team profile information",
"description": "Optional team profile information.",
"$ref": "../../type/profile.json"
},
"users": {
"description": "Optional IDs of users that are part of the team",
"description": "Optional IDs of users that are part of the team.",
"type": "array",
"items": {
"$ref": "../../type/basic.json#/definitions/uuid"
},
"default": null
},
"defaultRoles": {
"description": "Roles to be assigned to all users that are part of this team.",
"type": "array",
"items": {
"$ref": "../../type/basic.json#/definitions/uuid"

View File

@ -4,7 +4,6 @@
"title": "Team",
"description": "This schema defines the Team entity. A Team is a group of zero or more users. Teams can own zero or more data assets.",
"type": "object",
"definitions": {
"teamName": {
"description": "A unique name of the team typically the team ID from an identity provider. Example - group Id from LDAP.",
@ -13,7 +12,6 @@
"maxLength": 128
}
},
"properties": {
"id": {
"$ref": "../../type/basic.json#/definitions/uuid"
@ -66,6 +64,10 @@
"description": "When `true` indicates the entity has been soft deleted.",
"type": "boolean",
"default": false
},
"defaultRoles": {
"description": "Roles to be assigned to all users that are part of this team.",
"$ref": "../../type/entityReference.json#/definitions/entityReferenceList"
}
},
"required": ["id", "name", "href"],

View File

@ -36,12 +36,14 @@ import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.HttpResponseException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.api.teams.CreateTeam;
import org.openmetadata.catalog.entity.teams.Role;
import org.openmetadata.catalog.entity.teams.Team;
import org.openmetadata.catalog.entity.teams.User;
import org.openmetadata.catalog.exception.CatalogExceptionMessage;
@ -55,7 +57,6 @@ import org.openmetadata.catalog.type.FieldChange;
import org.openmetadata.catalog.type.ImageList;
import org.openmetadata.catalog.type.Profile;
import org.openmetadata.catalog.util.EntityInterface;
import org.openmetadata.catalog.util.EntityUtil;
import org.openmetadata.catalog.util.JsonUtils;
import org.openmetadata.catalog.util.TestUtils;
import org.openmetadata.catalog.util.TestUtils.UpdateType;
@ -88,21 +89,28 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
}
@Test
void post_teamWithUsers_200_OK(TestInfo test) throws IOException {
void post_teamWithUsersAndDefaultRoles_200_OK(TestInfo test) throws IOException {
// Add team to user relationships while creating a team
UserResourceTest userResourceTest = new UserResourceTest();
User user1 = userResourceTest.createEntity(userResourceTest.createRequest(test, 1), TEST_AUTH_HEADERS);
User user2 = userResourceTest.createEntity(userResourceTest.createRequest(test, 2), TEST_AUTH_HEADERS);
List<UUID> users = Arrays.asList(user1.getId(), user2.getId());
RoleResourceTest roleResourceTest = new RoleResourceTest();
Role role1 = roleResourceTest.createEntity(roleResourceTest.createRequest(test, 1), ADMIN_AUTH_HEADERS);
Role role2 = roleResourceTest.createEntity(roleResourceTest.createRequest(test, 2), ADMIN_AUTH_HEADERS);
List<UUID> roles = Arrays.asList(role1.getId(), role2.getId());
CreateTeam create =
createRequest(test)
.withDisplayName("displayName")
.withDescription("description")
.withProfile(PROFILE)
.withUsers(users);
.withUsers(users)
.withDefaultRoles(roles);
Team team = createAndCheckEntity(create, ADMIN_AUTH_HEADERS);
// Make sure the user entity has relationship to the team
// Ensure that the user entity has relationship to the team
user1 = userResourceTest.getEntity(user1.getId(), "teams", TEST_AUTH_HEADERS);
assertEquals(team.getId(), user1.getTeams().get(0).getId());
user2 = userResourceTest.getEntity(user2.getId(), "teams", TEST_AUTH_HEADERS);
@ -135,15 +143,26 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
UserResourceTest userResourceTest = new UserResourceTest();
User user1 = userResourceTest.createEntity(userResourceTest.createRequest(test, 1), ADMIN_AUTH_HEADERS);
List<UUID> users = Collections.singletonList(user1.getId());
CreateTeam create = createRequest(test).withUsers(users);
RoleResourceTest roleResourceTest = new RoleResourceTest();
Role role1 = roleResourceTest.createEntity(roleResourceTest.createRequest(test, 1), ADMIN_AUTH_HEADERS);
List<UUID> roles = Collections.singletonList(role1.getId());
CreateTeam create = createRequest(test).withUsers(users).withDefaultRoles(roles);
Team team = createAndCheckEntity(create, ADMIN_AUTH_HEADERS);
// Team with users can be deleted - Team -- has --> User relationships are deleted
// Team with users and defaultRoles can be deleted
// Team -- has --> User relationships are deleted
// Team -- has --> Role relationships are deleted
deleteAndCheckEntity(team, ADMIN_AUTH_HEADERS);
// Make sure user does not have relationship to this team
// Ensure that the user does not have relationship to this team
User user = userResourceTest.getEntity(user1.getId(), "teams", ADMIN_AUTH_HEADERS);
assertTrue(user.getTeams().isEmpty());
// Ensure that the role is not deleted
Role role = roleResourceTest.getEntity(role1.getId(), "", ADMIN_AUTH_HEADERS);
assertNotNull(role);
}
@Test
@ -160,7 +179,7 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
}
@Test
void patch_deleteUserFromTeam_200(TestInfo test) throws IOException {
void patch_deleteUserAndDefaultRoleFromTeam_200(TestInfo test) throws IOException {
UserResourceTest userResourceTest = new UserResourceTest();
final int totalUsers = 20;
ArrayList<UUID> users = new ArrayList<>();
@ -168,17 +187,35 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
User user = userResourceTest.createEntity(userResourceTest.createRequest(test, i), ADMIN_AUTH_HEADERS);
users.add(user.getId());
}
RoleResourceTest roleResourceTest = new RoleResourceTest();
roleResourceTest.createRolesAndSetDefault(test, 5, 0);
List<Role> roles = roleResourceTest.listEntities(Map.of(), ADMIN_AUTH_HEADERS).getData();
List<UUID> rolesIds = roles.stream().map(Role::getId).collect(Collectors.toList());
CreateTeam create =
createRequest(getEntityName(test), "description", "displayName", null).withProfile(PROFILE).withUsers(users);
createRequest(getEntityName(test), "description", "displayName", null)
.withProfile(PROFILE)
.withUsers(users)
.withDefaultRoles(rolesIds);
Team team = createAndCheckEntity(create, ADMIN_AUTH_HEADERS);
// Remove a user from the team list using patch request
// Remove a user from the team using patch request
String json = JsonUtils.pojoToJson(team);
int removeUserIndex = new Random().nextInt(totalUsers);
EntityReference deletedUser = team.getUsers().get(removeUserIndex).withHref(null);
team.getUsers().remove(removeUserIndex);
ChangeDescription change = getChangeDescription(team.getVersion());
change.getFieldsDeleted().add(new FieldChange().withName("users").withOldValue(Arrays.asList(deletedUser)));
team = patchEntityAndCheck(team, json, ADMIN_AUTH_HEADERS, UpdateType.MINOR_UPDATE, change);
// Remove a default role from the team using patch request
json = JsonUtils.pojoToJson(team);
int removeDefaultRoleIndex = new Random().nextInt(roles.size());
EntityReference deletedRole = team.getDefaultRoles().get(removeDefaultRoleIndex).withHref(null);
team.getDefaultRoles().remove(removeDefaultRoleIndex);
change = getChangeDescription(team.getVersion());
change.getFieldsDeleted().add(new FieldChange().withName("defaultRoles").withOldValue(Arrays.asList(deletedRole)));
patchEntityAndCheck(team, json, ADMIN_AUTH_HEADERS, UpdateType.MINOR_UPDATE, change);
}
@ -188,6 +225,7 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
String expectedDisplayName,
Profile expectedProfile,
List<EntityReference> expectedUsers,
List<EntityReference> expectedDefaultRoles,
String expectedUpdatedBy) {
assertListNotNull(team.getId(), team.getHref());
assertEquals(expectedDescription, team.getDescription());
@ -195,6 +233,7 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
assertEquals(expectedDisplayName, team.getDisplayName());
assertEquals(expectedProfile, team.getProfile());
TestUtils.assertEntityReferenceList(expectedUsers, team.getUsers());
TestUtils.assertEntityReferenceList(expectedDefaultRoles, team.getDefaultRoles());
TestUtils.validateEntityReference(team.getOwns());
}
@ -214,18 +253,20 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
expectedTeam.getDisplayName(),
expectedTeam.getProfile(),
null,
null,
updatedBy);
assertNull(getTeam.getOwns());
// .../teams?fields=users,owns
fields = "users,owns,profile";
// .../teams?fields=users,owns,profile,defaultRoles
fields = "users,owns,profile,defaultRoles";
getTeam =
byName
? getEntityByName(expectedTeam.getName(), fields, ADMIN_AUTH_HEADERS)
: getEntity(expectedTeam.getId(), fields, ADMIN_AUTH_HEADERS);
assertNotNull(getTeam.getProfile());
validateEntityReference(getTeam.getUsers());
validateEntityReference(getTeam.getOwns());
validateEntityReference(getTeam.getUsers());
validateEntityReference(getTeam.getDefaultRoles());
}
@Override
@ -252,14 +293,21 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
getEntityInterface(team), createRequest.getDescription(), TestUtils.getPrincipal(authHeaders), null);
assertEquals(createRequest.getProfile(), team.getProfile());
TestUtils.validateEntityReference(team.getOwns());
List<EntityReference> expectedUsers = new ArrayList<>();
for (UUID teamId : Optional.ofNullable(createRequest.getUsers()).orElse(Collections.emptyList())) {
expectedUsers.add(new EntityReference().withId(teamId).withType(Entity.USER));
for (UUID userId : Optional.ofNullable(createRequest.getUsers()).orElse(Collections.emptyList())) {
expectedUsers.add(new EntityReference().withId(userId).withType(Entity.USER));
}
expectedUsers = expectedUsers.isEmpty() ? null : expectedUsers;
TestUtils.assertEntityReferenceList(expectedUsers, team.getUsers());
TestUtils.validateEntityReference(team.getOwns());
List<EntityReference> expectedDefaultRoles = new ArrayList<>();
for (UUID roleId : Optional.ofNullable(createRequest.getDefaultRoles()).orElse(Collections.emptyList())) {
expectedDefaultRoles.add(new EntityReference().withId(roleId).withType(Entity.ROLE));
}
expectedDefaultRoles = expectedDefaultRoles.isEmpty() ? null : expectedDefaultRoles;
TestUtils.assertEntityReferenceList(expectedDefaultRoles, team.getDefaultRoles());
}
@Override
@ -287,15 +335,17 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
assertEquals(expected.getDisplayName(), updated.getDisplayName());
assertEquals(expected.getProfile(), updated.getProfile());
TestUtils.validateEntityReference(updated.getOwns());
List<EntityReference> expectedUsers = Optional.ofNullable(expected.getUsers()).orElse(Collections.emptyList());
List<EntityReference> actualUsers = Optional.ofNullable(updated.getUsers()).orElse(Collections.emptyList());
actualUsers.forEach(TestUtils::validateEntityReference);
TestUtils.assertEntityReferenceList(expectedUsers, actualUsers);
actualUsers.sort(EntityUtil.compareEntityReference);
expectedUsers.sort(EntityUtil.compareEntityReference);
assertEquals(expectedUsers, actualUsers);
TestUtils.validateEntityReference(updated.getOwns());
List<EntityReference> expectedDefaultRoles =
Optional.ofNullable(expected.getDefaultRoles()).orElse(Collections.emptyList());
List<EntityReference> actualDefaultRoles =
Optional.ofNullable(updated.getDefaultRoles()).orElse(Collections.emptyList());
TestUtils.assertEntityReferenceList(expectedDefaultRoles, actualDefaultRoles);
}
@Override
@ -308,11 +358,11 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
if (expected == actual) {
return;
}
if (fieldName.equals("users")) {
if (fieldName.equals("users") || fieldName.equals("defaultRoles")) {
@SuppressWarnings("unchecked")
List<EntityReference> expectedUsers = (List<EntityReference>) expected;
List<EntityReference> actualUsers = JsonUtils.readObjects(actual.toString(), EntityReference.class);
assertEntityReferencesFieldChange(expectedUsers, actualUsers);
List<EntityReference> expectedRefs = (List<EntityReference>) expected;
List<EntityReference> actualRefs = JsonUtils.readObjects(actual.toString(), EntityReference.class);
assertEntityReferencesFieldChange(expectedRefs, actualRefs);
} else {
assertCommonFieldChange(fieldName, expected, actual);
}

View File

@ -7,5 +7,5 @@ Provides metadata version information.
from incremental import Version
__version__ = Version("metadata", 0, 9, 0, dev=11)
__version__ = Version("metadata", 0, 9, 0, dev=12)
__all__ = ["__version__"]