Skip to content

Commit

Permalink
Merge pull request #268 from juliamcclellan/project_builder
Browse files Browse the repository at this point in the history
Project spec
  • Loading branch information
pcattori committed Aug 16, 2019
2 parents 850f09e + f1db4ff commit 18dff0e
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [#222](https://github.com/Datatamer/unify-client-python/issues/222) Dataset spec to update an existing dataset
- [#225](https://github.com/Datatamer/unify-client-python/issues/225) Attribute configuration spec to update an existing attribute configuration
- [#223](https://github.com/Datatamer/unify-client-python/issues/223) Update an attribute with an attribute spec
- [#224](https://github.com/Datatamer/unify-client-python/issues/224) Project spec to update a project

**BUG FIXES**
- [#235](https://github.com/Datatamer/unify-client-python/issues/235) Making `AttributeCollection` retrieve attributes directly instead of by streaming
Expand Down
6 changes: 6 additions & 0 deletions docs/developer-interface.rst
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ Project
.. autoclass:: tamr_unify_client.project.resource.Project
:members:

Project Spec
^^^^^^^^^^^^

.. autoclass:: tamr_unify_client.project.resource.ProjectSpec
:members:

Project Collection
^^^^^^^^^^^^^^^^^^

Expand Down
61 changes: 61 additions & 0 deletions tamr_unify_client/project/attribute_mapping/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,64 @@ def __repr__(self):
f"unified_dataset_name={self.unified_dataset_name!r}, "
f"unified_attribute_name={self.unified_attribute_name!r})"
)


class AttributeMappingSpec:
def __init__(self, client, data, api_path):
self.client = client
self._data = data
self.api_path = api_path

def from_data(self, data):
return AttributeMappingSpec(self.client, data, self.api_path)

def with_input_attribute_id(self, new_input_attribute_id):
""":type: str"""
return self.from_data(
{**self._data, "inputAttributeId": new_input_attribute_id}
)

def with_relative_input_attribute_id(self, new_relative_input_attribute_id):
""":type: str"""
return self.from_data(
{**self._data, "relativeInputAttributeId": new_relative_input_attribute_id}
)

def with_input_dataset_name(self, new_input_dataset_name):
""":type: str"""
return self.from_data(
{**self._data, "inputDatasetName": new_input_dataset_name}
)

def with_input_attribute_name(self, new_input_attribute_name):
""":type: str"""
return self.from_data(
{**self._data, "inputAttributeName": new_input_attribute_name}
)

def with_unified_attribute_id(self, new_unified_attribute_id):
""":type: str"""
return self.from_data(
{**self._data, "unifiedAttributeId": new_unified_attribute_id}
)

def with_relative_unified_attribute_id(self, new_relative_unified_attribute_id):
""":type: str"""
return self.from_data(
{
**self._data,
"relativeUnifiedAttributeId": new_relative_unified_attribute_id,
}
)

def with_unified_dataset_name(self, new_unified_dataset_name):
""":type: str"""
return self.from_data(
{**self._data, "unifiedDatasetName": new_unified_dataset_name}
)

def with_unified_attribute_name(self, new_unified_attribute_name):
""":type: str"""
return self.from_data(
{**self._data, "unifiedAttributeName": new_unified_attribute_name}
)
127 changes: 127 additions & 0 deletions tamr_unify_client/project/resource.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from copy import deepcopy

from tamr_unify_client.base_resource import BaseResource
from tamr_unify_client.dataset.collection import DatasetCollection
from tamr_unify_client.dataset.resource import Dataset
Expand Down Expand Up @@ -153,6 +155,14 @@ def attribute_mappings(self):
info = AttributeMappingCollection(self.client, alias)
return info

def spec(self):
"""Returns this project's spec.
:return: The spec for the project.
:rtype: :class:`~tamr_unify_client.project.resource.ProjectSpec`
"""
return ProjectSpec.of(self)

def __repr__(self):
return (
f"{self.__class__.__module__}."
Expand All @@ -161,3 +171,120 @@ def __repr__(self):
f"name={self.name!r}, "
f"type={self.type!r})"
)


class ProjectSpec:
"""A representation of the server view of a project."""

def __init__(self, client, data, api_path):
self.client = client
self._data = data
self.api_path = api_path

@staticmethod
def of(resource):
"""Creates a project spec from a project.
:param resource: The existing project.
:type resource: :class:`~tamr_unify_client.project.resource.Project`
:return: The corresponding project spec.
:rtype: :class:`~tamr_unify_client.project.resource.ProjectSpec`
"""
return ProjectSpec(resource.client, deepcopy(resource._data), resource.api_path)

@staticmethod
def new():
"""Creates a blank spec that could be used to construct a new project.
:return: The empty spec.
:rtype: :class:`~tamr_unify_client.project.resource.ProjectSpec`
"""
return ProjectSpec(None, {}, None)

def from_data(self, data):
"""Creates a spec with the same client and API path as this one, but new data.
:param data: The data for the new spec.
:type data: dict
:return: The new spec.
:rtype: :class:`~tamr_unify_client.project.resource.ProjectSpec`
"""
return ProjectSpec(self.client, data, self.api_path)

def to_dict(self):
"""Returns a version of this spec that conforms to the API representation.
:returns: The spec's dict.
:rtype: dict
"""
return deepcopy(self._data)

def with_name(self, new_name):
"""Creates a new spec with the same properties, updating name.
:param new_name: The new name.
:type new_name: str
:return: The new spec.
:rtype: :class:`~tamr_unify_client.project.resource.ProjectSpec`
"""
return self.from_data({**self._data, "name": new_name})

def with_description(self, new_description):
"""Creates a new spec with the same properties, updating description.
:param new_description: The new description.
:type new_description: str
:return: The new spec.
:rtype: :class:`~tamr_unify_client.project.resource.ProjectSpec`
"""
return self.from_data({**self._data, "description": new_description})

def with_type(self, new_type):
"""Creates a new spec with the same properties, updating type.
:param new_type: The new type.
:type new_type: str
:return: The new spec.
:rtype: :class:`~tamr_unify_client.project.resource.ProjectSpec`
"""
return self.from_data({**self._data, "type": new_type})

def with_external_id(self, new_external_id):
"""Creates a new spec with the same properties, updating external ID.
:param new_external_id: The new external ID.
:type new_external_id: str
:return: The new spec.
:rtype: :class:`~tamr_unify_client.project.resource.ProjectSpec`
"""
return self.from_data({**self._data, "externalId": new_external_id})

def with_unified_dataset_name(self, new_unified_dataset_name):
"""Creates a new spec with the same properties, updating unified dataset name.
:param new_unified_dataset_name: The new unified dataset name.
:type new_unified_dataset_name: str
:return: The new spec.
:rtype: :class:`~tamr_unify_client.project.resource.ProjectSpec`
"""
return self.from_data(
{**self._data, "unifiedDatasetName": new_unified_dataset_name}
)

def put(self):
"""Commits these changes by updating the project in Tamr.
:return: The updated project.
:rtype: :class:`~tamr_unify_client.project.resource.Project`
"""
updated_json = (
self.client.put(self.api_path, json=self._data).successful().json()
)
return Project.from_json(self.client, updated_json, self.api_path)

def __repr__(self):
return (
f"{self.__class__.__module__}."
f"{self.__class__.__qualname__}("
f"dict={self._data})"
)
79 changes: 65 additions & 14 deletions tests/unit/test_create_project.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,82 @@
from functools import partial
import json

import responses

from tamr_unify_client import Client
from tamr_unify_client.auth import UsernamePasswordAuth
from tamr_unify_client.project.resource import ProjectSpec

auth = UsernamePasswordAuth("username", "password")
tamr = Client(auth)

creation_spec = {
"name": "Project 1",
"description": "Mastering Project",
"type": "DEDUP",
"unifiedDatasetName": "Project 1 - Unified Dataset",
"externalId": "Project1",
}

project_json = {
**creation_spec,
"id": "unify://unified-data/v1/projects/1",
"created": {
"username": "admin",
"time": "2018-09-10T16:06:20.636Z",
"version": "project 1 created version",
},
"lastModified": {
"username": "admin",
"time": "2018-09-10T16:06:20.851Z",
"version": "project 1 modified version",
},
"relativeId": "projects/1",
}

projects_url = f"http://localhost:9100/api/versioned/v1/projects"
project_url = f"{projects_url}/1"


@responses.activate
def test_create_project():
creation_spec = {
"name": "Project 1",
"description": "Mastering Project",
"type": "DEDUP",
"unifiedDatasetName": "Project 1 - Unified Dataset",
"externalId": "Project1",
"resourceId": "1",
}
def create_callback(request, snoop):
snoop["payload"] = json.loads(request.body)
return 204, {}, json.dumps(project_json)

projects_url = f"http://localhost:9100/api/versioned/v1/projects"
project_url = f"http://localhost:9100/api/versioned/v1/projects/1"

responses.add(responses.POST, projects_url, json=creation_spec, status=204)
responses.add(responses.GET, project_url, json=creation_spec)
snoop_dict = {}
responses.add_callback(
responses.POST, projects_url, partial(create_callback, snoop=snoop_dict)
)
responses.add(responses.GET, project_url, json=project_json)

u = tamr.projects.create(creation_spec)
p = tamr.projects.by_resource_id("1")
assert print(p) == print(u)

assert snoop_dict["payload"] == creation_spec
assert p.__repr__() == u.__repr__()


@responses.activate
def test_create_from_spec():
def create_callback(request, snoop):
snoop["payload"] = json.loads(request.body)
return 204, {}, json.dumps(project_json)

snoop_dict = {}
responses.add_callback(
responses.POST, projects_url, partial(create_callback, snoop=snoop_dict)
)

spec = (
ProjectSpec.new()
.with_name(creation_spec["name"])
.with_description(creation_spec["description"])
.with_type(creation_spec["type"])
.with_unified_dataset_name(creation_spec["unifiedDatasetName"])
.with_external_id(creation_spec["externalId"])
)
p = tamr.projects.create(spec.to_dict())

assert snoop_dict["payload"] == creation_spec
assert p.relative_id == project_json["relativeId"]
59 changes: 59 additions & 0 deletions tests/unit/test_project.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from functools import partial
import json
from unittest import TestCase

import responses
Expand Down Expand Up @@ -130,6 +132,44 @@ def test_return_attribute_mapping(self):
self.mappings_json[0]["unifiedDatasetName"],
)

@responses.activate
def test_update_project(self):
def create_callback(request, snoop):
snoop["payload"] = request.body
return 200, {}, json.dumps(self._updated_project_json)

project_url = "http://localhost:9100/api/versioned/v1/projects/1"
snoop_dict = {}
responses.add_callback(
responses.PUT, project_url, partial(create_callback, snoop=snoop_dict)
)
project = Project(self.tamr, self.project_json[0])

temp_spec = project.spec().with_name(self._updated_project_json["name"])
new_project = (
temp_spec.with_description(self._updated_project_json["description"])
.with_external_id(self._updated_project_json["externalId"])
.put()
)
self.assertEqual(new_project.name, self._updated_project_json["name"])
self.assertEqual(
new_project.description, self._updated_project_json["description"]
)
self.assertEqual(
new_project.external_id, self._updated_project_json["externalId"]
)

self.assertEqual(json.loads(snoop_dict["payload"]), self._updated_project_json)

self.assertEqual(project.name, self.project_json[0]["name"])
self.assertEqual(project.description, self.project_json[0]["description"])
self.assertEqual(project.external_id, self.project_json[0]["externalId"])

# test that intermediate didn't change
self.assertEqual(
temp_spec.to_dict()["description"], self.project_json[0]["description"]
)

dataset_external_id = "1"
datasets_url = f"http://localhost:9100/api/versioned/v1/datasets?filter=externalId=={dataset_external_id}"
dataset_json = [
Expand Down Expand Up @@ -438,3 +478,22 @@ def test_return_attribute_mapping(self):
"unifiedAttributeName": "given_name",
},
]
_updated_project_json = {
"id": "unify://unified-data/v1/projects/1",
"externalId": "new external ID",
"name": "Renamed!",
"description": "project 1 description is more descriptive",
"type": "DEDUP",
"unifiedDatasetName": "project 1 unified dataset",
"created": {
"username": "admin",
"time": "2018-09-10T16:06:20.636Z",
"version": "project 1 created version",
},
"lastModified": {
"username": "admin",
"time": "2018-09-10T16:06:20.851Z",
"version": "project 1 modified version",
},
"relativeId": "projects/1",
}

0 comments on commit 18dff0e

Please sign in to comment.